main: fold the two infohash-dedup paths into one

dedupeTorrents and markInfoHash were the same map-of-infohash logic, one
batch and one incremental, with the same hazard explained in prose four
times. Both now go through a single seenInfoHash helper.
This commit is contained in:
2026-06-22 08:27:34 +09:00
parent a40cc67c38
commit b98c36acb7
3 changed files with 28 additions and 40 deletions

Binary file not shown.

62
main.go
View File

@@ -187,13 +187,27 @@ func (f *failedDownload) Stat() download.Stat {
return download.Stat{Name: f.name, Status: download.Errored, Total: -1} return download.Stat{Name: f.name, Status: download.Errored, Total: -1}
} }
// dedupeTorrents finds torrent/magnet sources that name the same torrent as an // seenInfoHash records source under its torrent infohash in seen and reports
// earlier source (same infohash), returning each duplicate URI mapped to the // whether that infohash was already recorded, returning the source that first
// earlier source it repeats. HTTP(S) URLs and any source whose infohash can't be // claimed it. The shared client keys torrents by infohash and does not refcount,
// read without a network fetch are never duplicates here. The shared client keys // so two sources naming one torrent would have the first to finish Drop it out
// torrents by infohash and does not refcount, so build() turns each duplicate // from under the other; callers fail the duplicate instead. A source whose
// into a failed download (duplicate infohash) rather than letting two jobs // infohash can't be known without a network fetch (an HTTP URL), or that is
// share — and Drop — one torrent. // v2-only or unreadable, is never a duplicate: it isn't recorded, so it runs.
func seenInfoHash(seen map[metainfo.Hash]string, source string, isFile bool) (first string, dup bool) {
h, ok := bt.SourceInfoHash(source, isFile)
if !ok {
return "", false
}
if first, dup = seen[h]; dup {
return first, true
}
seen[h] = source
return "", false
}
// dedupeTorrents finds torrent/magnet sources that repeat an earlier source's
// infohash, returning each duplicate URI mapped to the source it repeats.
func dedupeTorrents(uris []string, follow bool) map[string]string { func dedupeTorrents(uris []string, follow bool) map[string]string {
seen := map[metainfo.Hash]string{} seen := map[metainfo.Hash]string{}
dup := map[string]string{} dup := map[string]string{}
@@ -202,37 +216,13 @@ func dedupeTorrents(uris []string, follow bool) map[string]string {
if k != kindMagnet && k != kindTorrent { if k != kindMagnet && k != kindTorrent {
continue continue
} }
h, ok := bt.SourceInfoHash(u, k == kindTorrent) if first, isDup := seenInfoHash(seen, u, k == kindTorrent); isDup {
if !ok {
continue
}
if first, isDup := seen[h]; isDup {
dup[u] = first dup[u] = first
} else {
seen[h] = u
} }
} }
return dup return dup
} }
// markInfoHash records the v1 infohash of the .torrent at path in seen and
// reports whether it repeats one already seen, returning the path first recorded
// under that infohash. Callers fail the duplicate rather than let two jobs share
// one torrent on the client (see bt.SourceInfoHash for why that aliasing is
// unsafe). A v2-only or unreadable infohash is never a duplicate — the dedup
// can't see it, so it runs.
func markInfoHash(seen map[metainfo.Hash]string, path string) (first string, dup bool) {
h, ok := bt.SourceInfoHash(path, true)
if !ok {
return "", false
}
if first, dup = seen[h]; dup {
return first, true
}
seen[h] = path
return "", false
}
func build(opts *cli.Options, targets []target) ([]job, *torrent.Client, error) { func build(opts *cli.Options, targets []target) ([]job, *torrent.Client, error) {
flat := allURIs(targets) flat := allURIs(targets)
// Two sources naming the same torrent (a magnet and its own .torrent, say) // Two sources naming the same torrent (a magnet and its own .torrent, say)
@@ -383,11 +373,9 @@ func followUps(jobs []job, client *torrent.Client, bo bt.Options) []job {
if _, err := bt.Files(path); err != nil { if _, err := bt.Files(path); err != nil {
continue // downloaded file is not a valid torrent continue // downloaded file is not a valid torrent
} }
// Two fetched .torrent files that name the same torrent would share one // build() dedupes pass-1 sources but cannot reach a not-yet-fetched HTTP
// torrent on the refcount-less client: the first to finish Drops it out // .torrent, so dedupe the fetched ones here too against the same hazard.
// from under the other, stranding it at 0%. build() dedupes pass-1 sources if first, dup := seenInfoHash(seen, path, true); dup {
// but cannot reach a not-yet-fetched HTTP .torrent, so dedupe here too.
if first, dup := markInfoHash(seen, path); dup {
out = append(out, job{source: path, dl: &failedDownload{ out = append(out, job{source: path, dl: &failedDownload{
name: path, name: path,
err: fmt.Errorf("%w: same torrent as %s", errDuplicate, first), err: fmt.Errorf("%w: same torrent as %s", errDuplicate, first),

View File

@@ -241,13 +241,13 @@ func TestFollowUpInfoHashDedup(t *testing.T) {
} }
seen := map[metainfo.Hash]string{} seen := map[metainfo.Hash]string{}
if first, dup := markInfoHash(seen, a); dup { if first, dup := seenInfoHash(seen, a, true); dup {
t.Fatalf("a is the first occurrence, wrongly flagged a duplicate of %s", first) t.Fatalf("a is the first occurrence, wrongly flagged a duplicate of %s", first)
} }
if first, dup := markInfoHash(seen, b); !dup || first != a { if first, dup := seenInfoHash(seen, b, true); !dup || first != a {
t.Errorf("b should duplicate a: got first=%q dup=%v, want %q true", first, dup, a) t.Errorf("b should duplicate a: got first=%q dup=%v, want %q true", first, dup, a)
} }
if _, dup := markInfoHash(seen, c); dup { if _, dup := seenInfoHash(seen, c, true); dup {
t.Errorf("c is a distinct torrent, wrongly flagged a duplicate") t.Errorf("c is a distinct torrent, wrongly flagged a duplicate")
} }
} }