From 230b4b8533e8332770c0a9552a67108177d0827e Mon Sep 17 00:00:00 2001 From: Developer Date: Tue, 14 Jul 2026 17:20:27 +0000 Subject: [PATCH] fix: include all active torrent states --- .../integrations/qbittorrent.py | 2 +- .../widgets/sources.py | 14 +++++++++---- backend/tests/test_widgets.py | 10 ++++----- frontend/src/integrations/registry.ts | 3 ++- .../QbittorrentActiveTorrentsWidget.tsx | 21 +++++++++++++++---- .../src/widgets/QbittorrentTotalsWidget.tsx | 15 +++++++++---- 6 files changed, 46 insertions(+), 19 deletions(-) diff --git a/backend/src/media_library_viewer_api/integrations/qbittorrent.py b/backend/src/media_library_viewer_api/integrations/qbittorrent.py index 121d2ff..aaa73e1 100644 --- a/backend/src/media_library_viewer_api/integrations/qbittorrent.py +++ b/backend/src/media_library_viewer_api/integrations/qbittorrent.py @@ -117,7 +117,7 @@ DEFINITION = ServiceDefinition( widget_kind( kind="active", name="Active torrents", - description="Torrents currently downloading or uploading.", + description="All active download/upload work, including queued and stalled transfers.", model_cls=QbittorrentWidgetConfig, default_config={}, refresh_interval_ms=15_000, diff --git a/backend/src/media_library_viewer_api/widgets/sources.py b/backend/src/media_library_viewer_api/widgets/sources.py index f0aed93..40e5140 100644 --- a/backend/src/media_library_viewer_api/widgets/sources.py +++ b/backend/src/media_library_viewer_api/widgets/sources.py @@ -411,9 +411,10 @@ def _qbittorrent_client(cache_key: tuple[str, str, str, str, int]) -> Qbittorren _QBITTORRENT_DOWNLOAD_STATES = frozenset( - {"downloading", "forceddl", "stalleddl", "metadl", "allocating"} + {"downloading", "forceddl", "stalleddl", "queueddl", "metadl", "forcedmetadl", "allocating", "checkingdl"} ) -_QBITTORRENT_UPLOAD_STATES = frozenset({"uploading", "forcedup", "stalledup"}) +_QBITTORRENT_UPLOAD_STATES = frozenset({"uploading", "forcedup", "stalledup", "queuedup", "checkingup"}) +_QBITTORRENT_OTHER_ACTIVE_STATES = frozenset({"checkingresumedata", "moving"}) def _qbit_torrent_direction(torrent: dict[str, Any]) -> str | None: @@ -430,6 +431,11 @@ def _qbit_torrent_direction(torrent: dict[str, Any]) -> str | None: return None +def _qbit_torrent_is_active(torrent: dict[str, Any]) -> bool: + state = str(torrent.get("state") or "").lower() + return bool(_qbit_torrent_direction(torrent)) or state in _QBITTORRENT_OTHER_ACTIVE_STATES + + class QbittorrentWidgetSource: """Fetch qBittorrent data for totals, active, and speed widgets.""" @@ -492,9 +498,9 @@ class QbittorrentWidgetSource: if widget_kind == "active": active = [] for torrent in torrents.values(): - direction = _qbit_torrent_direction(torrent) - if not direction: + if not _qbit_torrent_is_active(torrent): continue + direction = _qbit_torrent_direction(torrent) active.append( { "name": torrent.get("name"), diff --git a/backend/tests/test_widgets.py b/backend/tests/test_widgets.py index 48faa9d..af002fe 100644 --- a/backend/tests/test_widgets.py +++ b/backend/tests/test_widgets.py @@ -1128,12 +1128,12 @@ async def test_qbittorrent_totals_counts_all_torrents(): assert result["by_state"]["uploading"] == 1 assert result["by_state"]["queuedDL"] == 1 assert result["by_state"]["pausedDL"] == 1 - assert result["by_direction"] == {"downloading": 2, "uploading": 2} + assert result["by_direction"] == {"downloading": 3, "uploading": 2} @pytest.mark.asyncio async def test_qbittorrent_active_filters_dl_ul_only(): - """Active kind returns only downloading/uploading torrents (Q3).""" + """Active kind returns all active download/upload states, including queued work.""" from media_library_viewer_api.widgets.sources import QbittorrentWidgetSource adapter = QbittorrentWidgetSource() @@ -1149,14 +1149,14 @@ async def test_qbittorrent_active_filters_dl_ul_only(): result = await adapter.fetch(service, "active", {}) active = result["torrents"] - assert len(active) == 4 + assert len(active) == 5 names = [t["name"] for t in active] assert "Movie.mkv" in names assert "Show.mkv" in names assert "Forced download" in names assert "Stalled upload" in names - # Queued and paused are excluded - assert "Queued" not in names + assert "Queued" in names + # Paused torrents remain excluded, but queued transfer work is visible. assert "Paused" not in names diff --git a/frontend/src/integrations/registry.ts b/frontend/src/integrations/registry.ts index 442c697..9d44d64 100644 --- a/frontend/src/integrations/registry.ts +++ b/frontend/src/integrations/registry.ts @@ -210,7 +210,8 @@ export const SERVICE_REGISTRY: Record = { { kind: "active", name: "Active torrents", - description: "Torrents currently downloading or uploading.", + description: + "All active download/upload work, including queued and stalled transfers.", refreshIntervalMs: 15_000, defaultConfig: {}, configSchema: { type: "object", properties: {}, required: [] }, diff --git a/frontend/src/widgets/QbittorrentActiveTorrentsWidget.tsx b/frontend/src/widgets/QbittorrentActiveTorrentsWidget.tsx index 12c8a80..2736db5 100644 --- a/frontend/src/widgets/QbittorrentActiveTorrentsWidget.tsx +++ b/frontend/src/widgets/QbittorrentActiveTorrentsWidget.tsx @@ -40,19 +40,30 @@ function formatProgress(progress: number | null): string { return `${Math.round(progress * 100)}% complete`; } -function formatState(state: string | null, direction?: ActiveTorrent["direction"]): string { +function formatState( + state: string | null, + direction?: ActiveTorrent["direction"], +): string { const labels: Record = { downloading: "Downloading", forcedDL: "Downloading", stalledDL: "Download stalled", + queuedDL: "Queued download", metaDL: "Downloading metadata", + forcedMetaDL: "Downloading metadata", + checkingDL: "Checking download", allocating: "Allocating", uploading: "Uploading", forcedUP: "Uploading", stalledUP: "Upload stalled", + queuedUP: "Queued upload", + checkingUP: "Checking upload", + checkingResumeData: "Checking resume data", + moving: "Moving", }; if (state && labels[state]) return labels[state]; - if (direction) return direction === "downloading" ? "Downloading" : "Uploading"; + if (direction) + return direction === "downloading" ? "Downloading" : "Uploading"; return state || "Unknown state"; } @@ -91,7 +102,8 @@ export function QbittorrentActiveTorrentsWidget({ {torrent.name ?? "Unknown torrent"}

- {formatSize(torrent.size)} · {formatProgress(torrent.progress)} + {formatSize(torrent.size)} ·{" "} + {formatProgress(torrent.progress)}

- ↓ {formatSpeed(torrent.dl_speed)} · ↑ {formatSpeed(torrent.up_speed)} + ↓ {formatSpeed(torrent.dl_speed)} · ↑{" "} + {formatSpeed(torrent.up_speed)}
); diff --git a/frontend/src/widgets/QbittorrentTotalsWidget.tsx b/frontend/src/widgets/QbittorrentTotalsWidget.tsx index 206a5c3..386aca4 100644 --- a/frontend/src/widgets/QbittorrentTotalsWidget.tsx +++ b/frontend/src/widgets/QbittorrentTotalsWidget.tsx @@ -50,7 +50,9 @@ const UPLOAD_STATES = new Set(["uploading", "forcedUP", "stalledUP"]); function stateLabel(state: string): string { return ( STATE_LABELS[state] ?? - state.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, (char) => char.toUpperCase()) + state + .replace(/([a-z])([A-Z])/g, "$1 $2") + .replace(/^./, (char) => char.toUpperCase()) ); } @@ -76,7 +78,8 @@ export function QbittorrentTotalsWidget({ payload?.by_direction?.downloading ?? fallbackDirectionCount(byState, DOWNLOAD_STATES); const uploading = - payload?.by_direction?.uploading ?? fallbackDirectionCount(byState, UPLOAD_STATES); + payload?.by_direction?.uploading ?? + fallbackDirectionCount(byState, UPLOAD_STATES); const stateEntries = Object.entries(byState).sort( ([stateA, countA], [stateB, countB]) => countB - countA || stateLabel(stateA).localeCompare(stateLabel(stateB)), @@ -95,7 +98,9 @@ export function QbittorrentTotalsWidget({
{payload.total ?? 0}
-
Total torrents
+
+ Total torrents +
{downloading}
@@ -126,7 +131,9 @@ export function QbittorrentTotalsWidget({ )}
) : ( -
No torrent data available.
+
+ No torrent data available. +
)} );