Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 940b966192 |
@@ -220,12 +220,7 @@ class QbittorrentClient:
|
|||||||
if fields is None:
|
if fields is None:
|
||||||
snap["torrents"].pop(hash_, None)
|
snap["torrents"].pop(hash_, None)
|
||||||
else:
|
else:
|
||||||
previous = snap["torrents"].get(hash_)
|
snap["torrents"][hash_] = fields
|
||||||
snap["torrents"][hash_] = (
|
|
||||||
{**previous, **fields}
|
|
||||||
if isinstance(previous, dict) and isinstance(fields, dict)
|
|
||||||
else fields
|
|
||||||
)
|
|
||||||
for hash_ in update.get("torrents_removed") or []:
|
for hash_ in update.get("torrents_removed") or []:
|
||||||
snap["torrents"].pop(hash_, None)
|
snap["torrents"].pop(hash_, None)
|
||||||
categories = update.get("categories")
|
categories = update.get("categories")
|
||||||
|
|||||||
@@ -547,7 +547,6 @@ class QbittorrentWidgetSource:
|
|||||||
"direction": direction,
|
"direction": direction,
|
||||||
"size": torrent.get("size"),
|
"size": torrent.get("size"),
|
||||||
"progress": torrent.get("progress"),
|
"progress": torrent.get("progress"),
|
||||||
"ratio": torrent.get("ratio"),
|
|
||||||
"dl_speed": torrent.get("dlspeed"),
|
"dl_speed": torrent.get("dlspeed"),
|
||||||
"up_speed": torrent.get("upspeed"),
|
"up_speed": torrent.get("upspeed"),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,21 +125,13 @@ class QbittorrentClientTests(unittest.TestCase):
|
|||||||
"rid": 10,
|
"rid": 10,
|
||||||
"full_update": True,
|
"full_update": True,
|
||||||
"server_state": {"dl_info_speed": 100},
|
"server_state": {"dl_info_speed": 100},
|
||||||
"torrents": {
|
"torrents": {"a": {"name": "A", "state": "downloading"}},
|
||||||
"a": {
|
|
||||||
"name": "A",
|
|
||||||
"state": "downloading",
|
|
||||||
"size": 1_024,
|
|
||||||
"progress": 0.5,
|
|
||||||
"dlspeed": 100,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
partial = {
|
partial = {
|
||||||
"rid": 11,
|
"rid": 11,
|
||||||
"full_update": False,
|
"full_update": False,
|
||||||
"server_state": {"dl_info_speed": 200},
|
"server_state": {"dl_info_speed": 200},
|
||||||
"torrents": {"a": {"dlspeed": 200}},
|
"torrents": {"a": {"name": "A", "state": "pausedDL"}},
|
||||||
}
|
}
|
||||||
self.session.get.side_effect = [self._get_response(full), self._get_response(partial)]
|
self.session.get.side_effect = [self._get_response(full), self._get_response(partial)]
|
||||||
|
|
||||||
@@ -151,11 +143,7 @@ class QbittorrentClientTests(unittest.TestCase):
|
|||||||
r2 = self.client.maindata()
|
r2 = self.client.maindata()
|
||||||
self.assertEqual(self.session.get.call_args_list[1].kwargs["params"].get("rid"), 10)
|
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["server_state"]["dl_info_speed"], 200) # merged
|
||||||
self.assertEqual(r2["torrents"]["a"]["dlspeed"], 200)
|
self.assertEqual(r2["torrents"]["a"]["state"], "pausedDL") # merged
|
||||||
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:
|
def test_maindata_caches_concurrent_calls_within_ttl(self) -> None:
|
||||||
"""Two calls within the TTL collapse to a single HTTP fetch."""
|
"""Two calls within the TTL collapse to a single HTTP fetch."""
|
||||||
@@ -239,8 +227,8 @@ class QbittorrentClientTests(unittest.TestCase):
|
|||||||
self.session.post.return_value = self._login_response()
|
self.session.post.return_value = self._login_response()
|
||||||
self.client._login()
|
self.client._login()
|
||||||
call_kwargs = self.session.post.call_args.kwargs
|
call_kwargs = self.session.post.call_args.kwargs
|
||||||
self.assertEqual(call_kwargs["timeout"], (5.0, 5.0))
|
assert call_kwargs["timeout"] == (5.0, 5.0)
|
||||||
self.assertNotIsInstance(call_kwargs["timeout"], int)
|
assert not isinstance(call_kwargs["timeout"], int)
|
||||||
|
|
||||||
def test_login_fails_message_names_bad_credentials(self) -> None:
|
def test_login_fails_message_names_bad_credentials(self) -> None:
|
||||||
"""'Fails.' body yields a clear 'invalid username or password' error."""
|
"""'Fails.' body yields a clear 'invalid username or password' error."""
|
||||||
|
|||||||
@@ -1085,7 +1085,6 @@ def _fake_qbit_maindata():
|
|||||||
"state": "downloading",
|
"state": "downloading",
|
||||||
"size": 1000,
|
"size": 1000,
|
||||||
"progress": 0.5,
|
"progress": 0.5,
|
||||||
"ratio": 1.25,
|
|
||||||
"dlspeed": 500,
|
"dlspeed": 500,
|
||||||
"upspeed": 10,
|
"upspeed": 10,
|
||||||
},
|
},
|
||||||
@@ -1094,7 +1093,6 @@ def _fake_qbit_maindata():
|
|||||||
"state": "uploading",
|
"state": "uploading",
|
||||||
"size": 2000,
|
"size": 2000,
|
||||||
"progress": 1.0,
|
"progress": 1.0,
|
||||||
"ratio": 0.5,
|
|
||||||
"dlspeed": 0,
|
"dlspeed": 0,
|
||||||
"upspeed": 100,
|
"upspeed": 100,
|
||||||
},
|
},
|
||||||
@@ -1180,7 +1178,6 @@ async def test_qbittorrent_active_filters_current_transfers_only():
|
|||||||
assert len(active) == 2
|
assert len(active) == 2
|
||||||
names = [torrent["name"] for torrent in active]
|
names = [torrent["name"] for torrent in active]
|
||||||
assert names == ["Movie.mkv", "Show.mkv"]
|
assert names == ["Movie.mkv", "Show.mkv"]
|
||||||
assert [torrent["ratio"] for torrent in active] == [1.25, 0.5]
|
|
||||||
assert all((torrent["dl_speed"] or 0) > 0 or (torrent["up_speed"] or 0) > 0 for torrent in active)
|
assert all((torrent["dl_speed"] or 0) > 0 or (torrent["up_speed"] or 0) > 0 for torrent in active)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -321,11 +321,6 @@ 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.
|
- 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.
|
- 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 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.
|
|
||||||
- Active-torrent entries must show each torrent's qBittorrent share ratio
|
|
||||||
(uploaded ÷ downloaded) alongside its size and completion progress.
|
|
||||||
- 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.
|
- 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.
|
- 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.
|
- Disabling a qBittorrent service pauses polling while retaining history; deleting the service purges its samples and scheduler history through the existing cascade-delete behavior.
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ interface ActiveTorrent {
|
|||||||
direction?: "downloading" | "uploading";
|
direction?: "downloading" | "uploading";
|
||||||
size: number | null;
|
size: number | null;
|
||||||
progress: number | null;
|
progress: number | null;
|
||||||
ratio: number | null;
|
|
||||||
dl_speed: number | null;
|
dl_speed: number | null;
|
||||||
up_speed: number | null;
|
up_speed: number | null;
|
||||||
}
|
}
|
||||||
@@ -41,12 +40,6 @@ function formatProgress(progress: number | null): string {
|
|||||||
return `${Math.round(progress * 100)}% complete`;
|
return `${Math.round(progress * 100)}% complete`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatRatio(ratio: number | null): string {
|
|
||||||
if (ratio === null || !Number.isFinite(ratio) || ratio < 0)
|
|
||||||
return "Ratio unknown";
|
|
||||||
return `Ratio ${ratio.toFixed(2)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatState(
|
function formatState(
|
||||||
state: string | null,
|
state: string | null,
|
||||||
direction?: ActiveTorrent["direction"],
|
direction?: ActiveTorrent["direction"],
|
||||||
@@ -110,8 +103,7 @@ export function QbittorrentActiveTorrentsWidget({
|
|||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{formatSize(torrent.size)} ·{" "}
|
{formatSize(torrent.size)} ·{" "}
|
||||||
{formatProgress(torrent.progress)} ·{" "}
|
{formatProgress(torrent.progress)}
|
||||||
{formatRatio(torrent.ratio)}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Badge
|
<Badge
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ describe("QbittorrentActiveTorrentsWidget", () => {
|
|||||||
state: "downloading",
|
state: "downloading",
|
||||||
size: 1000,
|
size: 1000,
|
||||||
progress: 0.5,
|
progress: 0.5,
|
||||||
ratio: 1.25,
|
|
||||||
dl_speed: 500000,
|
dl_speed: 500000,
|
||||||
up_speed: 1000,
|
up_speed: 1000,
|
||||||
},
|
},
|
||||||
@@ -78,7 +77,6 @@ describe("QbittorrentActiveTorrentsWidget", () => {
|
|||||||
expect(screen.getByText("Show.mkv")).toBeInTheDocument();
|
expect(screen.getByText("Show.mkv")).toBeInTheDocument();
|
||||||
expect(screen.getByText("Downloading")).toBeInTheDocument();
|
expect(screen.getByText("Downloading")).toBeInTheDocument();
|
||||||
expect(screen.getByText("Uploading")).toBeInTheDocument();
|
expect(screen.getByText("Uploading")).toBeInTheDocument();
|
||||||
expect(screen.getByText(/Ratio 1\.25/)).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows empty state when no active torrents", () => {
|
it("shows empty state when no active torrents", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user