From 72b094db7a01a40ab0f8bbf65ed92649cd467d51 Mon Sep 17 00:00:00 2001 From: splitsec2 <35583321+splitsec2@users.noreply.github.com> Date: Mon, 26 Oct 2026 02:44:19 -0600 Subject: [PATCH] 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. --- shelfmark/download/clients/qbittorrent.py | 4 +++ tests/prowlarr/test_qbittorrent_client.py | 44 +++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/shelfmark/download/clients/qbittorrent.py b/shelfmark/download/clients/qbittorrent.py index 869697cd..3facb293 100644 --- a/shelfmark/download/clients/qbittorrent.py +++ b/shelfmark/download/clients/qbittorrent.py @@ -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"), diff --git a/tests/prowlarr/test_qbittorrent_client.py b/tests/prowlarr/test_qbittorrent_client.py index 4c97b33a..da7864b1 100644 --- a/tests/prowlarr/test_qbittorrent_client.py +++ b/tests/prowlarr/test_qbittorrent_client.py @@ -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").