Fix shared request access and Jellyfin-ready pipeline status
This commit is contained in:
@@ -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(
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user