fix: merge duplicate rows instead of dropping the losers' trackers - #175
Merged
Merged
Conversation
When the same infohash arrives from more than one source, dedupe kept the row with the most seeders and threw the rest away. That also threw away their magnets, and a magnet's announce list is part of what a source knows about the torrent: 1337x and EZTV hand back the torrent's own trackers, while rows built through buildMagnet() carry only torlink's eleven public defaults. So a 1337x row losing on seeders to a TPB row traded a working announce list for a generic one -- the same loss baairon#146 fixed for a .torrent file's own trackers, arriving by a different route. The healthiest row still wins and still supplies every visible field. What changes is that the rows it beats are folded into it: their trackers are appended to its magnet, and numFiles / added are backfilled only where the winner reported nothing. Appending, rather than rebuilding through buildMagnet(), keeps the winning URI byte for byte, so magnet parameters torlink does not write (xl on SubsPlease rows) survive. dedupe moves to src/ui/dedupe.ts next to sort.ts and filter.ts, the two other pure result-list transforms, so it can be tested directly.
baairon
added a commit
to funsaized/torlink
that referenced
this pull request
Aug 28, 2026
The branch added its own dedupe in src/sources/results.ts. baairon#175 landed dedupeResults in src/ui/dedupe.ts first, and that version folds the beaten rows' announce lists into the survivor instead of discarding them, so the JSON document carries merged trackers by using it. defaultOrder moves from useConcurrentSearch into src/ui/sort.ts, beside sortResults whose "none" case already preserved that order. The TUI and the headless command now order through one function, so the two cannot drift apart. src/index.tsx keeps its existing indentation. The branch re-indented the whole TUI block, which restated the alt-screen, signal-handler and uncaughtException paths in the diff without changing any of them.
7 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What and why
dedupe()inuseConcurrentSearch.tskept the row with the most seeders and dropped the rest:Dropping the row also drops its magnet, and a magnet's announce list is part of what that source knew about the torrent:
x1337.ts:74) and EZTV uses the API'smagnet_url(eztv.ts:36) — both carry the torrent's own trackers.buildMagnet(), which writes only torlink's eleven public defaults.So whenever those overlap — and TV and Movies overlap constantly, that is what dedupe is for — the higher-seeder row wins and can trade a working announce list for a generic one.
buildMagnetalready states the principle this breaks:It is the same loss #146 fixed for a
.torrentfile's own trackers, arriving by a different route. Two rows for one infohash is exactly the case where torlink has more information than either source alone, and the old code was the only place that spent it.numFilesandaddedgo the same way — EZTV reportsdate_released_unixand 1337x parses an upload date, so a YTS row with neither can win and leave the Added column blank for a torrent whose date was right there.What changed
The winner is chosen exactly as before, and still supplies every visible field. The rows it beats are folded into it rather than discarded:
??, so a backfill only fills a genuine gap — a field the winner never reported is missing information, not a decision to overrule.mergeMagnetTrackerslives insrc/sources/magnet.tsnext tobuildMagnet, and appends the missingtrvalues rather than rebuilding the URI. That keeps the winning magnet byte for byte, so parameters torlink does not write itself survive — SubsPlease magnets carryxl, and rebuilding throughbuildMagnet()would have silently dropped it. Order is not a concern: everytrin a magnet lands in one announce list and the client contacts all of them.Anything that is not a magnet is returned untouched, so a malformed URI from a source can never be corrupted further.
Why dedupe moved file
dedupewas a private function inside a React hook, which is why it had no test. It is a pure result-list transform, so it now sits insrc/ui/dedupe.tsbesidesrc/ui/sort.tsandsrc/ui/filter.ts— the two functions of exactly the same shape — and the hook keeps one import. No behaviour rides on the move.Tests
src/ui/dedupe.test.ts(new): distinct hashes are untouched; the healthiest row still supplies name/source/seeders; a losing 1337x row's private-tracker URL survives into the winning magnet; three sources on one hash produce one row and no repeated tracker;numFiles/addedbackfill only into gaps; a seeder tie keeps the first row, as before.src/sources/magnet.test.ts:mergeMagnetTrackersappends only what is missing, returns the primary unchanged when there is nothing to add, preservesxl, and leaves non-magnets alone.Checklist
npm run typecheckis cleannpm testpassesHELP_GROUPSandfooterHintsinsrc/ui/keymap.ts— n/a, no new keyStorefield, I updatedmakeStoreinscripts/render-previews-impl.tsx— n/a, no new Store fieldfeat:/fix:/docs:/chore:)