Mark available downloads complete
Magent CI/CD / verify (push) Successful in 10m40s
Magent CI/CD / deploy-prod (push) Skipped
Magent CI/CD / deploy-beta (push) Successful in 14s

This commit is contained in:
2026-08-29 22:41:50 +12:00
parent 06a000bb06
commit c073581639
2 changed files with 130 additions and 11 deletions
+54 -11
View File
@@ -420,6 +420,10 @@ def _build_presentation(
missing = int(availability.get("missing") or 0)
total = int(availability.get("total") or 0)
partial = available > 0 and missing > 0
jellyfin_partial = bool(
jellyfin_found and snapshot.request_type == RequestType.tv and missing > 0
)
fully_available = bool(jellyfin_found and not jellyfin_partial)
download_visible = bool(download.get("visible"))
download_state = str(download.get("state") or "not_started")
@@ -432,7 +436,7 @@ def _build_presentation(
f"The request was approved, but it has not reached the {collector} collector yet. "
"Adding it to the library queue is the next step."
)
elif jellyfin_found and partial:
elif jellyfin_partial:
status_label = f"Partially available — {available} of {total} episodes collected"
meaning = (
f"Some of this request is ready to watch. {collector} is still looking for "
@@ -497,7 +501,11 @@ def _build_presentation(
meaning = snapshot.state_reason or "Magent is checking where this request is in the collection process."
action_ids = [action.id for action in snapshot.actions]
if "resume_torrent" in action_ids:
if fully_available:
next_title = "Ready to watch"
next_description = "Collection is complete. Open the title on the media server when you are ready."
recommended = []
elif "resume_torrent" in action_ids:
next_title = "Resume the interrupted download"
next_description = "The download exists but is not currently progressing. Resume it to continue collection."
recommended = ["resume_torrent"]
@@ -562,7 +570,9 @@ def _build_presentation(
else:
library_state, library_summary = "waiting", "Waiting for collector information"
if download_visible:
if fully_available:
search_state, search_summary = "complete", "No further search needed"
elif download_visible:
search_state = "complete"
search_summary = "A release was found"
elif arr_state in {"added", "searching"} and (missing or snapshot.request_type == RequestType.movie):
@@ -575,7 +585,15 @@ def _build_presentation(
else:
search_state, search_summary = "waiting", "Search has not started"
if download_visible:
completed_download_summary = (
"The requested content has been collected and is available to watch. "
"No further action is needed."
)
if fully_available:
download_stage_state, download_summary = "complete", completed_download_summary
pipeline_download_visible = False
pipeline_torrents: List[Dict[str, Any]] = []
elif download_visible:
download_stage_state = {
"downloading": "active",
"paused": "attention",
@@ -584,19 +602,34 @@ def _build_presentation(
"error": "attention",
}.get(download_state, "waiting")
download_summary = str(download.get("summary") or "A prior download attempt was found")
pipeline_download_visible = True
pipeline_torrents = download.get("torrents") or []
else:
download_stage_state, download_summary = "waiting", "No download attempt yet"
pipeline_download_visible = False
pipeline_torrents = []
if jellyfin_found and partial:
if jellyfin_partial:
available_state, available_summary = "partial", f"{available} of {total} episodes available"
elif jellyfin_found:
available_state, available_summary = "complete", "Available to watch"
else:
available_state, available_summary = "waiting", "Not available on the media server yet"
display_download = dict(download)
if fully_available:
display_download.update(
{
"visible": False,
"state": "completed",
"summary": completed_download_summary,
"torrents": [],
}
)
return {
"status": {"label": status_label, "meaning": meaning},
"download": download,
"download": display_download,
"nextStep": {
"title": next_title,
"description": next_description,
@@ -621,15 +654,19 @@ def _build_presentation(
"label": "Release search",
"state": search_state,
"summary": search_summary,
"actionIds": [action_id for action_id in ("search_auto", "search_releases") if action_id in action_ids],
"actionIds": [] if fully_available else [
action_id
for action_id in ("search_auto", "search_releases")
if action_id in action_ids
],
},
{
"id": "download",
"label": "Download",
"label": "Download complete" if fully_available else "Download",
"state": download_stage_state,
"summary": download_summary,
"visible": download_visible,
"torrents": download.get("torrents") or [],
"visible": pipeline_download_visible,
"torrents": pipeline_torrents,
},
{
"id": "available",
@@ -1158,7 +1195,13 @@ async def build_snapshot(request_id: str) -> Snapshot:
query = quote(snapshot.title or "")
jellyfin_link = f"{base_url}/web/index.html#!/search?query={query}"
availability = arr_details.get("availability") or {}
is_partial = bool(jellyfin_available and int(availability.get("missing") or 0) > 0)
is_partial = bool(
jellyfin_available
and snapshot.request_type == RequestType.tv
and int(availability.get("missing") or 0) > 0
)
if jellyfin_available and not is_partial:
snapshot.actions = []
snapshot.raw = {
"jellyseerr": jelly_request,
"arr": {
+76
View File
@@ -228,6 +228,82 @@ class RequestPresentationTests(unittest.TestCase):
download_stage = next(stage for stage in presentation["pipeline"] if stage["id"] == "download")
self.assertEqual(download_stage["summary"], "No download attempt yet")
def test_available_content_replaces_stale_download_warning_with_completion(self) -> None:
snapshot = Snapshot(
request_id="3909",
title="Example",
request_type=RequestType.tv,
state=NormalizedState.completed,
actions=[],
)
presentation = _build_presentation(
snapshot,
approved=True,
arr_state="available",
arr_details={
"availability": {"available": 6, "missing": 0, "total": 6, "seasons": []}
},
prowlarr_state="ok",
download={
"visible": True,
"state": "missing",
"summary": "A previous download was observed, but it is not currently visible in qBittorrent.",
"torrents": [],
},
jellyfin_found=True,
jellyfin_link="https://media.test/title/3909",
)
self.assertFalse(presentation["download"]["visible"])
self.assertEqual(presentation["download"]["state"], "completed")
self.assertEqual(presentation["nextStep"]["title"], "Ready to watch")
self.assertEqual(presentation["nextStep"]["actionIds"], [])
download_stage = next(stage for stage in presentation["pipeline"] if stage["id"] == "download")
self.assertEqual(download_stage["label"], "Download complete")
self.assertEqual(download_stage["state"], "complete")
self.assertFalse(download_stage["visible"])
self.assertEqual(
download_stage["summary"],
"The requested content has been collected and is available to watch. No further action is needed.",
)
search_stage = next(stage for stage in presentation["pipeline"] if stage["id"] == "search")
self.assertEqual(search_stage["state"], "complete")
self.assertEqual(search_stage["actionIds"], [])
def test_partially_available_content_keeps_missing_download_attention(self) -> None:
snapshot = Snapshot(
request_id="3909",
title="Example",
request_type=RequestType.tv,
state=NormalizedState.importing,
actions=[],
)
presentation = _build_presentation(
snapshot,
approved=True,
arr_state="added",
arr_details={
"availability": {"available": 3, "missing": 3, "total": 6, "seasons": []}
},
prowlarr_state="ok",
download={
"visible": True,
"state": "missing",
"summary": "A previous download is no longer visible.",
"torrents": [],
},
jellyfin_found=True,
jellyfin_link="https://media.test/title/3909",
)
self.assertTrue(presentation["download"]["visible"])
download_stage = next(stage for stage in presentation["pipeline"] if stage["id"] == "download")
self.assertEqual(download_stage["label"], "Download")
self.assertEqual(download_stage["state"], "attention")
self.assertTrue(download_stage["visible"])
class LiveDownloadProgressTests(unittest.IsolatedAsyncioTestCase):
async def test_live_download_progress_uses_saved_hash_and_current_qbittorrent_value(self) -> None: