Reconcile verified account IDs and make language repairs observable
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
from ..services.request_language import language_info, original_profile, is_original_profile
|
||||
from ..services.request_language import language_info, original_profile, is_original_profile, apply_original_to_movie, movie_search_outcome, series_search_outcome
|
||||
from ..feature_guards import require_request_access
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
import asyncio
|
||||
@@ -3180,6 +3180,13 @@ async def create_request(
|
||||
if not isinstance(details, dict):
|
||||
raise HTTPException(status_code=502, detail="Invalid response from Seerr media lookup")
|
||||
|
||||
language = language_info(details)
|
||||
accept_original = payload.get("acceptOriginalLanguage", False)
|
||||
if not isinstance(accept_original, bool):
|
||||
raise HTTPException(400, "The language choice must be true or false.")
|
||||
if accept_original and not language:
|
||||
raise HTTPException(409, "The original language could not be verified. Reload this title.")
|
||||
|
||||
media_info = details.get("mediaInfo") if isinstance(details.get("mediaInfo"), dict) else {}
|
||||
requests_list = media_info.get("requests")
|
||||
existing_request: Optional[Dict[str, Any]] = None
|
||||
@@ -3195,6 +3202,8 @@ async def create_request(
|
||||
year = int(date_value[:4])
|
||||
|
||||
if isinstance(existing_request, dict):
|
||||
if accept_original:
|
||||
raise HTTPException(409, 'This title is already requested. Open its request and choose Use original audio & search to update the existing movie.')
|
||||
existing_request_id = _quality_profile_id(existing_request.get("id"))
|
||||
existing_status = existing_request.get("status")
|
||||
if existing_request_id is not None:
|
||||
@@ -3229,16 +3238,12 @@ async def create_request(
|
||||
detail=f"Season selection is not available for this series: {invalid_seasons}",
|
||||
)
|
||||
|
||||
language = language_info(details)
|
||||
accept_original = payload.get("acceptOriginalLanguage", False)
|
||||
if not isinstance(accept_original, bool):
|
||||
raise HTTPException(400, "The language choice must be true or false.")
|
||||
if accept_original and not language:
|
||||
raise HTTPException(409, "The original language could not be verified. Reload this title.")
|
||||
destination = await _resolve_request_destination(runtime, client, media_type)
|
||||
if accept_original and media_type == "movie":
|
||||
destination["profile_id"] = await original_profile(
|
||||
RadarrClient(runtime.radarr_base_url, runtime.radarr_api_key), destination["profile_id"])
|
||||
# Seerr does not update an already-existing Radarr movie's profile on request creation.
|
||||
await apply_original_to_movie(RadarrClient(runtime.radarr_base_url, runtime.radarr_api_key), tmdb_id)
|
||||
|
||||
try:
|
||||
created = await client.create_request(
|
||||
@@ -3299,6 +3304,57 @@ async def ai_triage(request_id: str, user: Dict[str, str] = Depends(get_current_
|
||||
return triage_snapshot(snapshot)
|
||||
|
||||
|
||||
async def _request_language_context(request_id, user):
|
||||
runtime = get_runtime_settings()
|
||||
seerr = JellyseerrClient(runtime.jellyseerr_base_url, runtime.jellyseerr_api_key)
|
||||
await _ensure_request_access(seerr, int(request_id), user)
|
||||
request = await seerr.get_request(request_id)
|
||||
if not isinstance(request, dict) or request.get('type') != 'movie':
|
||||
return runtime, None, None
|
||||
tmdb_id = (request.get('media') or {}).get('tmdbId')
|
||||
if not isinstance(tmdb_id, int):
|
||||
raise HTTPException(502, 'Seerr did not return the movie identity.')
|
||||
details = await seerr.get_movie(tmdb_id)
|
||||
return runtime, tmdb_id, language_info(details or {})
|
||||
|
||||
|
||||
@router.get("/{request_id}/language")
|
||||
async def request_language(request_id: str, user: dict = Depends(get_current_user)):
|
||||
runtime, tmdb_id, language = await _request_language_context(request_id, user)
|
||||
if not language:
|
||||
return {'language': None}
|
||||
radarr = RadarrClient(runtime.radarr_base_url, runtime.radarr_api_key)
|
||||
movies = await radarr.get_movie_by_tmdb_id(tmdb_id)
|
||||
movie = next((m for m in (movies or []) if m.get('tmdbId') == tmdb_id), None)
|
||||
profiles = await radarr.get_quality_profiles() if movie else []
|
||||
profile = next((p for p in profiles if p['id'] == movie['qualityProfileId']), {}) if movie else {}
|
||||
return {'language': language, 'originalEnabled': is_original_profile(profile),
|
||||
'canChange': bool(movie) and _user_can_use_search_auto(user),
|
||||
'profileLanguage': (profile.get('language') or {}).get('name')}
|
||||
|
||||
|
||||
@router.post("/{request_id}/actions/language")
|
||||
async def accept_request_language(request_id: str, payload: dict, user: dict = Depends(get_current_user)):
|
||||
if not _user_can_use_search_auto(user):
|
||||
raise HTTPException(403, 'Search and download changes are disabled for this account.')
|
||||
if payload.get('acceptOriginalLanguage') is not True:
|
||||
raise HTTPException(400, 'Explicitly accept original-language audio before continuing.')
|
||||
runtime, tmdb_id, language = await _request_language_context(request_id, user)
|
||||
if not language:
|
||||
raise HTTPException(409, 'This request has no verified non-English original language.')
|
||||
if payload.get('languageCode') != language['code']:
|
||||
raise HTTPException(409, 'The language metadata changed. Reload the request and review it again.')
|
||||
radarr = RadarrClient(runtime.radarr_base_url, runtime.radarr_api_key)
|
||||
profile_id = await apply_original_to_movie(radarr, tmdb_id)
|
||||
if profile_id is None:
|
||||
raise HTTPException(409, 'The movie is not in Radarr yet. Recheck the pipeline first.')
|
||||
await asyncio.to_thread(save_action, request_id, 'original_language', 'Accept original-language audio',
|
||||
'ok', f"Original-language audio accepted ({language['code']}); Radarr profile {profile_id} verified.")
|
||||
result = await action_search_auto(request_id, user)
|
||||
result['message'] = 'Original-language audio enabled. ' + result['message']
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/{request_id}/actions/search")
|
||||
async def action_search(request_id: str, user: Dict[str, str] = Depends(get_current_user)) -> dict:
|
||||
runtime = get_runtime_settings()
|
||||
@@ -3377,6 +3433,9 @@ async def action_search(request_id: str, user: Dict[str, str] = Depends(get_curr
|
||||
raise HTTPException(status_code=502, detail=detail) from exc
|
||||
|
||||
releases = _filter_arr_release_results(results)
|
||||
rejection_reasons = sorted({str(reason) for result in results for reason in (result.get('rejections') or [])})[:8]
|
||||
result_message = (f"{collector} approved {len(releases)} releases against its assigned quality profile."
|
||||
if releases else f"No approved releases were found. " + (' '.join(rejection_reasons) if rejection_reasons else 'The indexers returned no suitable results. Try again later or review the audio language.'))
|
||||
|
||||
await asyncio.to_thread(
|
||||
save_action,
|
||||
@@ -3390,10 +3449,9 @@ async def action_search(request_id: str, user: Dict[str, str] = Depends(get_curr
|
||||
"status": "ok",
|
||||
"collector": collector,
|
||||
"qualityFiltered": True,
|
||||
"message": (
|
||||
f"{collector} approved {len(releases)} release{'s' if len(releases) != 1 else ''} "
|
||||
"against its assigned quality profile."
|
||||
),
|
||||
"message": result_message,
|
||||
"outcome": "matches" if releases else "attention",
|
||||
"rejectionReasons": rejection_reasons,
|
||||
"releases": releases,
|
||||
}
|
||||
|
||||
@@ -3444,13 +3502,14 @@ async def action_search_auto(request_id: str, user: Dict[str, str] = Depends(get
|
||||
responses.append(
|
||||
{"season": season_number, "episodeCount": len(episode_ids), "response": response}
|
||||
)
|
||||
message = "Search sent to Sonarr."
|
||||
outcome = await series_search_outcome(client, int(arr_item["id"]), [item['response'] for item in responses])
|
||||
message = outcome['message']
|
||||
if profile_message:
|
||||
message = f"{profile_message} {message}"
|
||||
await asyncio.to_thread(
|
||||
save_action, request_id, "search_auto", "Search and auto-download", "ok", message
|
||||
)
|
||||
return {"status": "ok", "message": message, "searched": responses}
|
||||
return {"status": outcome["status"], "message": message, "searched": responses}
|
||||
if snapshot.request_type.value == "movie":
|
||||
client = RadarrClient(runtime.radarr_base_url, runtime.radarr_api_key)
|
||||
if not client.configured():
|
||||
@@ -3472,13 +3531,14 @@ async def action_search_auto(request_id: str, user: Dict[str, str] = Depends(get
|
||||
await client.update_movie(movie)
|
||||
profile_message = f"Radarr quality profile updated to {target_profile_id} before search."
|
||||
response = await client.search(int(arr_item["id"]))
|
||||
message = "Search sent to Radarr."
|
||||
outcome = await movie_search_outcome(client, int(arr_item["id"]), response)
|
||||
message = outcome['message']
|
||||
if profile_message:
|
||||
message = f"{profile_message} {message}"
|
||||
await asyncio.to_thread(
|
||||
save_action, request_id, "search_auto", "Search and auto-download", "ok", message
|
||||
)
|
||||
return {"status": "ok", "message": message, "response": response}
|
||||
return {"status": outcome["status"], "message": message, "response": response}
|
||||
|
||||
raise HTTPException(status_code=400, detail="Unknown request type")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user