diff --git a/backend/src/media_library_viewer_api/clients/qbittorrent.py b/backend/src/media_library_viewer_api/clients/qbittorrent.py index 81c8af5..57d695b 100644 --- a/backend/src/media_library_viewer_api/clients/qbittorrent.py +++ b/backend/src/media_library_viewer_api/clients/qbittorrent.py @@ -220,7 +220,12 @@ class QbittorrentClient: if fields is None: snap["torrents"].pop(hash_, None) else: - snap["torrents"][hash_] = fields + previous = snap["torrents"].get(hash_) + snap["torrents"][hash_] = ( + {**previous, **fields} + if isinstance(previous, dict) and isinstance(fields, dict) + else fields + ) for hash_ in update.get("torrents_removed") or []: snap["torrents"].pop(hash_, None) categories = update.get("categories") diff --git a/backend/tests/test_qbittorrent_client.py b/backend/tests/test_qbittorrent_client.py index e30d6bb..f3776e8 100644 --- a/backend/tests/test_qbittorrent_client.py +++ b/backend/tests/test_qbittorrent_client.py @@ -125,13 +125,21 @@ class QbittorrentClientTests(unittest.TestCase): "rid": 10, "full_update": True, "server_state": {"dl_info_speed": 100}, - "torrents": {"a": {"name": "A", "state": "downloading"}}, + "torrents": { + "a": { + "name": "A", + "state": "downloading", + "size": 1_024, + "progress": 0.5, + "dlspeed": 100, + } + }, } partial = { "rid": 11, "full_update": False, "server_state": {"dl_info_speed": 200}, - "torrents": {"a": {"name": "A", "state": "pausedDL"}}, + "torrents": {"a": {"dlspeed": 200}}, } self.session.get.side_effect = [self._get_response(full), self._get_response(partial)] @@ -143,7 +151,11 @@ class QbittorrentClientTests(unittest.TestCase): r2 = self.client.maindata() self.assertEqual(self.session.get.call_args_list[1].kwargs["params"].get("rid"), 10) self.assertEqual(r2["server_state"]["dl_info_speed"], 200) # merged - self.assertEqual(r2["torrents"]["a"]["state"], "pausedDL") # merged + self.assertEqual(r2["torrents"]["a"]["dlspeed"], 200) + self.assertEqual(r2["torrents"]["a"]["name"], "A") + self.assertEqual(r2["torrents"]["a"]["state"], "downloading") + self.assertEqual(r2["torrents"]["a"]["size"], 1_024) + self.assertEqual(r2["torrents"]["a"]["progress"], 0.5) def test_maindata_caches_concurrent_calls_within_ttl(self) -> None: """Two calls within the TTL collapse to a single HTTP fetch.""" @@ -227,8 +239,8 @@ class QbittorrentClientTests(unittest.TestCase): self.session.post.return_value = self._login_response() self.client._login() call_kwargs = self.session.post.call_args.kwargs - assert call_kwargs["timeout"] == (5.0, 5.0) - assert not isinstance(call_kwargs["timeout"], int) + self.assertEqual(call_kwargs["timeout"], (5.0, 5.0)) + self.assertNotIsInstance(call_kwargs["timeout"], int) def test_login_fails_message_names_bad_credentials(self) -> None: """'Fails.' body yields a clear 'invalid username or password' error.""" diff --git a/docs/REQUIREMENTS.md b/docs/REQUIREMENTS.md index 126780b..343a008 100644 --- a/docs/REQUIREMENTS.md +++ b/docs/REQUIREMENTS.md @@ -321,6 +321,9 @@ These do not reference a service. - The scheduler should run immediately after startup with per-service staggering, use fixed-delay execution, prevent overlap/backlog, and reconcile configuration changes without a backend restart. - Poll failures should remain enabled, be persisted, and retry with bounded exponential backoff. A successful scheduled or manual run should clear backoff. - The qBittorrent widget-data endpoint must become read-only; only the scheduler may contact qBittorrent and append samples. +- The qBittorrent client must merge incremental torrent patches with the prior + snapshot so active-transfer rows retain their name, size, progress, and state + when only throughput changes. - The service UI should expose polling settings, current status, stale-data state, a manual `Run now` action, the shared selectable chart windows, an **All values** option that fetches every retained speed sample, and paginated scheduled-action history. - Scheduled-action runs should use dedicated generic records, retain at most 30 days or 1,000 runs per service/action, and never store secrets or raw credentials. - Disabling a qBittorrent service pauses polling while retaining history; deleting the service purges its samples and scheduler history through the existing cascade-delete behavior.