Finalize dev-1.3 upgrades and Seerr updates
This commit is contained in:
@@ -696,12 +696,13 @@ async def _fetch_all_jellyseerr_users(
|
||||
return save_jellyseerr_users_cache(users)
|
||||
return users
|
||||
|
||||
@router.post("/seerr/users/sync")
|
||||
@router.post("/jellyseerr/users/sync")
|
||||
async def jellyseerr_users_sync() -> 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="Jellyseerr not configured")
|
||||
raise HTTPException(status_code=400, detail="Seerr not configured")
|
||||
jellyseerr_users = await _fetch_all_jellyseerr_users(client, use_cache=False)
|
||||
if not jellyseerr_users:
|
||||
return {"status": "ok", "matched": 0, "skipped": 0, "total": 0}
|
||||
@@ -733,12 +734,13 @@ def _pick_jellyseerr_username(user: Dict[str, Any]) -> Optional[str]:
|
||||
return None
|
||||
|
||||
|
||||
@router.post("/seerr/users/resync")
|
||||
@router.post("/jellyseerr/users/resync")
|
||||
async def jellyseerr_users_resync() -> 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="Jellyseerr not configured")
|
||||
raise HTTPException(status_code=400, detail="Seerr not configured")
|
||||
jellyseerr_users = await _fetch_all_jellyseerr_users(client, use_cache=False)
|
||||
if not jellyseerr_users:
|
||||
return {"status": "ok", "imported": 0, "cleared": 0}
|
||||
@@ -772,7 +774,7 @@ async def requests_sync() -> 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="Jellyseerr not configured")
|
||||
raise HTTPException(status_code=400, detail="Seerr not configured")
|
||||
state = await requests_router.start_requests_sync(
|
||||
runtime.jellyseerr_base_url, runtime.jellyseerr_api_key
|
||||
)
|
||||
@@ -785,7 +787,7 @@ async def requests_sync_delta() -> 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="Jellyseerr not configured")
|
||||
raise HTTPException(status_code=400, detail="Seerr not configured")
|
||||
state = await requests_router.start_requests_delta_sync(
|
||||
runtime.jellyseerr_base_url, runtime.jellyseerr_api_key
|
||||
)
|
||||
@@ -902,7 +904,7 @@ async def requests_cache(limit: int = 50) -> Dict[str, Any]:
|
||||
logger.info("Requests cache titles repaired via settings view: %s", repaired)
|
||||
hydrated = await _hydrate_cache_titles_from_jellyseerr(limit)
|
||||
if hydrated:
|
||||
logger.info("Requests cache titles hydrated via Jellyseerr: %s", hydrated)
|
||||
logger.info("Requests cache titles hydrated via Seerr: %s", hydrated)
|
||||
rows = get_request_cache_overview(limit)
|
||||
return {"rows": rows}
|
||||
|
||||
@@ -1073,7 +1075,7 @@ async def user_system_action(username: str, payload: Dict[str, Any]) -> Dict[str
|
||||
"username": user.get("username"),
|
||||
"local": {"status": "pending"},
|
||||
"jellyfin": {"status": "skipped", "detail": "Jellyfin not configured"},
|
||||
"jellyseerr": {"status": "skipped", "detail": "Jellyseerr not configured or no linked user ID"},
|
||||
"jellyseerr": {"status": "skipped", "detail": "Seerr not configured or no linked user ID"},
|
||||
"invites": {"status": "pending", "disabled": 0},
|
||||
}
|
||||
|
||||
|
||||
@@ -566,21 +566,21 @@ async def jellyfin_login(request: Request, form_data: OAuth2PasswordRequestForm
|
||||
}
|
||||
|
||||
|
||||
@router.post("/seerr/login")
|
||||
@router.post("/jellyseerr/login")
|
||||
async def jellyseerr_login(request: Request, form_data: OAuth2PasswordRequestForm = Depends()) -> dict:
|
||||
_enforce_login_rate_limit(request, form_data.username)
|
||||
runtime = get_runtime_settings()
|
||||
client = JellyseerrClient(runtime.jellyseerr_base_url, runtime.jellyseerr_api_key)
|
||||
if not client.configured():
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Jellyseerr not configured")
|
||||
payload = {"email": form_data.username, "password": form_data.password}
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Seerr not configured")
|
||||
try:
|
||||
response = await client.post("/api/v1/auth/login", payload=payload)
|
||||
response = await client.login_local(form_data.username, form_data.password)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc)) from exc
|
||||
if not isinstance(response, dict):
|
||||
_record_login_failure(request, form_data.username)
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid Jellyseerr credentials")
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid Seerr credentials")
|
||||
jellyseerr_user_id = _extract_jellyseerr_user_id(response)
|
||||
ci_matches = get_users_by_username_ci(form_data.username)
|
||||
preferred_match = _pick_preferred_ci_user_match(ci_matches, form_data.username)
|
||||
|
||||
@@ -76,7 +76,6 @@ _artwork_prefetch_state: Dict[str, Any] = {
|
||||
"finished_at": None,
|
||||
}
|
||||
_artwork_prefetch_task: Optional[asyncio.Task] = None
|
||||
_media_endpoint_supported: Optional[bool] = None
|
||||
|
||||
STATUS_LABELS = {
|
||||
1: "Waiting for approval",
|
||||
@@ -419,23 +418,6 @@ async def _hydrate_title_from_tmdb(
|
||||
return None, None
|
||||
|
||||
|
||||
async def _hydrate_media_details(client: JellyseerrClient, media_id: Optional[int]) -> Optional[Dict[str, Any]]:
|
||||
if not media_id:
|
||||
return None
|
||||
global _media_endpoint_supported
|
||||
if _media_endpoint_supported is False:
|
||||
return None
|
||||
try:
|
||||
details = await client.get_media(int(media_id))
|
||||
except httpx.HTTPStatusError as exc:
|
||||
if exc.response is not None and exc.response.status_code == 405:
|
||||
_media_endpoint_supported = False
|
||||
logger.info("Jellyseerr media endpoint rejected GET requests; skipping media lookups.")
|
||||
return None
|
||||
_media_endpoint_supported = True
|
||||
return details if isinstance(details, dict) else None
|
||||
|
||||
|
||||
async def _hydrate_artwork_from_tmdb(
|
||||
client: JellyseerrClient, media_type: Optional[str], tmdb_id: Optional[int]
|
||||
) -> tuple[Optional[str], Optional[str]]:
|
||||
@@ -511,7 +493,7 @@ async def _sync_all_requests(client: JellyseerrClient) -> int:
|
||||
skip = 0
|
||||
stored = 0
|
||||
cache_mode = (get_runtime_settings().artwork_cache_mode or "remote").lower()
|
||||
logger.info("Jellyseerr sync starting: take=%s", take)
|
||||
logger.info("Seerr sync starting: take=%s", take)
|
||||
_sync_state.update(
|
||||
{
|
||||
"status": "running",
|
||||
@@ -527,11 +509,11 @@ async def _sync_all_requests(client: JellyseerrClient) -> int:
|
||||
try:
|
||||
response = await client.get_recent_requests(take=take, skip=skip)
|
||||
except httpx.HTTPError as exc:
|
||||
logger.warning("Jellyseerr sync failed at skip=%s: %s", skip, exc)
|
||||
logger.warning("Seerr sync failed at skip=%s: %s", skip, exc)
|
||||
_sync_state.update({"status": "failed", "message": f"Sync failed: {exc}"})
|
||||
break
|
||||
if not isinstance(response, dict):
|
||||
logger.warning("Jellyseerr sync stopped: non-dict response at skip=%s", skip)
|
||||
logger.warning("Seerr sync stopped: non-dict response at skip=%s", skip)
|
||||
_sync_state.update({"status": "failed", "message": "Invalid response"})
|
||||
break
|
||||
if _sync_state["total"] is None:
|
||||
@@ -546,7 +528,7 @@ async def _sync_all_requests(client: JellyseerrClient) -> int:
|
||||
_sync_state["total"] = total
|
||||
items = response.get("results") or []
|
||||
if not isinstance(items, list) or not items:
|
||||
logger.info("Jellyseerr sync completed: no more results at skip=%s", skip)
|
||||
logger.info("Seerr sync completed: no more results at skip=%s", skip)
|
||||
break
|
||||
for item in items:
|
||||
if not isinstance(item, dict):
|
||||
@@ -559,38 +541,18 @@ async def _sync_all_requests(client: JellyseerrClient) -> int:
|
||||
cached = get_request_cache_by_id(request_id)
|
||||
if cached and cached.get("title"):
|
||||
cached_title = cached.get("title")
|
||||
if not payload.get("title") or not payload.get("media_id"):
|
||||
logger.debug("Jellyseerr sync hydrate request_id=%s", request_id)
|
||||
needs_details = (
|
||||
not payload.get("title")
|
||||
or not payload.get("media_id")
|
||||
or not payload.get("tmdb_id")
|
||||
or not payload.get("media_type")
|
||||
)
|
||||
if needs_details:
|
||||
logger.debug("Seerr sync hydrate request_id=%s", request_id)
|
||||
details = await _get_request_details(client, request_id)
|
||||
if isinstance(details, dict):
|
||||
payload = _parse_request_payload(details)
|
||||
item = details
|
||||
if (
|
||||
not payload.get("title")
|
||||
and payload.get("media_id")
|
||||
and (not payload.get("tmdb_id") or not payload.get("media_type"))
|
||||
):
|
||||
media_details = await _hydrate_media_details(client, payload.get("media_id"))
|
||||
if isinstance(media_details, dict):
|
||||
media_title = media_details.get("title") or media_details.get("name")
|
||||
if media_title:
|
||||
payload["title"] = media_title
|
||||
if not payload.get("year") and media_details.get("year"):
|
||||
payload["year"] = media_details.get("year")
|
||||
if not payload.get("tmdb_id") and media_details.get("tmdbId"):
|
||||
payload["tmdb_id"] = media_details.get("tmdbId")
|
||||
if not payload.get("media_type") and media_details.get("mediaType"):
|
||||
payload["media_type"] = media_details.get("mediaType")
|
||||
if isinstance(item, dict):
|
||||
existing_media = item.get("media")
|
||||
if isinstance(existing_media, dict):
|
||||
merged = dict(media_details)
|
||||
for key, value in existing_media.items():
|
||||
if value is not None:
|
||||
merged[key] = value
|
||||
item["media"] = merged
|
||||
else:
|
||||
item["media"] = media_details
|
||||
poster_path, backdrop_path = _extract_artwork_paths(item)
|
||||
if cache_mode == "cache" and not (poster_path or backdrop_path):
|
||||
details = await _get_request_details(client, request_id)
|
||||
@@ -629,12 +591,12 @@ async def _sync_all_requests(client: JellyseerrClient) -> int:
|
||||
stored += 1
|
||||
_sync_state["stored"] = stored
|
||||
if len(items) < take:
|
||||
logger.info("Jellyseerr sync completed: stored=%s", stored)
|
||||
logger.info("Seerr sync completed: stored=%s", stored)
|
||||
break
|
||||
skip += take
|
||||
_sync_state["skip"] = skip
|
||||
_sync_state["message"] = f"Synced {stored} requests"
|
||||
logger.info("Jellyseerr sync progress: stored=%s skip=%s", stored, skip)
|
||||
logger.info("Seerr sync progress: stored=%s skip=%s", stored, skip)
|
||||
_sync_state.update(
|
||||
{
|
||||
"status": "completed",
|
||||
@@ -659,7 +621,7 @@ async def _sync_delta_requests(client: JellyseerrClient) -> int:
|
||||
stored = 0
|
||||
unchanged_pages = 0
|
||||
cache_mode = (get_runtime_settings().artwork_cache_mode or "remote").lower()
|
||||
logger.info("Jellyseerr delta sync starting: take=%s", take)
|
||||
logger.info("Seerr delta sync starting: take=%s", take)
|
||||
_sync_state.update(
|
||||
{
|
||||
"status": "running",
|
||||
@@ -675,16 +637,16 @@ async def _sync_delta_requests(client: JellyseerrClient) -> int:
|
||||
try:
|
||||
response = await client.get_recent_requests(take=take, skip=skip)
|
||||
except httpx.HTTPError as exc:
|
||||
logger.warning("Jellyseerr delta sync failed at skip=%s: %s", skip, exc)
|
||||
logger.warning("Seerr delta sync failed at skip=%s: %s", skip, exc)
|
||||
_sync_state.update({"status": "failed", "message": f"Delta sync failed: {exc}"})
|
||||
break
|
||||
if not isinstance(response, dict):
|
||||
logger.warning("Jellyseerr delta sync stopped: non-dict response at skip=%s", skip)
|
||||
logger.warning("Seerr delta sync stopped: non-dict response at skip=%s", skip)
|
||||
_sync_state.update({"status": "failed", "message": "Invalid response"})
|
||||
break
|
||||
items = response.get("results") or []
|
||||
if not isinstance(items, list) or not items:
|
||||
logger.info("Jellyseerr delta sync completed: no more results at skip=%s", skip)
|
||||
logger.info("Seerr delta sync completed: no more results at skip=%s", skip)
|
||||
break
|
||||
page_changed = False
|
||||
for item in items:
|
||||
@@ -698,37 +660,17 @@ async def _sync_delta_requests(client: JellyseerrClient) -> int:
|
||||
cached_title = cached.get("title") if cached else None
|
||||
if cached and incoming_updated and cached.get("updated_at") == incoming_updated and cached.get("title"):
|
||||
continue
|
||||
if not payload.get("title") or not payload.get("media_id"):
|
||||
needs_details = (
|
||||
not payload.get("title")
|
||||
or not payload.get("media_id")
|
||||
or not payload.get("tmdb_id")
|
||||
or not payload.get("media_type")
|
||||
)
|
||||
if needs_details:
|
||||
details = await _get_request_details(client, request_id)
|
||||
if isinstance(details, dict):
|
||||
payload = _parse_request_payload(details)
|
||||
item = details
|
||||
if (
|
||||
not payload.get("title")
|
||||
and payload.get("media_id")
|
||||
and (not payload.get("tmdb_id") or not payload.get("media_type"))
|
||||
):
|
||||
media_details = await _hydrate_media_details(client, payload.get("media_id"))
|
||||
if isinstance(media_details, dict):
|
||||
media_title = media_details.get("title") or media_details.get("name")
|
||||
if media_title:
|
||||
payload["title"] = media_title
|
||||
if not payload.get("year") and media_details.get("year"):
|
||||
payload["year"] = media_details.get("year")
|
||||
if not payload.get("tmdb_id") and media_details.get("tmdbId"):
|
||||
payload["tmdb_id"] = media_details.get("tmdbId")
|
||||
if not payload.get("media_type") and media_details.get("mediaType"):
|
||||
payload["media_type"] = media_details.get("mediaType")
|
||||
if isinstance(item, dict):
|
||||
existing_media = item.get("media")
|
||||
if isinstance(existing_media, dict):
|
||||
merged = dict(media_details)
|
||||
for key, value in existing_media.items():
|
||||
if value is not None:
|
||||
merged[key] = value
|
||||
item["media"] = merged
|
||||
else:
|
||||
item["media"] = media_details
|
||||
poster_path, backdrop_path = _extract_artwork_paths(item)
|
||||
if cache_mode == "cache" and not (poster_path or backdrop_path):
|
||||
details = await _get_request_details(client, request_id)
|
||||
@@ -772,15 +714,15 @@ async def _sync_delta_requests(client: JellyseerrClient) -> int:
|
||||
else:
|
||||
unchanged_pages = 0
|
||||
if len(items) < take or unchanged_pages >= 2:
|
||||
logger.info("Jellyseerr delta sync completed: stored=%s", stored)
|
||||
logger.info("Seerr delta sync completed: stored=%s", stored)
|
||||
break
|
||||
skip += take
|
||||
_sync_state["skip"] = skip
|
||||
_sync_state["message"] = f"Delta synced {stored} requests"
|
||||
logger.info("Jellyseerr delta sync progress: stored=%s skip=%s", stored, skip)
|
||||
logger.info("Seerr delta sync progress: stored=%s skip=%s", stored, skip)
|
||||
deduped = prune_duplicate_requests_cache()
|
||||
if deduped:
|
||||
logger.info("Jellyseerr delta sync removed duplicate rows: %s", deduped)
|
||||
logger.info("Seerr delta sync removed duplicate rows: %s", deduped)
|
||||
_sync_state.update(
|
||||
{
|
||||
"status": "completed",
|
||||
@@ -1118,7 +1060,7 @@ async def run_daily_requests_full_sync() -> None:
|
||||
runtime = get_runtime_settings()
|
||||
client = JellyseerrClient(runtime.jellyseerr_base_url, runtime.jellyseerr_api_key)
|
||||
if not client.configured():
|
||||
logger.info("Daily full sync skipped: Jellyseerr not configured.")
|
||||
logger.info("Daily full sync skipped: Seerr not configured.")
|
||||
continue
|
||||
if _sync_task and not _sync_task.done():
|
||||
logger.info("Daily full sync skipped: another sync is running.")
|
||||
@@ -1144,7 +1086,7 @@ async def start_requests_sync(base_url: Optional[str], api_key: Optional[str]) -
|
||||
if _sync_task and not _sync_task.done():
|
||||
return dict(_sync_state)
|
||||
if not base_url:
|
||||
_sync_state.update({"status": "failed", "message": "Jellyseerr not configured"})
|
||||
_sync_state.update({"status": "failed", "message": "Seerr not configured"})
|
||||
return dict(_sync_state)
|
||||
client = JellyseerrClient(base_url, api_key)
|
||||
_sync_state.update(
|
||||
@@ -1163,7 +1105,7 @@ async def start_requests_sync(base_url: Optional[str], api_key: Optional[str]) -
|
||||
try:
|
||||
await _sync_all_requests(client)
|
||||
except Exception as exc:
|
||||
logger.exception("Jellyseerr sync failed")
|
||||
logger.exception("Seerr sync failed")
|
||||
_sync_state.update(
|
||||
{
|
||||
"status": "failed",
|
||||
@@ -1181,7 +1123,7 @@ async def start_requests_delta_sync(base_url: Optional[str], api_key: Optional[s
|
||||
if _sync_task and not _sync_task.done():
|
||||
return dict(_sync_state)
|
||||
if not base_url:
|
||||
_sync_state.update({"status": "failed", "message": "Jellyseerr not configured"})
|
||||
_sync_state.update({"status": "failed", "message": "Seerr not configured"})
|
||||
return dict(_sync_state)
|
||||
client = JellyseerrClient(base_url, api_key)
|
||||
_sync_state.update(
|
||||
@@ -1200,7 +1142,7 @@ async def start_requests_delta_sync(base_url: Optional[str], api_key: Optional[s
|
||||
try:
|
||||
await _sync_delta_requests(client)
|
||||
except Exception as exc:
|
||||
logger.exception("Jellyseerr delta sync failed")
|
||||
logger.exception("Seerr delta sync failed")
|
||||
_sync_state.update(
|
||||
{
|
||||
"status": "failed",
|
||||
@@ -1514,7 +1456,7 @@ async def recent_requests(
|
||||
allow_remote = mode == "always_js"
|
||||
if allow_remote:
|
||||
if not client.configured():
|
||||
raise HTTPException(status_code=400, detail="Jellyseerr not configured")
|
||||
raise HTTPException(status_code=400, detail="Seerr not configured")
|
||||
try:
|
||||
await _ensure_requests_cache(client)
|
||||
except httpx.HTTPStatusError as exc:
|
||||
@@ -1690,7 +1632,7 @@ async def search_requests(
|
||||
runtime = get_runtime_settings()
|
||||
client = JellyseerrClient(runtime.jellyseerr_base_url, runtime.jellyseerr_api_key)
|
||||
if not client.configured():
|
||||
raise HTTPException(status_code=400, detail="Jellyseerr not configured")
|
||||
raise HTTPException(status_code=400, detail="Seerr not configured")
|
||||
|
||||
try:
|
||||
response = await client.search(query=query, page=page)
|
||||
|
||||
@@ -41,7 +41,7 @@ async def services_status() -> Dict[str, Any]:
|
||||
services = []
|
||||
services.append(
|
||||
await _check(
|
||||
"Jellyseerr",
|
||||
"Seerr",
|
||||
jellyseerr.configured(),
|
||||
lambda: jellyseerr.get_recent_requests(take=1, skip=0),
|
||||
)
|
||||
@@ -109,8 +109,13 @@ async def test_service(service: str) -> Dict[str, Any]:
|
||||
|
||||
service_key = service.strip().lower()
|
||||
checks = {
|
||||
"seerr": (
|
||||
"Seerr",
|
||||
jellyseerr.configured(),
|
||||
lambda: jellyseerr.get_recent_requests(take=1, skip=0),
|
||||
),
|
||||
"jellyseerr": (
|
||||
"Jellyseerr",
|
||||
"Seerr",
|
||||
jellyseerr.configured(),
|
||||
lambda: jellyseerr.get_recent_requests(take=1, skip=0),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user