Fix shared request access and Jellyfin-ready pipeline status
This commit is contained in:
@@ -1 +1 @@
|
||||
0303261507
|
||||
0303261601
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
BUILD_NUMBER = "0303261507"
|
||||
CHANGELOG = '2026-03-03|Improve SQLite batching and diagnostics visibility\n2026-03-03|Add login page visibility controls\n2026-03-03|Hotfix: expand landing-page search to all requests\n2026-03-02|Hotfix: add logged-out password reset flow\n2026-03-02|Process 1 build 0203261953\n2026-03-02|Process 1 build 0203261610\n2026-03-02|Process 1 build 0203261608\n2026-03-02|Add dedicated profile invites page and fix mobile admin layout\n2026-03-01|Persist Seerr media failure suppression and reduce sync error noise\n2026-03-01|Add repository line ending policy\n2026-03-01|Finalize diagnostics, logging controls, and email test support\n2026-03-01|Add invite email templates and delivery workflow\n2026-02-28|Finalize dev-1.3 upgrades and Seerr updates\n2026-02-27|admin docs and layout refresh, build 2702261314\n2026-02-27|Build 2702261153: fix jellyfin sync user visibility\n2026-02-26|Build 2602262241: live request page updates\n2026-02-26|Build 2602262204\n2026-02-26|Build 2602262159: restore jellyfin-first user source\n2026-02-26|Build 2602262049: split magent settings and harden local login\n2026-02-26|Build 2602262030: add magent settings and hardening\n2026-02-26|Build 2602261731: fix user resync after nuclear wipe\n2026-02-26|Build 2602261717: master invite policy and self-service invite controls\n2026-02-26|Build 2602261636: self-service invites and count fixes\n2026-02-26|Build 2602261605: invite trace and cross-system user lifecycle\n2026-02-26|Build 2602261536: refine invite layouts and tighten UI\n2026-02-26|Build 2602261523: live updates, invite cleanup and nuclear resync\n2026-02-26|Build 2602261442: tidy users and invite layouts\n2026-02-26|Build 2602261409: unify invite management controls\n2026-02-26|Build 2602260214: invites profiles and expiry admin controls\n2026-02-26|Build 2602260022: enterprise UI refresh and users bulk auto-search\n2026-02-25|Build 2502262321: fix auto-search quality and per-user toggle\n2026-02-02|Build 0202261541: allow FQDN service URLs\n2026-01-30|Build 3001262148: single container\n2026-01-29|Build 2901262244: format changelog\n2026-01-29|Build 2901262240: cache users\n2026-01-29|Tidy full changelog\n2026-01-29|Update full changelog\n2026-01-29|Bake build number and changelog\n2026-01-29|Hardcode build number in backend\n2026-01-29|release: 2901262102\n2026-01-29|release: 2901262044\n2026-01-29|release: 2901262036\n2026-01-27|Hydrate missing artwork from Jellyseerr (build 271261539)\n2026-01-27|Fallback to TMDB when artwork cache fails (build 271261524)\n2026-01-27|Add service test buttons (build 271261335)\n2026-01-27|Bump build number (process 2) 271261322\n2026-01-27|Add cache load spinner (build 271261238)\n2026-01-27|Fix snapshot title fallback (build 271261228)\n2026-01-27|Fix request titles in snapshots (build 271261219)\n2026-01-27|Bump build number to 271261202\n2026-01-27|Clarify request sync settings (build 271261159)\n2026-01-27|Fix backend cache stats import (build 271261149)\n2026-01-27|Improve cache stats performance (build 271261145)\n2026-01-27|Add cache control artwork stats\n2026-01-26|Fix sync progress bar animation\n2026-01-26|Fix cache title hydration\n2026-01-25|Build 2501262041\n2026-01-25|Harden request cache titles and cache-only reads\n2026-01-25|Serve bundled branding assets by default\n2026-01-25|Seed branding logo from bundled assets\n2026-01-25|Tidy request sync controls\n2026-01-25|Add Jellyfin login cache and admin-only stats\n2026-01-25|Add user stats and activity tracking\n2026-01-25|Move account actions into avatar menu\n2026-01-25|Improve mobile header layout\n2026-01-25|Automate build number tagging and sync\n2026-01-25|Add site banner, build number, and changelog\n2026-01-24|Improve request handling and qBittorrent categories\n2026-01-24|Map Prowlarr releases to Arr indexers for manual grab\n2026-01-24|Clarify how-it-works steps and fixes\n2026-01-24|Document fix buttons in how-it-works\n2026-01-24|Route grabs through Sonarr/Radarr only\n2026-01-23|Use backend branding assets for logo and favicon\n2026-01-23|Copy public assets into frontend image\n2026-01-23|Fix backend Dockerfile paths for root context\n2026-01-23|Add Docker Hub compose override\n2026-01-23|Remove password fields from users page\n2026-01-23|Use bundled branding assets\n2026-01-23|Add default branding assets when missing\n2026-01-23|Show available status on landing when in Jellyfin\n2026-01-23|Fix cache titles and move feedback link\n2026-01-23|Add feedback form and webhook\n2026-01-23|Hide header actions when signed out\n2026-01-23|Fallback manual grab to qBittorrent\n2026-01-23|Split search actions and improve download options\n2026-01-23|Fix cache titles via Jellyseerr media lookup\n2026-01-22|Update README with Docker-first guide\n2026-01-22|Update README\n2026-01-22|Ignore build artifacts\n2026-01-22|Initial commit'
|
||||
BUILD_NUMBER = "0303261601"
|
||||
CHANGELOG = '2026-03-03|Process 1 build 0303261507\n2026-03-03|Improve SQLite batching and diagnostics visibility\n2026-03-03|Add login page visibility controls\n2026-03-03|Hotfix: expand landing-page search to all requests\n2026-03-02|Hotfix: add logged-out password reset flow\n2026-03-02|Process 1 build 0203261953\n2026-03-02|Process 1 build 0203261610\n2026-03-02|Process 1 build 0203261608\n2026-03-02|Add dedicated profile invites page and fix mobile admin layout\n2026-03-01|Persist Seerr media failure suppression and reduce sync error noise\n2026-03-01|Add repository line ending policy\n2026-03-01|Finalize diagnostics, logging controls, and email test support\n2026-03-01|Add invite email templates and delivery workflow\n2026-02-28|Finalize dev-1.3 upgrades and Seerr updates\n2026-02-27|admin docs and layout refresh, build 2702261314\n2026-02-27|Build 2702261153: fix jellyfin sync user visibility\n2026-02-26|Build 2602262241: live request page updates\n2026-02-26|Build 2602262204\n2026-02-26|Build 2602262159: restore jellyfin-first user source\n2026-02-26|Build 2602262049: split magent settings and harden local login\n2026-02-26|Build 2602262030: add magent settings and hardening\n2026-02-26|Build 2602261731: fix user resync after nuclear wipe\n2026-02-26|Build 2602261717: master invite policy and self-service invite controls\n2026-02-26|Build 2602261636: self-service invites and count fixes\n2026-02-26|Build 2602261605: invite trace and cross-system user lifecycle\n2026-02-26|Build 2602261536: refine invite layouts and tighten UI\n2026-02-26|Build 2602261523: live updates, invite cleanup and nuclear resync\n2026-02-26|Build 2602261442: tidy users and invite layouts\n2026-02-26|Build 2602261409: unify invite management controls\n2026-02-26|Build 2602260214: invites profiles and expiry admin controls\n2026-02-26|Build 2602260022: enterprise UI refresh and users bulk auto-search\n2026-02-25|Build 2502262321: fix auto-search quality and per-user toggle\n2026-02-02|Build 0202261541: allow FQDN service URLs\n2026-01-30|Build 3001262148: single container\n2026-01-29|Build 2901262244: format changelog\n2026-01-29|Build 2901262240: cache users\n2026-01-29|Tidy full changelog\n2026-01-29|Update full changelog\n2026-01-29|Bake build number and changelog\n2026-01-29|Hardcode build number in backend\n2026-01-29|release: 2901262102\n2026-01-29|release: 2901262044\n2026-01-29|release: 2901262036\n2026-01-27|Hydrate missing artwork from Jellyseerr (build 271261539)\n2026-01-27|Fallback to TMDB when artwork cache fails (build 271261524)\n2026-01-27|Add service test buttons (build 271261335)\n2026-01-27|Bump build number (process 2) 271261322\n2026-01-27|Add cache load spinner (build 271261238)\n2026-01-27|Fix snapshot title fallback (build 271261228)\n2026-01-27|Fix request titles in snapshots (build 271261219)\n2026-01-27|Bump build number to 271261202\n2026-01-27|Clarify request sync settings (build 271261159)\n2026-01-27|Fix backend cache stats import (build 271261149)\n2026-01-27|Improve cache stats performance (build 271261145)\n2026-01-27|Add cache control artwork stats\n2026-01-26|Fix sync progress bar animation\n2026-01-26|Fix cache title hydration\n2026-01-25|Build 2501262041\n2026-01-25|Harden request cache titles and cache-only reads\n2026-01-25|Serve bundled branding assets by default\n2026-01-25|Seed branding logo from bundled assets\n2026-01-25|Tidy request sync controls\n2026-01-25|Add Jellyfin login cache and admin-only stats\n2026-01-25|Add user stats and activity tracking\n2026-01-25|Move account actions into avatar menu\n2026-01-25|Improve mobile header layout\n2026-01-25|Automate build number tagging and sync\n2026-01-25|Add site banner, build number, and changelog\n2026-01-24|Improve request handling and qBittorrent categories\n2026-01-24|Map Prowlarr releases to Arr indexers for manual grab\n2026-01-24|Clarify how-it-works steps and fixes\n2026-01-24|Document fix buttons in how-it-works\n2026-01-24|Route grabs through Sonarr/Radarr only\n2026-01-23|Use backend branding assets for logo and favicon\n2026-01-23|Copy public assets into frontend image\n2026-01-23|Fix backend Dockerfile paths for root context\n2026-01-23|Add Docker Hub compose override\n2026-01-23|Remove password fields from users page\n2026-01-23|Use bundled branding assets\n2026-01-23|Add default branding assets when missing\n2026-01-23|Show available status on landing when in Jellyfin\n2026-01-23|Fix cache titles and move feedback link\n2026-01-23|Add feedback form and webhook\n2026-01-23|Hide header actions when signed out\n2026-01-23|Fallback manual grab to qBittorrent\n2026-01-23|Split search actions and improve download options\n2026-01-23|Fix cache titles via Jellyseerr media lookup\n2026-01-22|Update README with Docker-first guide\n2026-01-22|Update README\n2026-01-22|Ignore build artifacts\n2026-01-22|Initial commit'
|
||||
|
||||
@@ -25,7 +25,8 @@ SQLITE_MMAP_SIZE_BYTES = 256 * 1024 * 1024
|
||||
def _db_path() -> str:
|
||||
path = settings.sqlite_path or "data/magent.db"
|
||||
if not os.path.isabs(path):
|
||||
path = os.path.join(os.getcwd(), path)
|
||||
app_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
path = os.path.join(app_root, path)
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
return path
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ from ..db import (
|
||||
clear_seerr_media_failure,
|
||||
)
|
||||
from ..models import Snapshot, TriageResult, RequestType
|
||||
from ..services.snapshot import build_snapshot
|
||||
from ..services.snapshot import build_snapshot, jellyfin_item_matches_request
|
||||
|
||||
router = APIRouter(prefix="/requests", tags=["requests"], dependencies=[Depends(get_current_user)])
|
||||
|
||||
@@ -118,6 +118,57 @@ 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)
|
||||
|
||||
|
||||
def _status_label_with_jellyfin(current_status: Any, jellyfin_available: bool) -> str:
|
||||
if not jellyfin_available:
|
||||
return _status_label(current_status)
|
||||
try:
|
||||
status_code = int(current_status)
|
||||
except (TypeError, ValueError):
|
||||
status_code = None
|
||||
if status_code == 6:
|
||||
return STATUS_LABELS[6]
|
||||
return STATUS_LABELS[4]
|
||||
|
||||
|
||||
async def _request_is_available_in_jellyfin(
|
||||
jellyfin: JellyfinClient,
|
||||
title: Optional[str],
|
||||
year: Optional[int],
|
||||
media_type: Optional[str],
|
||||
request_payload: Optional[Dict[str, Any]],
|
||||
availability_cache: Dict[str, bool],
|
||||
) -> bool:
|
||||
if not jellyfin.configured() or not title:
|
||||
return False
|
||||
cache_key = f"{media_type or ''}:{title.lower()}:{year or ''}:{request_payload.get('id') if isinstance(request_payload, dict) else ''}"
|
||||
cached_value = availability_cache.get(cache_key)
|
||||
if cached_value is not None:
|
||||
return cached_value
|
||||
types = ["Movie"] if media_type == "movie" else ["Series"]
|
||||
try:
|
||||
search = await jellyfin.search_items(title, types, limit=50)
|
||||
except Exception:
|
||||
availability_cache[cache_key] = False
|
||||
return False
|
||||
if isinstance(search, dict):
|
||||
items = search.get("Items") or search.get("items") or []
|
||||
request_type = RequestType.movie if media_type == "movie" else RequestType.tv
|
||||
for item in items:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
if jellyfin_item_matches_request(
|
||||
item,
|
||||
title=title,
|
||||
year=year,
|
||||
request_type=request_type,
|
||||
request_payload=request_payload,
|
||||
):
|
||||
availability_cache[cache_key] = True
|
||||
return True
|
||||
availability_cache[cache_key] = False
|
||||
return False
|
||||
_failed_detail_cache.pop(key, None)
|
||||
|
||||
|
||||
@@ -1295,23 +1346,9 @@ def get_requests_sync_state() -> Dict[str, Any]:
|
||||
async def _ensure_request_access(
|
||||
client: JellyseerrClient, request_id: int, user: Dict[str, str]
|
||||
) -> None:
|
||||
if user.get("role") == "admin":
|
||||
if user.get("role") == "admin" or user.get("username"):
|
||||
return
|
||||
runtime = get_runtime_settings()
|
||||
mode = (runtime.requests_data_source or "prefer_cache").lower()
|
||||
cached = get_request_cache_payload(request_id)
|
||||
if mode != "always_js":
|
||||
if cached is None:
|
||||
logger.debug("access cache miss: request_id=%s mode=%s", request_id, mode)
|
||||
raise HTTPException(status_code=404, detail="Request not found in cache")
|
||||
logger.debug("access cache hit: request_id=%s mode=%s", request_id, mode)
|
||||
if _request_matches_user(cached, user.get("username", "")):
|
||||
return
|
||||
raise HTTPException(status_code=403, detail="Request not accessible for this user")
|
||||
logger.debug("access cache miss: request_id=%s mode=%s", request_id, mode)
|
||||
details = await _get_request_details(client, request_id)
|
||||
if details is None or not _request_matches_user(details, user.get("username", "")):
|
||||
raise HTTPException(status_code=403, detail="Request not accessible for this user")
|
||||
raise HTTPException(status_code=403, detail="Request not accessible for this user")
|
||||
|
||||
|
||||
def _build_recent_map(response: Dict[str, Any]) -> Dict[int, Dict[str, Any]]:
|
||||
@@ -1619,36 +1656,6 @@ async def recent_requests(
|
||||
allow_artwork_hydrate = client.configured()
|
||||
jellyfin = JellyfinClient(runtime.jellyfin_base_url, runtime.jellyfin_api_key)
|
||||
jellyfin_cache: Dict[str, bool] = {}
|
||||
|
||||
async def _jellyfin_available(
|
||||
title_value: Optional[str], year_value: Optional[int], media_type_value: Optional[str]
|
||||
) -> bool:
|
||||
if not jellyfin.configured() or not title_value:
|
||||
return False
|
||||
cache_key = f"{media_type_value or ''}:{title_value.lower()}:{year_value or ''}"
|
||||
cached_value = jellyfin_cache.get(cache_key)
|
||||
if cached_value is not None:
|
||||
return cached_value
|
||||
types = ["Movie"] if media_type_value == "movie" else ["Series"]
|
||||
try:
|
||||
search = await jellyfin.search_items(title_value, types)
|
||||
except Exception:
|
||||
jellyfin_cache[cache_key] = False
|
||||
return False
|
||||
if isinstance(search, dict):
|
||||
items = search.get("Items") or search.get("items") or []
|
||||
for item in items:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
name = item.get("Name") or item.get("title")
|
||||
year = item.get("ProductionYear") or item.get("Year")
|
||||
if name and name.strip().lower() == title_value.strip().lower():
|
||||
if year_value and year and int(year) != int(year_value):
|
||||
continue
|
||||
jellyfin_cache[cache_key] = True
|
||||
return True
|
||||
jellyfin_cache[cache_key] = False
|
||||
return False
|
||||
results = []
|
||||
for row in rows:
|
||||
status = row.get("status")
|
||||
@@ -1743,10 +1750,16 @@ async def recent_requests(
|
||||
payload_json=json.dumps(details, ensure_ascii=True),
|
||||
)
|
||||
status_label = _status_label(status)
|
||||
if status_label == "Working on it":
|
||||
is_available = await _jellyfin_available(title, year, row.get("media_type"))
|
||||
if is_available:
|
||||
status_label = "Available"
|
||||
if status_label in {"Working on it", "Ready to watch", "Partially ready"}:
|
||||
is_available = await _request_is_available_in_jellyfin(
|
||||
jellyfin,
|
||||
title,
|
||||
year,
|
||||
row.get("media_type"),
|
||||
details if isinstance(details, dict) else None,
|
||||
jellyfin_cache,
|
||||
)
|
||||
status_label = _status_label_with_jellyfin(status, is_available)
|
||||
results.append(
|
||||
{
|
||||
"id": row.get("request_id"),
|
||||
@@ -1790,6 +1803,8 @@ async def search_requests(
|
||||
pass
|
||||
|
||||
results = []
|
||||
jellyfin = JellyfinClient(runtime.jellyfin_base_url, runtime.jellyfin_api_key)
|
||||
jellyfin_cache: Dict[str, bool] = {}
|
||||
for item in response.get("results", []):
|
||||
media_type = item.get("mediaType")
|
||||
title = item.get("title") or item.get("name")
|
||||
@@ -1824,11 +1839,19 @@ async def search_requests(
|
||||
details = get_request_cache_payload(request_id)
|
||||
if not isinstance(details, dict):
|
||||
details = await _get_request_details(client, request_id)
|
||||
requested_by = _request_display_name(details)
|
||||
if user.get("role") == "admin":
|
||||
accessible = True
|
||||
elif isinstance(details, dict):
|
||||
accessible = _request_matches_user(details, user.get("username", ""))
|
||||
requested_by = _request_display_name(details)
|
||||
accessible = True
|
||||
if status is not None:
|
||||
is_available = await _request_is_available_in_jellyfin(
|
||||
jellyfin,
|
||||
title,
|
||||
year,
|
||||
media_type,
|
||||
details if isinstance(details, dict) else None,
|
||||
jellyfin_cache,
|
||||
)
|
||||
status_label = _status_label_with_jellyfin(status, is_available)
|
||||
|
||||
results.append(
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from typing import Any, Dict, List, Optional
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
from datetime import datetime, timezone
|
||||
from urllib.parse import quote
|
||||
import httpx
|
||||
@@ -57,6 +58,100 @@ def _pick_first(value: Any) -> Optional[Dict[str, Any]]:
|
||||
return None
|
||||
|
||||
|
||||
def _normalize_media_title(value: Any) -> Optional[str]:
|
||||
if not isinstance(value, str):
|
||||
return None
|
||||
normalized = re.sub(r"[^a-z0-9]+", " ", value.lower()).strip()
|
||||
return normalized or None
|
||||
|
||||
|
||||
def _canonical_provider_key(value: str) -> str:
|
||||
normalized = value.strip().lower()
|
||||
if normalized.endswith("id"):
|
||||
normalized = normalized[:-2]
|
||||
return normalized
|
||||
|
||||
|
||||
def extract_request_provider_ids(payload: Any) -> Dict[str, str]:
|
||||
provider_ids: Dict[str, str] = {}
|
||||
candidates: List[Any] = []
|
||||
if isinstance(payload, dict):
|
||||
candidates.append(payload)
|
||||
media = payload.get("media")
|
||||
if isinstance(media, dict):
|
||||
candidates.append(media)
|
||||
for candidate in candidates:
|
||||
if not isinstance(candidate, dict):
|
||||
continue
|
||||
embedded = candidate.get("ProviderIds") or candidate.get("providerIds")
|
||||
if isinstance(embedded, dict):
|
||||
for key, value in embedded.items():
|
||||
if value is None:
|
||||
continue
|
||||
text = str(value).strip()
|
||||
if text:
|
||||
provider_ids[_canonical_provider_key(str(key))] = text
|
||||
for key in ("tmdbId", "tvdbId", "imdbId", "tmdb_id", "tvdb_id", "imdb_id"):
|
||||
value = candidate.get(key)
|
||||
if value is None:
|
||||
continue
|
||||
text = str(value).strip()
|
||||
if text:
|
||||
provider_ids[_canonical_provider_key(key)] = text
|
||||
return provider_ids
|
||||
|
||||
|
||||
def jellyfin_item_matches_request(
|
||||
item: Dict[str, Any],
|
||||
*,
|
||||
title: Optional[str],
|
||||
year: Optional[int],
|
||||
request_type: RequestType,
|
||||
request_payload: Optional[Dict[str, Any]] = None,
|
||||
) -> bool:
|
||||
request_provider_ids = extract_request_provider_ids(request_payload or {})
|
||||
item_provider_ids = extract_request_provider_ids(item)
|
||||
|
||||
provider_priority = ("tmdb", "tvdb", "imdb")
|
||||
for key in provider_priority:
|
||||
request_id = request_provider_ids.get(key)
|
||||
item_id = item_provider_ids.get(key)
|
||||
if request_id and item_id and request_id == item_id:
|
||||
return True
|
||||
|
||||
request_title = _normalize_media_title(title)
|
||||
if not request_title:
|
||||
return False
|
||||
|
||||
item_titles = [
|
||||
_normalize_media_title(item.get("Name")),
|
||||
_normalize_media_title(item.get("OriginalTitle")),
|
||||
_normalize_media_title(item.get("SortName")),
|
||||
_normalize_media_title(item.get("SeriesName")),
|
||||
_normalize_media_title(item.get("title")),
|
||||
]
|
||||
item_titles = [candidate for candidate in item_titles if candidate]
|
||||
|
||||
item_year = item.get("ProductionYear") or item.get("Year")
|
||||
try:
|
||||
item_year_value = int(item_year) if item_year is not None else None
|
||||
except (TypeError, ValueError):
|
||||
item_year_value = None
|
||||
|
||||
if year and item_year_value and int(year) != item_year_value:
|
||||
return False
|
||||
|
||||
if request_title in item_titles:
|
||||
return True
|
||||
|
||||
if request_type == RequestType.tv:
|
||||
for candidate in item_titles:
|
||||
if candidate and (candidate.startswith(request_title) or request_title.startswith(candidate)):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _extract_http_error_message(exc: httpx.HTTPStatusError) -> Optional[str]:
|
||||
response = exc.response
|
||||
if response is None:
|
||||
@@ -513,7 +608,7 @@ async def build_snapshot(request_id: str) -> Snapshot:
|
||||
if jellyfin.configured() and snapshot.title:
|
||||
types = ["Movie"] if snapshot.request_type == RequestType.movie else ["Series"]
|
||||
try:
|
||||
search = await jellyfin.search_items(snapshot.title, types)
|
||||
search = await jellyfin.search_items(snapshot.title, types, limit=50)
|
||||
except Exception:
|
||||
search = None
|
||||
if isinstance(search, dict):
|
||||
@@ -521,11 +616,13 @@ async def build_snapshot(request_id: str) -> Snapshot:
|
||||
for item in items:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
name = item.get("Name") or item.get("title")
|
||||
year = item.get("ProductionYear") or item.get("Year")
|
||||
if name and name.strip().lower() == (snapshot.title or "").strip().lower():
|
||||
if snapshot.year and year and int(year) != int(snapshot.year):
|
||||
continue
|
||||
if jellyfin_item_matches_request(
|
||||
item,
|
||||
title=snapshot.title,
|
||||
year=snapshot.year,
|
||||
request_type=snapshot.request_type,
|
||||
request_payload=jelly_request,
|
||||
):
|
||||
jellyfin_available = True
|
||||
jellyfin_item = item
|
||||
break
|
||||
@@ -646,12 +743,22 @@ async def build_snapshot(request_id: str) -> Snapshot:
|
||||
snapshot.state = NormalizedState.added_to_arr
|
||||
snapshot.state_reason = "Item is present in Sonarr/Radarr"
|
||||
|
||||
if jellyfin_available and snapshot.state not in {
|
||||
NormalizedState.downloading,
|
||||
NormalizedState.importing,
|
||||
}:
|
||||
snapshot.state = NormalizedState.completed
|
||||
snapshot.state_reason = "Ready to watch in Jellyfin."
|
||||
if jellyfin_available:
|
||||
missing_episodes = arr_details.get("missingEpisodes")
|
||||
if snapshot.request_type == RequestType.tv and isinstance(missing_episodes, dict) and missing_episodes:
|
||||
snapshot.state = NormalizedState.importing
|
||||
snapshot.state_reason = "Some episodes are available in Jellyfin, but the request is still incomplete."
|
||||
for hop in timeline:
|
||||
if hop.service == "Seerr":
|
||||
hop.status = "Partially ready"
|
||||
else:
|
||||
snapshot.state = NormalizedState.completed
|
||||
snapshot.state_reason = "Ready to watch in Jellyfin."
|
||||
for hop in timeline:
|
||||
if hop.service == "Seerr":
|
||||
hop.status = "Available"
|
||||
elif hop.service == "Sonarr/Radarr" and hop.status not in {"error"}:
|
||||
hop.status = "available"
|
||||
|
||||
snapshot.timeline = timeline
|
||||
actions: List[ActionOption] = []
|
||||
|
||||
@@ -567,21 +567,17 @@ export default function HomePage() {
|
||||
<button
|
||||
key={`${item.title || 'Untitled'}-${index}`}
|
||||
type="button"
|
||||
disabled={!item.requestId || !item.accessible}
|
||||
disabled={!item.requestId}
|
||||
onClick={() =>
|
||||
item.requestId && item.accessible && router.push(`/requests/${item.requestId}`)
|
||||
item.requestId && router.push(`/requests/${item.requestId}`)
|
||||
}
|
||||
>
|
||||
{item.title || 'Untitled'} {item.year ? `(${item.year})` : ''}{' '}
|
||||
{!item.requestId
|
||||
? '- not requested'
|
||||
: !item.accessible
|
||||
? `- ${item.statusLabel || 'Requested'} · requested by ${
|
||||
item.requestedBy || 'another user'
|
||||
}`
|
||||
: item.statusLabel
|
||||
? `- ${item.statusLabel}`
|
||||
: ''}
|
||||
: item.statusLabel
|
||||
? `- ${item.statusLabel}`
|
||||
: '- already requested'}
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
|
||||
@@ -368,7 +368,14 @@ export default function RequestTimelinePage() {
|
||||
const jellyfinLink = snapshot.raw?.jellyfin?.link
|
||||
const posterUrl = snapshot.artwork?.poster_url
|
||||
const resolvedPoster =
|
||||
posterUrl && posterUrl.startsWith('http') ? posterUrl : posterUrl ? `${getApiBase()}${posterUrl}` : null
|
||||
posterUrl && posterUrl.startsWith('http') ? posterUrl : posterUrl ? `${getApiBase()}${posterUrl}` : null
|
||||
const hasPartialReadyTimeline = snapshot.timeline.some(
|
||||
(hop) => hop.service === 'Seerr' && hop.status === 'Partially ready'
|
||||
)
|
||||
const currentStatusText =
|
||||
snapshot.state === 'IMPORTING' && hasPartialReadyTimeline
|
||||
? 'Partially ready'
|
||||
: friendlyState(snapshot.state)
|
||||
|
||||
return (
|
||||
<main className="card">
|
||||
@@ -400,7 +407,7 @@ export default function RequestTimelinePage() {
|
||||
<section className="status-box">
|
||||
<div>
|
||||
<h2>Status</h2>
|
||||
<p className="status-text">{friendlyState(snapshot.state)}</p>
|
||||
<p className="status-text">{currentStatusText}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h2>What this means</h2>
|
||||
|
||||
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "magent-frontend",
|
||||
"version": "0303261507",
|
||||
"version": "0303261601",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "magent-frontend",
|
||||
"version": "0303261507",
|
||||
"version": "0303261601",
|
||||
"dependencies": {
|
||||
"next": "16.1.6",
|
||||
"react": "19.2.4",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "magent-frontend",
|
||||
"private": true,
|
||||
"version": "0303261507",
|
||||
"version": "0303261601",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
|
||||
Reference in New Issue
Block a user