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}
}
// dedupeTorrents finds torrent/magnet sources that name the same torrent as an
// earlier source (same infohash), returning each duplicate URI mapped to the
// earlier source it repeats. HTTP(S) URLs and any source whose infohash can't be
// read without a network fetch are never duplicates here. The shared client keys
// torrents by infohash and does not refcount, so build() turns each duplicate
// into a failed download (duplicate infohash) rather than letting two jobs
// share — and Drop — one torrent.
// seenInfoHash records source under its torrent infohash in seen and reports
// whether that infohash was already recorded, returning the source that first
// claimed it. The shared client keys torrents by infohash and does not refcount,
// so two sources naming one torrent would have the first to finish Drop it out
// from under the other; callers fail the duplicate instead. A source whose
// infohash can't be known without a network fetch (an HTTP URL), or that is
// 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 {
seen := map[metainfo.Hash]string{}
dup := map[string]string{}
@@ -202,37 +216,13 @@ func dedupeTorrents(uris []string, follow bool) map[string]string {
if k != kindMagnet && k != kindTorrent {
continue
}
h, ok := bt.SourceInfoHash(u, k == kindTorrent)
if !ok {
continue
}
if first, isDup := seen[h]; isDup {
if first, isDup := seenInfoHash(seen, u, k == kindTorrent); isDup {
dup[u] = first
} else {
seen[h] = u
}
}
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) {
flat := allURIs(targets)
// 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 {
continue // downloaded file is not a valid torrent
}
// Two fetched .torrent files that name the same torrent would share one
// torrent on the refcount-less client: the first to finish Drops it out
// from under the other, stranding it at 0%. build() dedupes pass-1 sources
// but cannot reach a not-yet-fetched HTTP .torrent, so dedupe here too.
if first, dup := markInfoHash(seen, path); dup {
// build() dedupes pass-1 sources but cannot reach a not-yet-fetched HTTP
// .torrent, so dedupe the fetched ones here too against the same hazard.
if first, dup := seenInfoHash(seen, path, true); dup {
out = append(out, job{source: path, dl: &failedDownload{
name: path,
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{}
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)
}
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)
}
if _, dup := markInfoHash(seen, c); dup {
if _, dup := seenInfoHash(seen, c, true); dup {
t.Errorf("c is a distinct torrent, wrongly flagged a duplicate")
}
}