Process 1 build 0803262216
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -34,6 +34,24 @@ class JellyseerrClient(ApiClient):
|
||||
},
|
||||
)
|
||||
|
||||
async def create_request(
|
||||
self,
|
||||
*,
|
||||
media_type: str,
|
||||
media_id: int,
|
||||
seasons: Optional[list[int]] = None,
|
||||
is_4k: Optional[bool] = None,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
payload: Dict[str, Any] = {
|
||||
"mediaType": media_type,
|
||||
"mediaId": media_id,
|
||||
}
|
||||
if isinstance(seasons, list) and seasons:
|
||||
payload["seasons"] = seasons
|
||||
if isinstance(is_4k, bool):
|
||||
payload["is4k"] = is_4k
|
||||
return await self.post("/api/v1/request", payload=payload)
|
||||
|
||||
async def get_users(self, take: int = 50, skip: int = 0) -> Optional[Dict[str, Any]]:
|
||||
return await self.get(
|
||||
"/api/v1/user",
|
||||
|
||||
@@ -421,6 +421,34 @@ def _extract_tmdb_lookup(payload: Dict[str, Any]) -> tuple[Optional[int], Option
|
||||
return tmdb_id, media_type
|
||||
|
||||
|
||||
def _normalize_media_type(value: Any) -> Optional[str]:
|
||||
if not isinstance(value, str):
|
||||
return None
|
||||
normalized = value.strip().lower()
|
||||
if normalized in {"movie", "tv"}:
|
||||
return normalized
|
||||
return None
|
||||
|
||||
|
||||
def _normalize_seasons(value: Any) -> list[int]:
|
||||
if value is None:
|
||||
return []
|
||||
if not isinstance(value, list):
|
||||
raise HTTPException(status_code=400, detail="seasons must be an array of positive integers")
|
||||
normalized: list[int] = []
|
||||
for raw in value:
|
||||
try:
|
||||
season = int(raw)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise HTTPException(
|
||||
status_code=400, detail="seasons must contain only positive integers"
|
||||
) from exc
|
||||
if season <= 0:
|
||||
raise HTTPException(status_code=400, detail="seasons must contain only positive integers")
|
||||
normalized.append(season)
|
||||
return sorted(set(normalized))
|
||||
|
||||
|
||||
def _artwork_missing_for_payload(payload: Dict[str, Any]) -> bool:
|
||||
poster_path, backdrop_path = _extract_artwork_paths(payload)
|
||||
tmdb_id, media_type = _extract_tmdb_lookup(payload)
|
||||
@@ -1864,12 +1892,135 @@ async def search_requests(
|
||||
"statusLabel": status_label,
|
||||
"requestedBy": requested_by,
|
||||
"accessible": accessible,
|
||||
"posterPath": item.get("posterPath") or item.get("poster_path"),
|
||||
"backdropPath": item.get("backdropPath") or item.get("backdrop_path"),
|
||||
}
|
||||
)
|
||||
|
||||
return {"results": results}
|
||||
|
||||
|
||||
@router.post("/create")
|
||||
async def create_request(
|
||||
payload: Dict[str, Any], user: Dict[str, Any] = Depends(get_current_user)
|
||||
) -> Dict[str, Any]:
|
||||
runtime = get_runtime_settings()
|
||||
client = JellyseerrClient(runtime.jellyseerr_base_url, runtime.jellyseerr_api_key)
|
||||
if not client.configured():
|
||||
raise HTTPException(status_code=400, detail="Seerr not configured")
|
||||
|
||||
media_type = _normalize_media_type(
|
||||
payload.get("mediaType") or payload.get("type") or payload.get("media_type")
|
||||
)
|
||||
if media_type is None:
|
||||
raise HTTPException(status_code=400, detail="mediaType must be 'movie' or 'tv'")
|
||||
|
||||
raw_tmdb_id = payload.get("tmdbId")
|
||||
if raw_tmdb_id is None:
|
||||
raw_tmdb_id = payload.get("mediaId")
|
||||
if raw_tmdb_id is None:
|
||||
raw_tmdb_id = payload.get("id")
|
||||
try:
|
||||
tmdb_id = int(raw_tmdb_id)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise HTTPException(status_code=400, detail="tmdbId must be a valid integer") from exc
|
||||
if tmdb_id <= 0:
|
||||
raise HTTPException(status_code=400, detail="tmdbId must be a positive integer")
|
||||
|
||||
seasons = _normalize_seasons(payload.get("seasons")) if media_type == "tv" else []
|
||||
raw_is_4k = payload.get("is4k")
|
||||
if raw_is_4k is not None and not isinstance(raw_is_4k, bool):
|
||||
raise HTTPException(status_code=400, detail="is4k must be true or false")
|
||||
is_4k = raw_is_4k if isinstance(raw_is_4k, bool) else None
|
||||
|
||||
try:
|
||||
details = await (client.get_movie(tmdb_id) if media_type == "movie" else client.get_tv(tmdb_id))
|
||||
except httpx.HTTPStatusError as exc:
|
||||
raise HTTPException(status_code=502, detail=_format_upstream_error("Seerr", exc)) from exc
|
||||
|
||||
if not isinstance(details, dict):
|
||||
raise HTTPException(status_code=502, detail="Invalid response from Seerr media lookup")
|
||||
|
||||
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
|
||||
if isinstance(requests_list, list) and requests_list:
|
||||
first_request = requests_list[0]
|
||||
if isinstance(first_request, dict):
|
||||
existing_request = first_request
|
||||
|
||||
title = details.get("title") or details.get("name")
|
||||
year: Optional[int] = None
|
||||
date_value = details.get("releaseDate") or details.get("firstAirDate")
|
||||
if isinstance(date_value, str) and len(date_value) >= 4 and date_value[:4].isdigit():
|
||||
year = int(date_value[:4])
|
||||
|
||||
if isinstance(existing_request, dict):
|
||||
existing_request_id = _quality_profile_id(existing_request.get("id"))
|
||||
existing_status = existing_request.get("status")
|
||||
if existing_request_id is not None:
|
||||
request_payload = await _get_request_details(client, existing_request_id)
|
||||
if isinstance(request_payload, dict):
|
||||
parsed_payload = _parse_request_payload(request_payload)
|
||||
upsert_request_cache(**_build_request_cache_record(parsed_payload, request_payload))
|
||||
_cache_set(f"request:{existing_request_id}", request_payload)
|
||||
title = parsed_payload.get("title") or title
|
||||
year = parsed_payload.get("year") or year
|
||||
return {
|
||||
"status": "exists",
|
||||
"requestId": existing_request_id,
|
||||
"type": media_type,
|
||||
"tmdbId": tmdb_id,
|
||||
"title": title,
|
||||
"year": year,
|
||||
"statusCode": existing_status,
|
||||
"statusLabel": _status_label(existing_status),
|
||||
}
|
||||
|
||||
try:
|
||||
created = await client.create_request(
|
||||
media_type=media_type,
|
||||
media_id=tmdb_id,
|
||||
seasons=seasons if media_type == "tv" else None,
|
||||
is_4k=is_4k,
|
||||
)
|
||||
except httpx.HTTPStatusError as exc:
|
||||
raise HTTPException(status_code=502, detail=_format_upstream_error("Seerr", exc)) from exc
|
||||
|
||||
if not isinstance(created, dict):
|
||||
raise HTTPException(status_code=502, detail="Invalid response from Seerr request create")
|
||||
|
||||
parsed = _parse_request_payload(created)
|
||||
request_id = _quality_profile_id(parsed.get("request_id"))
|
||||
status_code = parsed.get("status")
|
||||
title = parsed.get("title") or title
|
||||
year = parsed.get("year") or year
|
||||
|
||||
if request_id is not None:
|
||||
upsert_request_cache(**_build_request_cache_record(parsed, created))
|
||||
_cache_set(f"request:{request_id}", created)
|
||||
_recent_cache["updated_at"] = None
|
||||
await asyncio.to_thread(
|
||||
save_action,
|
||||
str(request_id),
|
||||
"request_created",
|
||||
"Create request",
|
||||
"ok",
|
||||
f"{media_type} request created from discovery by {user.get('username')}.",
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "created",
|
||||
"requestId": request_id,
|
||||
"type": media_type,
|
||||
"tmdbId": tmdb_id,
|
||||
"title": title,
|
||||
"year": year,
|
||||
"statusCode": status_code,
|
||||
"statusLabel": _status_label(status_code),
|
||||
}
|
||||
|
||||
|
||||
@router.post("/{request_id}/ai/triage", response_model=TriageResult)
|
||||
async def ai_triage(request_id: str, user: Dict[str, str] = Depends(get_current_user)) -> TriageResult:
|
||||
runtime = get_runtime_settings()
|
||||
|
||||
Reference in New Issue
Block a user