Finalize dev-1.3 upgrades and Seerr updates

This commit is contained in:
2026-02-28 21:41:16 +13:00
parent 05a3d1e3b0
commit c205df4367
27 changed files with 1201 additions and 214 deletions

View File

@@ -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",
)
)

View File

@@ -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'

View File

@@ -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

View File

@@ -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()

View File

@@ -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},
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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),
),

View File

@@ -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 [])

View File

@@ -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")

View File

@@ -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

View File

@@ -1,9 +1,9 @@
fastapi==0.115.0
uvicorn==0.30.6
httpx==0.27.2
pydantic==2.9.2
pydantic-settings==2.5.2
python-jose[cryptography]==3.3.0
fastapi==0.134.0
uvicorn==0.41.0
httpx==0.28.1
pydantic==2.12.5
pydantic-settings==2.13.1
python-jose[cryptography]==3.5.0
passlib==1.7.4
python-multipart==0.0.9
Pillow==10.4.0
python-multipart==0.0.22
Pillow==12.1.1