fix(qbittorrent): map the qBittorrent 5 stopped states and forcedMetaDL (#1570)

On qBittorrent 5 a stopped torrent shows up as an unknown state. 5.0
renamed `pausedDL` and `pausedUP` to `stoppedDL` and `stoppedUP`, and
`get_status` only knows the old names, so the torrent comes back as
state `unknown` with the raw state name as its message. The paused
handling in the download handler never applies to it, for example the
grace it gives a torrent that has not started. `forcedMetaDL` is missing
from the same table, so a force-started magnet fetching its metadata
also shows up as unknown, although it is already in the
incomplete-states set and the metadata wait.

The three missing states are now mapped like their neighbours: the two
stopped states as paused, `forcedMetaDL` as downloading with "Fetching
metadata". The state names are the ones `qbittorrent-api` lists in its
`TorrentState` enum.

Tests: one parametrized test covers the old and new paused names and
`forcedMetaDL` (the two old paused names are there as controls). The
three new cases fail without the change. Full suite passes.
This commit is contained in:
splitsec2 authored and GitHub committed 2026-10-26 02:44:19 -06:00
1 parent 5d94e96bd9
commit 72b094db7a
2 files changed
+48

No files matched your search

@@ -639,6 +639,7 @@ class QBittorrentClient(DownloadClient):
), # None = use default progress message
"stalledDL": ("downloading", "Stalled"),
"metaDL": ("downloading", "Fetching metadata"),
"forcedMetaDL": ("downloading", "Fetching metadata"),
"forcedDL": ("downloading", None),
"allocating": ("downloading", "Allocating space"),
"uploading": ("seeding", "Seeding"),
@@ -646,6 +647,9 @@ class QBittorrentClient(DownloadClient):
"forcedUP": ("seeding", "Seeding"),
"pausedDL": ("paused", "Paused"),
"pausedUP": ("paused", "Paused"),
# qBittorrent 5 renamed "paused" to "stopped"
"stoppedDL": ("paused", "Paused"),
"stoppedUP": ("paused", "Paused"),
"queuedDL": ("queued", "Queued"),
"queuedUP": ("queued", "Queued"),
"checkingDL": ("checking", "Checking files"),
+44
View File
@@ -538,6 +538,50 @@ class TestQBittorrentClientGetStatus:
assert status.state_value == "complete"
assert status.file_path == "/downloads/completed.epub"
@pytest.mark.parametrize(
("torrent_state", "expected_state", "expected_message"),
[
("pausedDL", "paused", "Paused"),
("pausedUP", "paused", "Paused"),
("stoppedDL", "paused", "Paused"),
("stoppedUP", "paused", "Paused"),
("forcedMetaDL", "downloading", "Fetching metadata"),
],
)
def test_get_status_maps_paused_stopped_and_forced_metadata_states(
self, monkeypatch, torrent_state, expected_state, expected_message
):
"""qBittorrent 5 reports "stopped" where 4.x said "paused"; neither may read as unknown."""
config_values = {
"QBITTORRENT_URL": "http://localhost:8080",
"QBITTORRENT_USERNAME": "admin",
"QBITTORRENT_CATEGORY": "test",
}
monkeypatch.setattr(
"shelfmark.download.clients.qbittorrent.config.get",
lambda key, default="": config_values.get(key, default),
)
mock_torrent = MockTorrent(progress=0.3, state=torrent_state)
mock_client_instance = MagicMock()
mock_client_instance._session.get.return_value = create_mock_session_response(
[mock_torrent], status_code=200
)
mock_client_class = MagicMock(return_value=mock_client_instance)
with patch.dict("sys.modules", {"qbittorrentapi": MagicMock(Client=mock_client_class)}):
import importlib
import shelfmark.download.clients.qbittorrent as qb_module
importlib.reload(qb_module)
status = qb_module.QBittorrentClient().get_status("abc123")
assert status.state_value == expected_state
assert status.message == expected_message
assert status.complete is False
def test_get_status_full_progress_still_downloading_not_complete(self, monkeypatch):
"""rdt-client reports progress 1.0 while still finalising (state stays "downloading").