Files
Magent/backend/app/services/request_language.py
T

130 lines
6.9 KiB
Python

"""Explicit original-language requests without changing shared quality defaults."""
import asyncio
import copy
import hashlib
import json
import re
import httpx
from fastapi import HTTPException
_profile_lock = asyncio.Lock()
_prefix = "Magent Original "
def language_info(details):
code = str(details.get("originalLanguage") or details.get("original_language") or "").lower()
if not re.fullmatch(r"[a-z]{2}", code) or code in {"en", "xx", "zz"}:
return None
return {"code": code}
def profile_body(profile):
return {key: copy.deepcopy(value) for key, value in profile.items() if key not in {"id", "name"}}
def profile_name(body):
return _prefix + hashlib.sha256(json.dumps(body, sort_keys=True).encode()).hexdigest()[:16]
def is_original_profile(profile):
return ((profile.get("language") or {}).get("id") == -2
and profile.get("name") == profile_name(profile_body(profile)))
async def original_profile(client, default_id):
# Reuse immutable copies; never edit a profile already used by other titles.
async with _profile_lock:
try:
profiles = await client.get_quality_profiles()
except httpx.HTTPError as exc:
raise HTTPException(502, "Radarr could not load the language profile. Try again.") from exc
if not isinstance(profiles, list):
raise HTTPException(502, "Radarr returned invalid quality profiles.")
default = next((p for p in profiles if p.get("id") == default_id), None)
if not default:
raise HTTPException(409, "The default quality profile changed. Reload the request.")
body = profile_body(default)
body["language"] = {"id": -2, "name": "Original"}
name = profile_name(body)
match = next((p for p in profiles if p.get("name") == name and profile_body(p) == body), None)
if match:
return match["id"]
try:
result = await client.post("/api/v3/qualityprofile", payload={**body, "name": name})
except httpx.HTTPError as exc:
raise HTTPException(502, "Radarr could not prepare the original-language profile. Try again.") from exc
if not isinstance(result, dict) or not isinstance(result.get("id"), int):
raise HTTPException(502, "Radarr could not prepare the original-language profile. Try again.")
return result["id"]
async def apply_original_to_movie(client, tmdb_id):
movies = await client.get_movie_by_tmdb_id(tmdb_id)
if not isinstance(movies, list):
raise HTTPException(502, "Radarr did not return the movie list.")
matches = [movie for movie in movies if movie.get('tmdbId') == tmdb_id]
if not matches:
return None
if len(matches) != 1:
raise HTTPException(409, "Radarr returned multiple movies for this identity.")
movie = matches[0]
profile_id = await original_profile(client, movie['qualityProfileId'])
if movie['qualityProfileId'] != profile_id:
movie['qualityProfileId'] = profile_id
await client.update_movie(movie)
verified = await client.get_movie(movie['id'])
if not verified or verified.get('qualityProfileId') != profile_id:
raise HTTPException(502, "Radarr did not save the original-language choice. Try again before searching.")
return profile_id
async def movie_search_outcome(client, movie_id, command, attempts=12, delay=2):
command_id = command.get('id') if isinstance(command, dict) else None
if not isinstance(command_id, int):
return {'status': 'searching', 'message': 'Search submitted; download confirmation is not available yet. Recheck the request shortly.'}
for attempt in range(attempts):
state = await client.get(f'/api/v3/command/{command_id}')
status = str((state or {}).get('status', '')).lower()
queue = await client.get_queue(movie_id)
records = queue.get('records', []) if isinstance(queue, dict) else queue or []
matching = [item for item in records if item.get('movieId') == movie_id]
if any(item.get('trackedDownloadStatus') in {'warning', 'error'} for item in matching):
return {'status': 'attention', 'message': 'Radarr found a download, but it reports a download or import problem. Open the pipeline details to review it.'}
if matching:
return {'status': 'downloading', 'message': 'Radarr has a download queued for this movie. The pipeline will track its progress.'}
if status in {'failed', 'aborted', 'cancelled'}:
return {'status': 'attention', 'message': 'Radarr could not complete the search. Check service health or try Search and choose a download.'}
if status == 'completed':
movie = await client.get_movie(movie_id)
if (movie or {}).get('hasFile'):
return {'status': 'complete', 'message': 'Radarr already has the movie file. Recheck the request for Jellyfin availability.'}
# Command completion precedes download-client queue refresh. Keep polling.
pass
if attempt + 1 < attempts:
await asyncio.sleep(delay)
return {'status': 'pending', 'message': 'The search was submitted, but a download is not confirmed yet. The download queue may still be updating. Close this window and recheck the request shortly.'}
async def series_search_outcome(client, series_id, commands, attempts=12, delay=2):
ids = [item.get('id') for item in commands if isinstance(item, dict) and isinstance(item.get('id'), int)]
if not ids:
return {'status': 'searching', 'message': 'Search submitted to Sonarr; no download is confirmed yet. Recheck the pipeline shortly.'}
for attempt in range(attempts):
states = await asyncio.gather(*(client.get(f'/api/v3/command/{identity}') for identity in ids))
queue = await client.get_queue(series_id)
records = queue.get('records', []) if isinstance(queue, dict) else queue or []
matching = [item for item in records if item.get('seriesId') == series_id]
if any(item.get('trackedDownloadStatus') in {'warning', 'error'} for item in matching):
return {'status': 'attention', 'message': 'Sonarr has a download with a reported problem. Review the pipeline details.'}
if matching:
return {'status': 'downloading', 'message': 'Sonarr has downloads queued for this show. The pipeline will track their progress.'}
statuses = {str((state or {}).get('status', '')).lower() for state in states}
if statuses & {'failed', 'aborted', 'cancelled'}:
return {'status': 'attention', 'message': 'A Sonarr search failed. Check the service or try Search and choose a download.'}
# Even completed commands can precede Sonarr's download queue refresh.
if attempt + 1 < attempts:
await asyncio.sleep(delay)
return {'status': 'pending', 'message': 'The search was submitted, but a download is not confirmed yet. The download queue may still be updating. Close this window and recheck the request shortly.'}