Persist Seerr media failure suppression and reduce sync error noise
This commit is contained in:
@@ -1 +1 @@
|
||||
0103262231
|
||||
0103262251
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
BUILD_NUMBER = "0103262231"
|
||||
BUILD_NUMBER = "0103262251"
|
||||
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'
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -18,6 +18,17 @@ class ApiClient:
|
||||
def headers(self) -> Dict[str, str]:
|
||||
return {"X-Api-Key": self.api_key} if self.api_key else {}
|
||||
|
||||
def _response_summary(self, response: Optional[httpx.Response]) -> Optional[Any]:
|
||||
if response is None:
|
||||
return None
|
||||
try:
|
||||
payload = sanitize_value(response.json())
|
||||
except ValueError:
|
||||
payload = sanitize_value(response.text)
|
||||
if isinstance(payload, str) and len(payload) > 500:
|
||||
return f"{payload[:500]}..."
|
||||
return payload
|
||||
|
||||
async def _request(
|
||||
self,
|
||||
method: str,
|
||||
@@ -60,6 +71,20 @@ class ApiClient:
|
||||
if not response.content:
|
||||
return None
|
||||
return response.json()
|
||||
except httpx.HTTPStatusError as exc:
|
||||
duration_ms = round((time.perf_counter() - started_at) * 1000, 2)
|
||||
response = exc.response
|
||||
status = response.status_code if response is not None else "unknown"
|
||||
log_fn = self.logger.error if isinstance(status, int) and status >= 500 else self.logger.warning
|
||||
log_fn(
|
||||
"outbound request returned error method=%s url=%s status=%s duration_ms=%s response=%s",
|
||||
method,
|
||||
url,
|
||||
status,
|
||||
duration_ms,
|
||||
self._response_summary(response),
|
||||
)
|
||||
raise
|
||||
except Exception:
|
||||
duration_ms = round((time.perf_counter() - started_at) * 1000, 2)
|
||||
self.logger.exception(
|
||||
|
||||
@@ -11,6 +11,11 @@ from .security import hash_password, verify_password
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
SEERR_MEDIA_FAILURE_SHORT_SUPPRESS_HOURS = 6
|
||||
SEERR_MEDIA_FAILURE_RETRY_SUPPRESS_HOURS = 24
|
||||
SEERR_MEDIA_FAILURE_PERSISTENT_SUPPRESS_DAYS = 30
|
||||
SEERR_MEDIA_FAILURE_PERSISTENT_THRESHOLD = 3
|
||||
|
||||
|
||||
def _db_path() -> str:
|
||||
path = settings.sqlite_path or "data/magent.db"
|
||||
@@ -271,6 +276,22 @@ def init_db() -> None:
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS seerr_media_failures (
|
||||
media_type TEXT NOT NULL,
|
||||
tmdb_id INTEGER NOT NULL,
|
||||
status_code INTEGER,
|
||||
error_message TEXT,
|
||||
failure_count INTEGER NOT NULL DEFAULT 1,
|
||||
first_failed_at TEXT NOT NULL,
|
||||
last_failed_at TEXT NOT NULL,
|
||||
suppress_until TEXT NOT NULL,
|
||||
is_persistent INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (media_type, tmdb_id)
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE INDEX IF NOT EXISTS idx_requests_cache_created_at
|
||||
@@ -289,6 +310,12 @@ def init_db() -> None:
|
||||
ON artwork_cache_status (updated_at)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE INDEX IF NOT EXISTS idx_seerr_media_failures_suppress_until
|
||||
ON seerr_media_failures (suppress_until)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS user_activity (
|
||||
@@ -2226,6 +2253,154 @@ def get_settings_overrides() -> Dict[str, str]:
|
||||
return overrides
|
||||
|
||||
|
||||
def get_seerr_media_failure(media_type: Optional[str], tmdb_id: Optional[int]) -> Optional[Dict[str, Any]]:
|
||||
if not media_type or not tmdb_id:
|
||||
return None
|
||||
normalized_media_type = str(media_type).strip().lower()
|
||||
try:
|
||||
normalized_tmdb_id = int(tmdb_id)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
with _connect() as conn:
|
||||
row = conn.execute(
|
||||
"""
|
||||
SELECT media_type, tmdb_id, status_code, error_message, failure_count,
|
||||
first_failed_at, last_failed_at, suppress_until, is_persistent
|
||||
FROM seerr_media_failures
|
||||
WHERE media_type = ? AND tmdb_id = ?
|
||||
""",
|
||||
(normalized_media_type, normalized_tmdb_id),
|
||||
).fetchone()
|
||||
if not row:
|
||||
return None
|
||||
return {
|
||||
"media_type": row[0],
|
||||
"tmdb_id": row[1],
|
||||
"status_code": row[2],
|
||||
"error_message": row[3],
|
||||
"failure_count": row[4],
|
||||
"first_failed_at": row[5],
|
||||
"last_failed_at": row[6],
|
||||
"suppress_until": row[7],
|
||||
"is_persistent": bool(row[8]),
|
||||
}
|
||||
|
||||
|
||||
def is_seerr_media_failure_suppressed(media_type: Optional[str], tmdb_id: Optional[int]) -> bool:
|
||||
record = get_seerr_media_failure(media_type, tmdb_id)
|
||||
if not record:
|
||||
return False
|
||||
suppress_until = _parse_datetime_value(record.get("suppress_until"))
|
||||
if suppress_until and suppress_until > datetime.now(timezone.utc):
|
||||
return True
|
||||
clear_seerr_media_failure(media_type, tmdb_id)
|
||||
return False
|
||||
|
||||
|
||||
def record_seerr_media_failure(
|
||||
media_type: Optional[str],
|
||||
tmdb_id: Optional[int],
|
||||
*,
|
||||
status_code: Optional[int] = None,
|
||||
error_message: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
if not media_type or not tmdb_id:
|
||||
return {}
|
||||
normalized_media_type = str(media_type).strip().lower()
|
||||
normalized_tmdb_id = int(tmdb_id)
|
||||
now = datetime.now(timezone.utc)
|
||||
existing = get_seerr_media_failure(normalized_media_type, normalized_tmdb_id)
|
||||
failure_count = int(existing.get("failure_count", 0)) + 1 if existing else 1
|
||||
is_persistent = failure_count >= SEERR_MEDIA_FAILURE_PERSISTENT_THRESHOLD
|
||||
if is_persistent:
|
||||
suppress_until = now + timedelta(days=SEERR_MEDIA_FAILURE_PERSISTENT_SUPPRESS_DAYS)
|
||||
elif failure_count >= 2:
|
||||
suppress_until = now + timedelta(hours=SEERR_MEDIA_FAILURE_RETRY_SUPPRESS_HOURS)
|
||||
else:
|
||||
suppress_until = now + timedelta(hours=SEERR_MEDIA_FAILURE_SHORT_SUPPRESS_HOURS)
|
||||
payload = {
|
||||
"media_type": normalized_media_type,
|
||||
"tmdb_id": normalized_tmdb_id,
|
||||
"status_code": status_code,
|
||||
"error_message": error_message,
|
||||
"failure_count": failure_count,
|
||||
"first_failed_at": existing.get("first_failed_at") if existing else now.isoformat(),
|
||||
"last_failed_at": now.isoformat(),
|
||||
"suppress_until": suppress_until.isoformat(),
|
||||
"is_persistent": is_persistent,
|
||||
}
|
||||
with _connect() as conn:
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO seerr_media_failures (
|
||||
media_type,
|
||||
tmdb_id,
|
||||
status_code,
|
||||
error_message,
|
||||
failure_count,
|
||||
first_failed_at,
|
||||
last_failed_at,
|
||||
suppress_until,
|
||||
is_persistent
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(media_type, tmdb_id) DO UPDATE SET
|
||||
status_code = excluded.status_code,
|
||||
error_message = excluded.error_message,
|
||||
failure_count = excluded.failure_count,
|
||||
first_failed_at = excluded.first_failed_at,
|
||||
last_failed_at = excluded.last_failed_at,
|
||||
suppress_until = excluded.suppress_until,
|
||||
is_persistent = excluded.is_persistent
|
||||
""",
|
||||
(
|
||||
payload["media_type"],
|
||||
payload["tmdb_id"],
|
||||
payload["status_code"],
|
||||
payload["error_message"],
|
||||
payload["failure_count"],
|
||||
payload["first_failed_at"],
|
||||
payload["last_failed_at"],
|
||||
payload["suppress_until"],
|
||||
1 if payload["is_persistent"] else 0,
|
||||
),
|
||||
)
|
||||
logger.warning(
|
||||
"seerr_media_failure upsert: media_type=%s tmdb_id=%s status=%s failure_count=%s suppress_until=%s persistent=%s",
|
||||
payload["media_type"],
|
||||
payload["tmdb_id"],
|
||||
payload["status_code"],
|
||||
payload["failure_count"],
|
||||
payload["suppress_until"],
|
||||
payload["is_persistent"],
|
||||
)
|
||||
return payload
|
||||
|
||||
|
||||
def clear_seerr_media_failure(media_type: Optional[str], tmdb_id: Optional[int]) -> None:
|
||||
if not media_type or not tmdb_id:
|
||||
return
|
||||
normalized_media_type = str(media_type).strip().lower()
|
||||
try:
|
||||
normalized_tmdb_id = int(tmdb_id)
|
||||
except (TypeError, ValueError):
|
||||
return
|
||||
with _connect() as conn:
|
||||
deleted = conn.execute(
|
||||
"""
|
||||
DELETE FROM seerr_media_failures
|
||||
WHERE media_type = ? AND tmdb_id = ?
|
||||
""",
|
||||
(normalized_media_type, normalized_tmdb_id),
|
||||
).rowcount
|
||||
if deleted:
|
||||
logger.info(
|
||||
"seerr_media_failure cleared: media_type=%s tmdb_id=%s",
|
||||
normalized_media_type,
|
||||
normalized_tmdb_id,
|
||||
)
|
||||
|
||||
|
||||
def run_integrity_check() -> str:
|
||||
with _connect() as conn:
|
||||
row = conn.execute("PRAGMA integrity_check").fetchone()
|
||||
|
||||
@@ -42,6 +42,9 @@ from ..db import (
|
||||
set_setting,
|
||||
update_artwork_cache_stats,
|
||||
cleanup_history,
|
||||
is_seerr_media_failure_suppressed,
|
||||
record_seerr_media_failure,
|
||||
clear_seerr_media_failure,
|
||||
)
|
||||
from ..models import Snapshot, TriageResult, RequestType
|
||||
from ..services.snapshot import build_snapshot
|
||||
@@ -50,6 +53,8 @@ router = APIRouter(prefix="/requests", tags=["requests"], dependencies=[Depends(
|
||||
|
||||
CACHE_TTL_SECONDS = 600
|
||||
_detail_cache: Dict[str, Tuple[float, Dict[str, Any]]] = {}
|
||||
FAILED_DETAIL_CACHE_TTL_SECONDS = 3600
|
||||
_failed_detail_cache: Dict[str, float] = {}
|
||||
REQUEST_CACHE_TTL_SECONDS = 600
|
||||
logger = logging.getLogger(__name__)
|
||||
_sync_state: Dict[str, Any] = {
|
||||
@@ -100,6 +105,45 @@ def _cache_get(key: str) -> Optional[Dict[str, Any]]:
|
||||
|
||||
def _cache_set(key: str, payload: Dict[str, Any]) -> None:
|
||||
_detail_cache[key] = (time.time() + CACHE_TTL_SECONDS, payload)
|
||||
_failed_detail_cache.pop(key, None)
|
||||
|
||||
|
||||
def _failure_cache_has(key: str) -> bool:
|
||||
expires_at = _failed_detail_cache.get(key)
|
||||
if not expires_at:
|
||||
return False
|
||||
if expires_at < time.time():
|
||||
_failed_detail_cache.pop(key, None)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _failure_cache_set(key: str, ttl_seconds: int = FAILED_DETAIL_CACHE_TTL_SECONDS) -> None:
|
||||
_failed_detail_cache[key] = time.time() + ttl_seconds
|
||||
|
||||
|
||||
def _extract_http_error_message(exc: httpx.HTTPStatusError) -> Optional[str]:
|
||||
response = exc.response
|
||||
if response is None:
|
||||
return None
|
||||
try:
|
||||
payload = response.json()
|
||||
except ValueError:
|
||||
payload = response.text
|
||||
if isinstance(payload, dict):
|
||||
message = payload.get("message") or payload.get("error")
|
||||
return str(message).strip() if message else json.dumps(payload, ensure_ascii=True)
|
||||
if isinstance(payload, str):
|
||||
trimmed = payload.strip()
|
||||
return trimmed or None
|
||||
return str(payload)
|
||||
|
||||
|
||||
def _should_persist_seerr_media_failure(exc: httpx.HTTPStatusError) -> bool:
|
||||
response = exc.response
|
||||
if response is None:
|
||||
return False
|
||||
return response.status_code == 404 or response.status_code >= 500
|
||||
|
||||
|
||||
def _status_label(value: Any) -> str:
|
||||
@@ -383,9 +427,12 @@ async def _get_request_details(client: JellyseerrClient, request_id: int) -> Opt
|
||||
cached = _cache_get(cache_key)
|
||||
if isinstance(cached, dict):
|
||||
return cached
|
||||
if _failure_cache_has(cache_key):
|
||||
return None
|
||||
try:
|
||||
fetched = await client.get_request(str(request_id))
|
||||
except httpx.HTTPStatusError:
|
||||
_failure_cache_set(cache_key)
|
||||
return None
|
||||
if isinstance(fetched, dict):
|
||||
_cache_set(cache_key, fetched)
|
||||
@@ -393,54 +440,80 @@ async def _get_request_details(client: JellyseerrClient, request_id: int) -> Opt
|
||||
return None
|
||||
|
||||
|
||||
async def _get_media_details(
|
||||
client: JellyseerrClient, media_type: Optional[str], tmdb_id: Optional[int]
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
if not tmdb_id or not media_type:
|
||||
return None
|
||||
normalized_media_type = str(media_type).strip().lower()
|
||||
if normalized_media_type not in {"movie", "tv"}:
|
||||
return None
|
||||
cache_key = f"media:{normalized_media_type}:{int(tmdb_id)}"
|
||||
cached = _cache_get(cache_key)
|
||||
if isinstance(cached, dict):
|
||||
return cached
|
||||
if is_seerr_media_failure_suppressed(normalized_media_type, int(tmdb_id)):
|
||||
logger.debug(
|
||||
"Seerr media hydration suppressed from db: media_type=%s tmdb_id=%s",
|
||||
normalized_media_type,
|
||||
tmdb_id,
|
||||
)
|
||||
_failure_cache_set(cache_key, ttl_seconds=FAILED_DETAIL_CACHE_TTL_SECONDS)
|
||||
return None
|
||||
if _failure_cache_has(cache_key):
|
||||
return None
|
||||
try:
|
||||
if normalized_media_type == "movie":
|
||||
fetched = await client.get_movie(int(tmdb_id))
|
||||
else:
|
||||
fetched = await client.get_tv(int(tmdb_id))
|
||||
except httpx.HTTPStatusError as exc:
|
||||
_failure_cache_set(cache_key)
|
||||
if _should_persist_seerr_media_failure(exc):
|
||||
record_seerr_media_failure(
|
||||
normalized_media_type,
|
||||
int(tmdb_id),
|
||||
status_code=exc.response.status_code if exc.response is not None else None,
|
||||
error_message=_extract_http_error_message(exc),
|
||||
)
|
||||
return None
|
||||
if isinstance(fetched, dict):
|
||||
clear_seerr_media_failure(normalized_media_type, int(tmdb_id))
|
||||
_cache_set(cache_key, fetched)
|
||||
return fetched
|
||||
return None
|
||||
|
||||
|
||||
async def _hydrate_title_from_tmdb(
|
||||
client: JellyseerrClient, media_type: Optional[str], tmdb_id: Optional[int]
|
||||
) -> tuple[Optional[str], Optional[int]]:
|
||||
if not tmdb_id or not media_type:
|
||||
return None, None
|
||||
try:
|
||||
if media_type == "movie":
|
||||
details = await client.get_movie(int(tmdb_id))
|
||||
if isinstance(details, dict):
|
||||
title = details.get("title")
|
||||
release_date = details.get("releaseDate")
|
||||
year = int(release_date[:4]) if release_date else None
|
||||
return title, year
|
||||
if media_type == "tv":
|
||||
details = await client.get_tv(int(tmdb_id))
|
||||
if isinstance(details, dict):
|
||||
title = details.get("name") or details.get("title")
|
||||
first_air = details.get("firstAirDate")
|
||||
year = int(first_air[:4]) if first_air else None
|
||||
return title, year
|
||||
except httpx.HTTPStatusError:
|
||||
details = await _get_media_details(client, media_type, tmdb_id)
|
||||
if not isinstance(details, dict):
|
||||
return None, None
|
||||
normalized_media_type = str(media_type).strip().lower() if media_type else None
|
||||
if normalized_media_type == "movie":
|
||||
title = details.get("title")
|
||||
release_date = details.get("releaseDate")
|
||||
year = int(release_date[:4]) if release_date else None
|
||||
return title, year
|
||||
if normalized_media_type == "tv":
|
||||
title = details.get("name") or details.get("title")
|
||||
first_air = details.get("firstAirDate")
|
||||
year = int(first_air[:4]) if first_air else None
|
||||
return title, year
|
||||
return None, None
|
||||
|
||||
|
||||
async def _hydrate_artwork_from_tmdb(
|
||||
client: JellyseerrClient, media_type: Optional[str], tmdb_id: Optional[int]
|
||||
) -> tuple[Optional[str], Optional[str]]:
|
||||
if not tmdb_id or not media_type:
|
||||
details = await _get_media_details(client, media_type, tmdb_id)
|
||||
if not isinstance(details, dict):
|
||||
return None, None
|
||||
try:
|
||||
if media_type == "movie":
|
||||
details = await client.get_movie(int(tmdb_id))
|
||||
if isinstance(details, dict):
|
||||
return (
|
||||
details.get("posterPath") or details.get("poster_path"),
|
||||
details.get("backdropPath") or details.get("backdrop_path"),
|
||||
)
|
||||
if media_type == "tv":
|
||||
details = await client.get_tv(int(tmdb_id))
|
||||
if isinstance(details, dict):
|
||||
return (
|
||||
details.get("posterPath") or details.get("poster_path"),
|
||||
details.get("backdropPath") or details.get("backdrop_path"),
|
||||
)
|
||||
except httpx.HTTPStatusError:
|
||||
return None, None
|
||||
return None, None
|
||||
return (
|
||||
details.get("posterPath") or details.get("poster_path"),
|
||||
details.get("backdropPath") or details.get("backdrop_path"),
|
||||
)
|
||||
|
||||
|
||||
def _artwork_url(path: Optional[str], size: str, cache_mode: str) -> Optional[str]:
|
||||
|
||||
@@ -3,6 +3,7 @@ import asyncio
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from urllib.parse import quote
|
||||
import httpx
|
||||
|
||||
from ..clients.jellyseerr import JellyseerrClient
|
||||
from ..clients.jellyfin import JellyfinClient
|
||||
@@ -18,6 +19,9 @@ from ..db import (
|
||||
get_recent_snapshots,
|
||||
get_setting,
|
||||
set_setting,
|
||||
is_seerr_media_failure_suppressed,
|
||||
record_seerr_media_failure,
|
||||
clear_seerr_media_failure,
|
||||
)
|
||||
from ..models import ActionOption, NormalizedState, RequestType, Snapshot, TimelineHop
|
||||
|
||||
@@ -53,6 +57,59 @@ def _pick_first(value: Any) -> Optional[Dict[str, Any]]:
|
||||
return None
|
||||
|
||||
|
||||
def _extract_http_error_message(exc: httpx.HTTPStatusError) -> Optional[str]:
|
||||
response = exc.response
|
||||
if response is None:
|
||||
return None
|
||||
try:
|
||||
payload = response.json()
|
||||
except ValueError:
|
||||
payload = response.text
|
||||
if isinstance(payload, dict):
|
||||
message = payload.get("message") or payload.get("error")
|
||||
return str(message).strip() if message else str(payload)
|
||||
if isinstance(payload, str):
|
||||
trimmed = payload.strip()
|
||||
return trimmed or None
|
||||
return str(payload)
|
||||
|
||||
|
||||
def _should_persist_seerr_media_failure(exc: httpx.HTTPStatusError) -> bool:
|
||||
response = exc.response
|
||||
if response is None:
|
||||
return False
|
||||
return response.status_code == 404 or response.status_code >= 500
|
||||
|
||||
|
||||
async def _get_seerr_media_details(
|
||||
jellyseerr: JellyseerrClient, request_type: RequestType, tmdb_id: int
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
media_type = request_type.value
|
||||
if media_type not in {"movie", "tv"}:
|
||||
return None
|
||||
if is_seerr_media_failure_suppressed(media_type, tmdb_id):
|
||||
logger.debug("Seerr snapshot hydration suppressed: media_type=%s tmdb_id=%s", media_type, tmdb_id)
|
||||
return None
|
||||
try:
|
||||
if request_type == RequestType.movie:
|
||||
details = await jellyseerr.get_movie(int(tmdb_id))
|
||||
else:
|
||||
details = await jellyseerr.get_tv(int(tmdb_id))
|
||||
except httpx.HTTPStatusError as exc:
|
||||
if _should_persist_seerr_media_failure(exc):
|
||||
record_seerr_media_failure(
|
||||
media_type,
|
||||
int(tmdb_id),
|
||||
status_code=exc.response.status_code if exc.response is not None else None,
|
||||
error_message=_extract_http_error_message(exc),
|
||||
)
|
||||
return None
|
||||
if isinstance(details, dict):
|
||||
clear_seerr_media_failure(media_type, int(tmdb_id))
|
||||
return details
|
||||
return None
|
||||
|
||||
|
||||
async def _maybe_refresh_jellyfin(snapshot: Snapshot) -> None:
|
||||
if snapshot.state not in {NormalizedState.available, NormalizedState.completed}:
|
||||
return
|
||||
@@ -300,33 +357,22 @@ async def build_snapshot(request_id: str) -> Snapshot:
|
||||
if snapshot.title in {None, "", "Unknown"} and allow_remote:
|
||||
tmdb_id = jelly_request.get("media", {}).get("tmdbId")
|
||||
if tmdb_id:
|
||||
try:
|
||||
details = await _get_seerr_media_details(jellyseerr, snapshot.request_type, int(tmdb_id))
|
||||
if isinstance(details, dict):
|
||||
if snapshot.request_type == RequestType.movie:
|
||||
details = await jellyseerr.get_movie(int(tmdb_id))
|
||||
if isinstance(details, dict):
|
||||
snapshot.title = details.get("title") or snapshot.title
|
||||
release_date = details.get("releaseDate")
|
||||
snapshot.year = int(release_date[:4]) if release_date else snapshot.year
|
||||
poster_path = poster_path or details.get("posterPath") or details.get("poster_path")
|
||||
backdrop_path = (
|
||||
backdrop_path
|
||||
or details.get("backdropPath")
|
||||
or details.get("backdrop_path")
|
||||
)
|
||||
snapshot.title = details.get("title") or snapshot.title
|
||||
release_date = details.get("releaseDate")
|
||||
snapshot.year = int(release_date[:4]) if release_date else snapshot.year
|
||||
elif snapshot.request_type == RequestType.tv:
|
||||
details = await jellyseerr.get_tv(int(tmdb_id))
|
||||
if isinstance(details, dict):
|
||||
snapshot.title = details.get("name") or details.get("title") or snapshot.title
|
||||
first_air = details.get("firstAirDate")
|
||||
snapshot.year = int(first_air[:4]) if first_air else snapshot.year
|
||||
poster_path = poster_path or details.get("posterPath") or details.get("poster_path")
|
||||
backdrop_path = (
|
||||
backdrop_path
|
||||
or details.get("backdropPath")
|
||||
or details.get("backdrop_path")
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
snapshot.title = details.get("name") or details.get("title") or snapshot.title
|
||||
first_air = details.get("firstAirDate")
|
||||
snapshot.year = int(first_air[:4]) if first_air else snapshot.year
|
||||
poster_path = poster_path or details.get("posterPath") or details.get("poster_path")
|
||||
backdrop_path = (
|
||||
backdrop_path
|
||||
or details.get("backdropPath")
|
||||
or details.get("backdrop_path")
|
||||
)
|
||||
|
||||
cache_mode = (runtime.artwork_cache_mode or "remote").lower()
|
||||
snapshot.artwork = {
|
||||
|
||||
5
frontend/package-lock.json
generated
5
frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "magent-frontend",
|
||||
"version": "0103262231",
|
||||
"version": "0103262251",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "magent-frontend",
|
||||
"version": "0103262231",
|
||||
"version": "0103262251",
|
||||
"dependencies": {
|
||||
"next": "16.1.6",
|
||||
"react": "19.2.4",
|
||||
@@ -977,3 +977,4 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "magent-frontend",
|
||||
"private": true,
|
||||
"version": "0103262231",
|
||||
"version": "0103262251",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
@@ -23,3 +23,4 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user