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.
+
)}
);