Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/stowarr/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,8 @@ def do_GET(self):
self.send_json(200, manager.store.move_queue())
elif path == "/api/reconcile-queue":
self.send_json(200, manager.store.reconcile_queue())
elif path == "/api/queue-summary":
self.send_json(200, manager.store.queue_summary())
elif path == "/api/recovery":
self.send_json(200, manager.recovery_status())
elif path.startswith("/api/operations/") and path.endswith("/events"):
Expand Down
20 changes: 20 additions & 0 deletions src/stowarr/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,26 @@ def move_queue(self, limit: int = 200) -> list[dict]:
for row in rows
]

def queue_summary(self) -> dict[str, dict[str, int]]:
terminal = ("COMPLETE", "FAILED", "CANCELLED", "INTERRUPTED")
result: dict[str, dict[str, int]] = {}
with self.lock:
for kind, table in (
("move", "move_queue"),
("reconcile", "reconcile_queue"),
):
row = self.db.execute(
f"""SELECT COUNT(*) AS total,
SUM(CASE WHEN state IN (?,?,?,?) THEN 1 ELSE 0 END) AS terminal
FROM {table}""", # nosec
terminal,
).fetchone()
result[kind] = {
"total": int(row["total"]),
"terminal": int(row["terminal"] or 0),
}
return result

def _clear_queue(self, table: str) -> int:
if table not in {"move_queue", "reconcile_queue"}:
raise ValueError("Unknown queue")
Expand Down
16 changes: 10 additions & 6 deletions src/stowarr/web/app.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 30 additions & 0 deletions tests/test_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,11 +310,41 @@ def test_reconcile_queue_cleanup_keeps_waiting_work(self):
"waiting", {"auxiliaryFiles": []}, "waiting", {}
)

self.assertEqual(
store.queue_summary()["reconcile"],
{"total": 2, "terminal": 1},
)
self.assertEqual(store.clear_reconcile_queue(), 1)
self.assertEqual(
[item["public_id"] for item in store.reconcile_queue()],
[waiting["public_id"]],
)
self.assertEqual(
store.queue_summary()["reconcile"],
{"total": 1, "terminal": 0},
)

def test_queue_summary_counts_finished_work_beyond_list_limit(self):
with tempfile.TemporaryDirectory() as directory:
store = Store(Path(directory) / "state.db")
finished = store.enqueue_move("finished", "p1", {}, "finished", {})
store.claim_next_move()
store.finish_move(finished["id"], "COMPLETE")
for index in range(200):
store.enqueue_move(
f"waiting-{index}", "p1", {}, f"waiting-{index}", {}
)

visible = store.move_queue()
self.assertEqual(len(visible), 200)
self.assertFalse(
any(item["state"] in {"COMPLETE", "FAILED", "CANCELLED", "INTERRUPTED"}
for item in visible)
)
self.assertEqual(
store.queue_summary()["move"],
{"total": 201, "terminal": 1},
)

def test_restart_marks_queue_and_history_and_pauses_later_work(self):
with tempfile.TemporaryDirectory() as directory:
Expand Down
Loading