feat: add seasons from request details
Magent CI/CD / verify (push) Successful in 1m51s
Magent CI/CD / deploy-prod (push) Skipped
Magent CI/CD / deploy-beta (push) Skipped

This commit is contained in:
2026-09-16 17:15:40 +12:00
parent 05a540ecbb
commit aed1bf9256
6 changed files with 532 additions and 5 deletions
+138 -1
View File
@@ -273,8 +273,14 @@ def _user_can_use_search_auto(user: Dict[str, Any]) -> bool:
def _filter_snapshot_for_user(snapshot: Snapshot, user: Dict[str, Any]) -> Snapshot:
if not _user_can_use_search_auto(user):
can_add_seasons = _user_can_use_search_auto(user)
if not can_add_seasons:
snapshot.actions = [action for action in snapshot.actions if action.id != "search_auto"]
pipeline = snapshot.presentation.get("pipeline")
if isinstance(pipeline, list):
for stage in pipeline:
if isinstance(stage, dict) and stage.get("id") == "library":
stage["canAddSeasons"] = can_add_seasons
if user.get("role") != "admin":
# The standard request view is intentionally collaborative, but service payloads can
# contain requester identities, internal URLs, download hashes and diagnostic errors.
@@ -2474,6 +2480,137 @@ async def action_search_missing_media(
return {"status": "ok", "message": message, "episode_ids": searched_ids}
@router.post("/{request_id}/actions/add-seasons")
async def action_add_seasons(
request_id: str,
payload: Dict[str, Any],
user: Dict[str, str] = Depends(get_current_user),
) -> Dict[str, Any]:
if not request_id.isdigit():
raise HTTPException(status_code=400, detail="Invalid request id")
if not _user_can_use_search_auto(user):
raise HTTPException(status_code=403, detail="Adding seasons is disabled for this user")
season_numbers = _positive_id_list(
payload.get("season_numbers"), field="season_numbers", maximum=100
)
if not season_numbers:
raise HTTPException(status_code=400, detail="Choose at least one season")
runtime = get_runtime_settings()
seerr = JellyseerrClient(runtime.jellyseerr_base_url, runtime.jellyseerr_api_key)
if seerr.configured():
await _ensure_request_access(seerr, int(request_id), user)
snapshot = await build_snapshot(request_id)
if snapshot.request_type != RequestType.tv:
raise HTTPException(status_code=400, detail="Additional seasons are only available for TV requests")
arr_item = snapshot.raw.get("arr", {}).get("item")
if not isinstance(arr_item, dict) or not isinstance(arr_item.get("id"), int):
raise HTTPException(status_code=404, detail="Series not found in Sonarr")
sonarr = SonarrClient(runtime.sonarr_base_url, runtime.sonarr_api_key)
if not sonarr.configured():
raise HTTPException(status_code=400, detail="Sonarr is not configured")
series_id = int(arr_item["id"])
label = "Add seasons"
try:
series = await sonarr.get_series(series_id)
if not isinstance(series, dict) or not isinstance(series.get("seasons"), list):
raise HTTPException(status_code=502, detail="Sonarr did not return the series seasons")
known_seasons = {
season.get("seasonNumber")
for season in series["seasons"]
if isinstance(season, dict)
and isinstance(season.get("seasonNumber"), int)
and season.get("seasonNumber") > 0
}
if any(season_number not in known_seasons for season_number in season_numbers):
raise HTTPException(status_code=409, detail="One or more selected seasons are no longer available in Sonarr")
updated_seasons = [
{**season, "monitored": True}
if isinstance(season, dict) and season.get("seasonNumber") in season_numbers
else season
for season in series["seasons"]
]
if series.get("monitored") is not True or updated_seasons != series["seasons"]:
await sonarr.update_series({**series, "monitored": True, "seasons": updated_seasons})
episodes = await sonarr.get_episodes(series_id)
if not isinstance(episodes, list):
raise HTTPException(status_code=502, detail="Sonarr did not return an episode list")
selected_episodes = [
episode for episode in episodes
if isinstance(episode, dict)
and episode.get("seasonNumber") in season_numbers
and isinstance(episode.get("id"), int)
]
episode_ids = [int(episode["id"]) for episode in selected_episodes]
if episode_ids:
await sonarr.monitor_episodes(episode_ids, True)
search_ids = [
int(episode["id"])
for episode in selected_episodes
if _released_episode(episode)
and episode.get("hasFile") is not True
and not (
isinstance(episode.get("episodeFileId"), int)
and episode.get("episodeFileId") > 0
)
]
if search_ids:
await sonarr.search_episodes(search_ids)
verified_series = await sonarr.get_series(series_id)
verified_seasons = verified_series.get("seasons") if isinstance(verified_series, dict) else []
verified_season_map = {
season.get("seasonNumber"): season.get("monitored")
for season in verified_seasons
if isinstance(season, dict) and isinstance(season.get("seasonNumber"), int)
}
if (
not isinstance(verified_series, dict)
or verified_series.get("monitored") is not True
or any(verified_season_map.get(number) is not True for number in season_numbers)
):
raise HTTPException(status_code=502, detail="Sonarr did not enable monitoring for every selected season")
if episode_ids:
verified_episodes = await sonarr.get_episodes(series_id)
if not isinstance(verified_episodes, list) or any(
isinstance(episode, dict)
and episode.get("id") in episode_ids
and episode.get("monitored") is not True
for episode in verified_episodes
):
raise HTTPException(status_code=502, detail="Sonarr did not enable monitoring for every selected episode")
except HTTPException as exc:
detail = f"The seasons could not be added: {exc.detail}"
await asyncio.to_thread(save_action, request_id, "add_seasons", label, "failed", detail)
raise
except Exception as exc:
logger.exception("add seasons failed request_id=%s", request_id)
detail = "Sonarr could not add the selected seasons."
await asyncio.to_thread(save_action, request_id, "add_seasons", label, "failed", detail)
raise HTTPException(status_code=502, detail=detail) from exc
season_label = ", ".join(str(number) for number in season_numbers)
message = f"Season{'s' if len(season_numbers) != 1 else ''} {season_label} added to Sonarr."
if search_ids:
message += f" Searching for {len(search_ids)} released missing episode{'s' if len(search_ids) != 1 else ''}."
elif episode_ids:
message += " All known episodes are already collected or have not aired yet."
else:
message += " New episodes will be monitored when Sonarr discovers them."
await asyncio.to_thread(save_action, request_id, "add_seasons", label, "ok", message)
fresh_snapshot = _filter_snapshot_for_user(await build_snapshot(request_id), user)
return {
"status": "ok",
"message": message,
"season_numbers": season_numbers,
"searched_episode_count": len(search_ids),
"snapshot": fresh_snapshot,
}
@router.post("/{request_id}/actions/repair-subtitles")
async def action_repair_subtitles(
request_id: str,