Skip to content

fix(transmission): identify downloads by info hash, not the session torrent id - #2711

Open
ccarpinteri wants to merge 1 commit into
vavallee:mainfrom
ccarpinteri:pr/transmission-info-hash
Open

ccarpinteri wants to merge 1 commit into
vavallee:mainfrom
ccarpinteri:pr/transmission-info-hash

Conversation

@ccarpinteri

Copy link
Copy Markdown

Problem

torrent-add returns a numeric id that is scoped to the daemon session. Transmission renumbers every torrent when it restarts. Bindery stores that id as the download's TorrentID and the poller looks downloads up by it, so every restart breaks the link.

After a restart the stored id matches nothing. checkTransmissionDownloads skips 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:

download stored id actual id after restarts
audiobook grabbed 4 Sep 5 2
ebook grabbed 16 Aug 17 1

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 addedDate against the download's grab time, which a restart does not change, and only when the pairing is clear:

  • no download near a torrent's added time: leave the torrent alone
  • exactly one: assign it
  • several, which happens with a batch of grabs minutes apart: the release name has to pick out exactly one, otherwise leave it for the user

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_LegacyBatchDoesNotCollapseOntoOneTorrent covers exactly that case.

Tests

New in internal/importer/scanner_transmission_hash_test.go:

  • a download stranded by a renumbered id is recovered and rewritten to the hash
  • a stale id that now belongs to an unrelated torrent is ignored, and the row is left alone
  • a batch of downloads grabbed in one window does not collapse onto a single torrent
  • the release name comparison, which has to tolerate +, _ and . where a tracker put them instead of spaces

Existing fixtures that asserted id based identity now assert hash based identity. AddTorrent keeps its signature and delegates to a new AddTorrentDetailed that returns the whole torrent, so nothing else that calls it had to change.

Full importer, downloader, db, scheduler and api suites pass. The two failures I see locally, TestFindCaseInsensitivePathUnder and TestDiagnose_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

@codecov

codecov Bot commented Sep 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 78.94737% with 28 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/importer/scanner_poll.go 85.39% 8 Missing and 5 partials ⚠️
internal/downloader/adapter.go 61.90% 3 Missing and 5 partials ⚠️
internal/downloader/transmission/client.go 69.56% 7 Missing ⚠️

📢 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
ccarpinteri force-pushed the pr/transmission-info-hash branch from 0478af4 to 0893012 Compare September 19, 2026 03:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bindery-notified Discord notification already sent for this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant