130 lines
7.1 KiB
Python
130 lines
7.1 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.'}
|
|
return {'status': 'attention', 'message': 'Search finished, but no download appeared in Radarr. Use Search and choose a download to see matching releases and rejection reasons. For a foreign-language title, review the audio choice above.'}
|
|
if attempt + 1 < attempts:
|
|
await asyncio.sleep(delay)
|
|
return {'status': 'searching', 'message': 'Radarr is still searching. No download is confirmed yet; the pipeline will keep checking. You can close this window.'}
|
|
|
|
|
|
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.'}
|
|
if statuses == {'completed'}:
|
|
return {'status': 'attention', 'message': 'Sonarr finished searching, but no download is visible yet. Use Search and choose a download to review available releases and rejection reasons.'}
|
|
if attempt + 1 < attempts:
|
|
await asyncio.sleep(delay)
|
|
return {'status': 'searching', 'message': 'Sonarr is still searching. No download is confirmed yet; you can close this window and follow the pipeline.'}
|