fix(transmission): identify downloads by info hash, not the session torrent id - #2711
Open
ccarpinteri wants to merge 1 commit into
Open
ccarpinteri wants to merge 1 commit into
ccarpinteri wants to merge 1 commit into
Conversation
This was referenced Sep 19, 2026
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
…orrent id torrent-add returns a numeric id that only lasts as long as the daemon session. Transmission renumbers every torrent when it restarts, but Bindery stored that id as the download's TorrentID and the poller looked downloads up by it. After a restart the id matches nothing, so the poller skips the row and the download sits at "downloading" for good while the torrent carries on seeding. Nothing imports it. Ids are also reused, so a stored id can come back pointing at a different torrent, and then the wrong payload is imported and the wrong torrent is acted on. This stores hashString instead, which stays the same for the life of the torrent, and looks downloads up by it. The RPC takes a hash anywhere it takes an id, so removal works off the same value. Stall detection and the queue's live progress overlay are keyed by hash too, since both compare against the stored value. Stall detection gets no numeric fallback because the caller removes what it matches; the overlay keeps one because it only draws a row. Rows written before this change still hold a numeric id. They are matched by comparing Transmission's addedDate with the download's grab time, which a restart does not change, and only when the pairing is clear: a torrent no download is close to is left alone, a torrent one download is close to is assigned, and a torrent several downloads are close to needs the release name to pick one or it is left for the user. The row is then rewritten to the hash, the same way the qBittorrent hash recovery in vavallee#939 works. The stale id is never used to find the torrent. Leaving a download stuck can be undone, removing the wrong torrent cannot. Terminal downloads are skipped: they will not be imported or removed again, so rewriting their identifier can only be wrong. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ccarpinteri
force-pushed
the
pr/transmission-info-hash
branch
from
September 19, 2026 03:09
0478af4 to
0893012
Compare
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.
Problem
torrent-addreturns a numeric id that is scoped to the daemon session. Transmission renumbers every torrent when it restarts. Bindery stores that id as the download'sTorrentIDand the poller looks downloads up by it, so every restart breaks the link.After a restart the stored id matches nothing.
checkTransmissionDownloadsskips the row, the download sits at "downloading" for good, and the torrent keeps seeding. Nothing ever imports it.Ids are also reused. A stored id can come back pointing at a different torrent, and then Bindery acts on the wrong one: the wrong payload is imported, and anything that removes by id removes a torrent the user never grabbed.
I hit this on my own install. Two downloads had been stranded for weeks:
Both torrents were healthy and sitting in the client the whole time. One of them was complete and ready to import.
qBittorrent does not have this problem because it stores the hash. Transmission is the only torrent client keyed on a value that changes.
Change
Store
hashString, which stays the same for the life of the torrent, and look downloads up by it. The RPC accepts a hash anywhere it accepts an id, so removal works off the same value with no extra lookup.Two other places compared the stored value against a map keyed by numeric id, and both are keyed by hash now:
GetStalledIDs, with no numeric fallback. The caller removes what this matches, so a stale id here would remove the wrong torrent.GetLiveStatuses, which keeps the numeric id as an extra key. That overlay only draws a queue row, so the worst a stale id can do is mislabel one.Rows written before this change
Those still hold a numeric id. They are matched by comparing Transmission's
addedDateagainst the download's grab time, which a restart does not change, and only when the pairing is clear:The row is then rewritten to the hash. This is the same recovery shape as the qBittorrent hash backfill in #939.
The stale id is never used to find the torrent. Leaving a download stuck can be undone; removing the wrong torrent cannot. Terminal downloads are skipped for the same reason: they will not be imported or removed again, so rewriting their identifier can only be wrong.
I got this wrong on the first pass in a way worth mentioning. An earlier version accepted any single torrent inside the time window without the name check, and on a live database seven already imported rows from one afternoon's grabs all matched the one torrent from that window that still existed. The mutual uniqueness rule and the terminal skip are there because of it, and
TestCheckTransmissionDownloads_LegacyBatchDoesNotCollapseOntoOneTorrentcovers exactly that case.Tests
New in
internal/importer/scanner_transmission_hash_test.go:+,_and.where a tracker put them instead of spacesExisting fixtures that asserted id based identity now assert hash based identity.
AddTorrentkeeps its signature and delegates to a newAddTorrentDetailedthat returns the whole torrent, so nothing else that calls it had to change.Full
importer,downloader,db,schedulerandapisuites pass. The two failures I see locally,TestFindCaseInsensitivePathUnderandTestDiagnose_CaseDivergenceWarns, also fail on a clean checkout of main on macOS.Running on my own install since this morning, with both stranded downloads recovered on the first poll.
Note on ordering
This is the first of three. The other two stack on it: polling the audiobook category, and the remove on import feature from #2046. That one needs this fix to be safe, since it removes the torrent it matched.
🤖 Generated with Claude Code