Skip to content

fix(download): do not cancel a queued torrent as stalled - #1420

Merged
calibrain merged 4 commits into
calibrain:mainfrom
splitsec2:fix/queued-torrent-not-stalled
Oct 3, 2026
Merged

calibrain merged 4 commits into
calibrain:mainfrom
splitsec2:fix/queued-torrent-not-stalled

Conversation

@splitsec2

@splitsec2 splitsec2 commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

qBittorrent only runs a few downloads at once (max_active_downloads defaults to 3) and holds the rest as queuedDL. Shelfmark's stall timer cancels a download after five minutes without a changed status or progress. A queued torrent reports "Queued" on every poll, so it looks idle, and Shelfmark cancels a download that was only waiting for a slot.

I run Shelfmark against the same qBittorrent that Sonarr, Radarr and Lidarr use, which I assume is a common setup. Their torrents and Shelfmark's share one limit, so anything that fills the slots makes Shelfmark's torrent wait. In my case three torrents that never fetched metadata held every slot. I'd expect a busy Sonarr queue to hold them the same way, but I haven't reproduced that. Once the stall timer had cancelled the waiting downloads they stayed in the client, still queued behind the same blockers. On my instance 10 AudioBookBay downloads were cancelled while qBittorrent still had them queued.

This changes the torrent poll loop. When the client reports a queued torrent, it asks for an activity grace (the mechanism html_get_page already uses for protection bypasses) and releases it as soon as the torrent leaves the queue. The grace is renewed every ten minutes and stops at two hours, so a queue that never moves still ends. A torrent that is downloading and moving is timed as before until the new window below applies.

The second commit sets the stall message before the cancel runs. The terminal hook copies the message into the history row, and it was set afterwards, so a download the timer ended was recorded with the last poll's "Queued". That left no way to tell it apart from a person cancelling.

The last two commits are about slow torrents. The timer only counts a change in progress, so a torrent that needs more than five minutes to fetch metadata or find its first peer is cancelled before it has any progress to count, and one that moves a few megabytes at a time can be cancelled between bursts. I hit this with a torrent that had a single peer. A torrent now gets one 15 minute window the first time it is seen outside the queue, and each step forward in progress renews it. One that makes no progress for 15 minutes is still cancelled, so a dead torrent now takes about 15 minutes to end instead of five. That trade is the part I'm least sure about, so the number is easy to change.

Tests cover the grace being requested once, released when the torrent starts, renewed, and capped, the order of the message and the cancel, and the start and movement windows. The full suite passes, and I broke each rule on purpose to check a test catches it.

The two hour ceiling is a guess. If you'd rather have a setting for it, or a different number, I'm happy to change it.

qBittorrent only runs a few downloads at once (3 by default) and holds the rest as
queuedDL. A queued torrent reports the same status on every poll, and the stall
timer only counts a changed status or progress as activity, so a torrent that was
just waiting for a slot was cancelled after five minutes.

A queued torrent now asks for an activity grace, renewed every ten minutes, and
releases it as soon as it leaves the queue. There is a two hour ceiling so a queue
that never moves still ends.
The terminal hook copies the task's status message into the history row, and the
stall timer set its message after cancelling. The row kept whatever the last poll
said ("Queued"), so a download the timer ended looked the same as one a person
cancelled. The message is now set before the cancel.
The stall timer waits five minutes for a change in progress. Torrents are jerky: a
swarm with one seed can sit still for several minutes and then carry on, so a slow
but live download was cancelled and left behind.

Each time a torrent's progress goes up it now asks for a 15 minute activity grace,
which restarts from that moment, so a torrent is only called stalled 15 minutes after
it last moved. One that never moved (a magnet that cannot fetch metadata) keeps the
five minute window, and one that has stopped does not get more time.
The orchestrator's 300s stall timer cancelled torrents that needed longer
than five minutes to fetch metadata or find a first peer, before any
progress existed to extend the window. A torrent now gets one
MOVING_STALL_SECONDS grace the first time it is seen outside the queue,
and each later step forward renews it as before.
@calibrain

Copy link
Copy Markdown
Owner

Thank you

@calibrain
calibrain merged commit 88f73e3 into calibrain:main Oct 3, 2026
14 of 17 checks passed
calibrain pushed a commit that referenced this pull request Oct 3, 2026
Cancelling a torrent download leaves the torrent in the client.
`_safe_remove_download` documents the rule: "torrents: never remove or
delete client data (avoid breaking seeding)". That makes sense for a
torrent that has data. A torrent that is still at 0% has nothing to seed
and nothing to resume, and leaving it behind holds a download slot in
qBittorrent's queue or sits there as a dead entry.

I hit this on a qBittorrent that is shared with Sonarr, Radarr and
Lidarr and has a download limit. Shelfmark cancelled downloads that were
queued (#1420) and left them in the client. Three torrents that never
fetched metadata held every active slot, and each torrent added after
them waited behind them. Removing those by hand freed the slots.

This removes the torrent and its files when a download is cancelled,
Shelfmark added the torrent, and the client reports 0% progress. A
torrent with any data is left alone as before. So is one that was
already in the client when the download started, since Shelfmark didn't
add it. If the removal fails it is logged and the cancel carries on.

Tests cover removal at 0% for a queued and a downloading torrent,
leaving one that has data, leaving one the user already had, and a
failing removal. The full suite passes, and I broke each rule on purpose
to check a test catches it.

This changes a rule you wrote down, so I've kept it apart from the queue
fix. If you'd rather have it as a setting, or only remove a torrent that
is still queued or fetching metadata, I'm happy to rework it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants