Finalize dev-1.3 upgrades and Seerr updates
This commit is contained in:
@@ -9,12 +9,12 @@ def triage_snapshot(snapshot: Snapshot) -> TriageResult:
|
||||
|
||||
if snapshot.state == NormalizedState.requested:
|
||||
root_cause = "approval"
|
||||
summary = "The request is waiting for approval in Jellyseerr."
|
||||
summary = "The request is waiting for approval in Seerr."
|
||||
recommendations.append(
|
||||
TriageRecommendation(
|
||||
action_id="wait_for_approval",
|
||||
title="Ask an admin to approve the request",
|
||||
reason="Jellyseerr has not marked this request as approved.",
|
||||
reason="Seerr has not marked this request as approved.",
|
||||
risk="low",
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
BUILD_NUMBER = "2702261314"
|
||||
CHANGELOG = '2026-01-22\\n- Initial commit\\n- Ignore build artifacts\\n- Update README\\n- Update README with Docker-first guide\\n\\n2026-01-23\\n- Fix cache titles via Jellyseerr media lookup\\n- Split search actions and improve download options\\n- Fallback manual grab to qBittorrent\\n- Hide header actions when signed out\\n- Add feedback form and webhook\\n- Fix cache titles and move feedback link\\n- Show available status on landing when in Jellyfin\\n- Add default branding assets when missing\\n- Use bundled branding assets\\n- Remove password fields from users page\\n- Add Docker Hub compose override\\n- Fix backend Dockerfile paths for root context\\n- Copy public assets into frontend image\\n- Use backend branding assets for logo and favicon\\n\\n2026-01-24\\n- Route grabs through Sonarr/Radarr only\\n- Document fix buttons in how-it-works\\n- Clarify how-it-works steps and fixes\\n- Map Prowlarr releases to Arr indexers for manual grab\\n- Improve request handling and qBittorrent categories\\n\\n2026-01-25\\n- Add site banner, build number, and changelog\\n- Automate build number tagging and sync\\n- Improve mobile header layout\\n- Move account actions into avatar menu\\n- Add user stats and activity tracking\\n- Add Jellyfin login cache and admin-only stats\\n- Tidy request sync controls\\n- Seed branding logo from bundled assets\\n- Serve bundled branding assets by default\\n- Harden request cache titles and cache-only reads\\n- Build 2501262041\\n\\n2026-01-26\\n- Fix cache title hydration\\n- Fix sync progress bar animation\\n\\n2026-01-27\\n- Add cache control artwork stats\\n- Improve cache stats performance (build 271261145)\\n- Fix backend cache stats import (build 271261149)\\n- Clarify request sync settings (build 271261159)\\n- Bump build number to 271261202\\n- Fix request titles in snapshots (build 271261219)\\n- Fix snapshot title fallback (build 271261228)\\n- Add cache load spinner (build 271261238)\\n- Bump build number (process 2) 271261322\\n- Add service test buttons (build 271261335)\\n- Fallback to TMDB when artwork cache fails (build 271261524)\\n- Hydrate missing artwork from Jellyseerr (build 271261539)\\n\\n2026-01-29\\n- release: 2901262036\\n- release: 2901262044\\n- release: 2901262102\\n- Hardcode build number in backend\\n- Bake build number and changelog\\n- Update full changelog\\n- Tidy full changelog\\n- Build 2901262240: cache users\n\n2026-01-30\n- Merge backend and frontend into one container'
|
||||
BUILD_NUMBER = "2802262051"
|
||||
CHANGELOG = '2026-01-22\\n- Initial commit\\n- Ignore build artifacts\\n- Update README\\n- Update README with Docker-first guide\\n\\n2026-01-23\\n- Fix cache titles via Seerr media lookup\\n- Split search actions and improve download options\\n- Fallback manual grab to qBittorrent\\n- Hide header actions when signed out\\n- Add feedback form and webhook\\n- Fix cache titles and move feedback link\\n- Show available status on landing when in Jellyfin\\n- Add default branding assets when missing\\n- Use bundled branding assets\\n- Remove password fields from users page\\n- Add Docker Hub compose override\\n- Fix backend Dockerfile paths for root context\\n- Copy public assets into frontend image\\n- Use backend branding assets for logo and favicon\\n\\n2026-01-24\\n- Route grabs through Sonarr/Radarr only\\n- Document fix buttons in how-it-works\\n- Clarify how-it-works steps and fixes\\n- Map Prowlarr releases to Arr indexers for manual grab\\n- Improve request handling and qBittorrent categories\\n\\n2026-01-25\\n- Add site banner, build number, and changelog\\n- Automate build number tagging and sync\\n- Improve mobile header layout\\n- Move account actions into avatar menu\\n- Add user stats and activity tracking\\n- Add Jellyfin login cache and admin-only stats\\n- Tidy request sync controls\\n- Seed branding logo from bundled assets\\n- Serve bundled branding assets by default\\n- Harden request cache titles and cache-only reads\\n- Build 2501262041\\n\\n2026-01-26\\n- Fix cache title hydration\\n- Fix sync progress bar animation\\n\\n2026-01-27\\n- Add cache control artwork stats\\n- Improve cache stats performance (build 271261145)\\n- Fix backend cache stats import (build 271261149)\\n- Clarify request sync settings (build 271261159)\\n- Bump build number to 271261202\\n- Fix request titles in snapshots (build 271261219)\\n- Fix snapshot title fallback (build 271261228)\\n- Add cache load spinner (build 271261238)\\n- Bump build number (process 2) 271261322\\n- Add service test buttons (build 271261335)\\n- Fallback to TMDB when artwork cache fails (build 271261524)\\n- Hydrate missing artwork from Seerr (build 271261539)\\n\\n2026-01-29\\n- release: 2901262036\\n- release: 2901262044\\n- release: 2901262102\\n- Hardcode build number in backend\\n- Bake build number and changelog\\n- Update full changelog\\n- Tidy full changelog\\n- Build 2901262240: cache users\n\n2026-01-30\n- Merge backend and frontend into one container'
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from typing import Any, Dict, Optional
|
||||
import httpx
|
||||
from .base import ApiClient
|
||||
|
||||
|
||||
@@ -18,9 +19,6 @@ class JellyseerrClient(ApiClient):
|
||||
},
|
||||
)
|
||||
|
||||
async def get_media(self, media_id: int) -> Optional[Dict[str, Any]]:
|
||||
return await self.get(f"/api/v1/media/{media_id}")
|
||||
|
||||
async def get_movie(self, tmdb_id: int) -> Optional[Dict[str, Any]]:
|
||||
return await self.get(f"/api/v1/movie/{tmdb_id}")
|
||||
|
||||
@@ -50,3 +48,14 @@ class JellyseerrClient(ApiClient):
|
||||
|
||||
async def delete_user(self, user_id: int) -> Optional[Dict[str, Any]]:
|
||||
return await self.delete(f"/api/v1/user/{user_id}")
|
||||
|
||||
async def login_local(self, email: str, password: str) -> Optional[Dict[str, Any]]:
|
||||
payload = {"email": email, "password": password}
|
||||
try:
|
||||
return await self.post("/api/v1/auth/local", payload=payload)
|
||||
except httpx.HTTPStatusError as exc:
|
||||
# Backward compatibility for older Seerr/Overseerr deployments
|
||||
# that still expose /auth/login instead of /auth/local.
|
||||
if exc.response is not None and exc.response.status_code in {404, 405}:
|
||||
return await self.post("/api/v1/auth/login", payload=payload)
|
||||
raise
|
||||
|
||||
@@ -703,7 +703,7 @@ def get_all_users() -> list[Dict[str, Any]]:
|
||||
}
|
||||
)
|
||||
# Admin user management uses Jellyfin as the source of truth for non-admin
|
||||
# user objects. Jellyseerr rows are treated as enrichment-only and hidden
|
||||
# user objects. Seerr rows are treated as enrichment-only and hidden
|
||||
# from admin/user-management views to avoid duplicate accounts in the UI.
|
||||
def _provider_rank(user: Dict[str, Any]) -> int:
|
||||
provider = str(user.get("auth_provider") or "local").strip().lower()
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
|
||||
@@ -29,7 +29,7 @@ async def sync_jellyfin_users() -> int:
|
||||
if not isinstance(users, list):
|
||||
return 0
|
||||
save_jellyfin_users_cache(users)
|
||||
# Jellyfin is the canonical source for local user objects; Jellyseerr IDs are
|
||||
# Jellyfin is the canonical source for local user objects; Seerr IDs are
|
||||
# matched as enrichment when possible.
|
||||
jellyseerr_users = get_cached_jellyseerr_users()
|
||||
candidate_map = build_jellyseerr_candidate_map(jellyseerr_users or [])
|
||||
|
||||
@@ -242,14 +242,14 @@ async def build_snapshot(request_id: str) -> Snapshot:
|
||||
|
||||
allow_remote = mode == "always_js" and jellyseerr.configured()
|
||||
if not jellyseerr.configured() and not cached_request:
|
||||
timeline.append(TimelineHop(service="Jellyseerr", status="not_configured"))
|
||||
timeline.append(TimelineHop(service="Seerr", status="not_configured"))
|
||||
timeline.append(TimelineHop(service="Sonarr/Radarr", status="not_configured"))
|
||||
timeline.append(TimelineHop(service="Prowlarr", status="not_configured"))
|
||||
timeline.append(TimelineHop(service="qBittorrent", status="not_configured"))
|
||||
snapshot.timeline = timeline
|
||||
return snapshot
|
||||
if cached_request is None and not allow_remote:
|
||||
timeline.append(TimelineHop(service="Jellyseerr", status="cache_miss"))
|
||||
timeline.append(TimelineHop(service="Seerr", status="cache_miss"))
|
||||
snapshot.timeline = timeline
|
||||
snapshot.state = NormalizedState.unknown
|
||||
snapshot.state_reason = "Request not found in cache"
|
||||
@@ -260,20 +260,20 @@ async def build_snapshot(request_id: str) -> Snapshot:
|
||||
try:
|
||||
jelly_request = await jellyseerr.get_request(request_id)
|
||||
logging.getLogger(__name__).debug(
|
||||
"snapshot jellyseerr fetch: request_id=%s mode=%s", request_id, mode
|
||||
"snapshot Seerr fetch: request_id=%s mode=%s", request_id, mode
|
||||
)
|
||||
except Exception as exc:
|
||||
timeline.append(TimelineHop(service="Jellyseerr", status="error", details={"error": str(exc)}))
|
||||
timeline.append(TimelineHop(service="Seerr", status="error", details={"error": str(exc)}))
|
||||
snapshot.timeline = timeline
|
||||
snapshot.state = NormalizedState.failed
|
||||
snapshot.state_reason = "Failed to reach Jellyseerr"
|
||||
snapshot.state_reason = "Failed to reach Seerr"
|
||||
return snapshot
|
||||
|
||||
if not jelly_request:
|
||||
timeline.append(TimelineHop(service="Jellyseerr", status="not_found"))
|
||||
timeline.append(TimelineHop(service="Seerr", status="not_found"))
|
||||
snapshot.timeline = timeline
|
||||
snapshot.state = NormalizedState.unknown
|
||||
snapshot.state_reason = "Request not found in Jellyseerr"
|
||||
snapshot.state_reason = "Request not found in Seerr"
|
||||
return snapshot
|
||||
|
||||
jelly_status = jelly_request.get("status", "unknown")
|
||||
@@ -338,7 +338,7 @@ async def build_snapshot(request_id: str) -> Snapshot:
|
||||
|
||||
timeline.append(
|
||||
TimelineHop(
|
||||
service="Jellyseerr",
|
||||
service="Seerr",
|
||||
status=jelly_status_label,
|
||||
details={
|
||||
"requestedBy": jelly_request.get("requestedBy", {}).get("displayName")
|
||||
|
||||
@@ -114,7 +114,7 @@ def save_jellyseerr_users_cache(users: List[Dict[str, Any]]) -> List[Dict[str, A
|
||||
}
|
||||
)
|
||||
_save_cached_users(JELLYSEERR_CACHE_KEY, JELLYSEERR_CACHE_AT_KEY, normalized)
|
||||
logger.debug("Cached Jellyseerr users: %s", len(normalized))
|
||||
logger.debug("Cached Seerr users: %s", len(normalized))
|
||||
return normalized
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user