fix(jellyseer): resolve request titles via /movie|tv endpoints
The requests table showed all names as "—" because Jellyseerr's /api/v1/request
list does NOT embed titles — they live on the Movie/Series records. Added
JellyseerrClient._resolve_title(media_type, tmdb_id) that fetches
/api/v1/movie/{tmdbId} (→ title) or /api/v1/tv/{tmdbId} (→ name), cached on
the client instance so subsequent polls are instant.
Also scoped the table fetch to open requests only (pending + approved) via
Jellyseerr's filter param, instead of fetching all 800+ historical requests.
open_requests() fetches pending+approved (paginated), resolves their titles
(small set → fast), and returns them sorted by date added desc.
Updated the frontend table's status filter to Open/Pending/Approved (the data
only contains open requests now).
Tests: title resolution end-to-end (movie tmdbId → title), caching across
polls, filter param used. 404/404 backend + 184/184 frontend + build green.
This commit is contained in:
@@ -56,6 +56,7 @@ class JellyseerrClient:
|
||||
"Accept": "application/json",
|
||||
}
|
||||
)
|
||||
self._title_cache: dict[tuple[str, str], str] = {}
|
||||
|
||||
def get(self, path: str, **params: Any) -> Any:
|
||||
"""GET a Jellyseerr endpoint and include useful response text on errors."""
|
||||
@@ -84,6 +85,26 @@ class JellyseerrClient:
|
||||
path = f"/{path}"
|
||||
return f"{self.base_url}{path}"
|
||||
|
||||
def _resolve_title(self, media_type: Any, tmdb_id: Any) -> str:
|
||||
"""Resolve a media title via /movie/{tmdbId} or /tv/{tmdbId}, cached.
|
||||
|
||||
Jellyseerr's /request list doesn't include titles; they live on the
|
||||
Movie/Series records. Cached per (type, tmdbId) so repeated polls reuse.
|
||||
"""
|
||||
if not tmdb_id:
|
||||
return ""
|
||||
key = (str(media_type or ""), str(tmdb_id))
|
||||
if key in self._title_cache:
|
||||
return self._title_cache[key]
|
||||
try:
|
||||
is_tv = str(media_type) in ("2", "tv")
|
||||
data = self.get(f"/{'tv' if is_tv else 'movie'}/{tmdb_id}")
|
||||
title = str(data.get("name" if is_tv else "title") or "")
|
||||
except Exception:
|
||||
title = ""
|
||||
self._title_cache[key] = title
|
||||
return title
|
||||
|
||||
def jellyfin_users(self) -> list[dict[str, Any]]:
|
||||
"""Return Jellyfin-linked users known to Jellyseerr.
|
||||
|
||||
@@ -175,7 +196,7 @@ class JellyseerrClient:
|
||||
return counts
|
||||
|
||||
def recent_requests(self, take: int = 20) -> list[dict[str, Any]]:
|
||||
"""Return the most recently modified requests, lightly mapped."""
|
||||
"""Return the most recently modified requests with resolved titles."""
|
||||
take = max(1, min(int(take), 100))
|
||||
payload = self.get("/request", sort="modified", skip=0, take=take)
|
||||
if not isinstance(payload, dict):
|
||||
@@ -185,11 +206,15 @@ class JellyseerrClient:
|
||||
mapped: list[dict[str, Any]] = []
|
||||
for r in items:
|
||||
media = r.get("media") or {}
|
||||
tmdb_id = media.get("tmdbId")
|
||||
name = r.get("title") or media.get("title") or media.get("name") or ""
|
||||
if not name and tmdb_id:
|
||||
name = self._resolve_title(r.get("type"), tmdb_id)
|
||||
mapped.append(
|
||||
{
|
||||
"id": r.get("id"),
|
||||
"type": r.get("type"),
|
||||
"name": r.get("title") or media.get("title") or media.get("name") or "—",
|
||||
"type": _label(r.get("type"), _REQUEST_TYPE),
|
||||
"name": name or "—",
|
||||
"status": _label(r.get("status"), _REQUEST_STATUS),
|
||||
"media_status": _label((media or {}).get("status"), _MEDIA_STATUS),
|
||||
"created_at": r.get("createdAt"),
|
||||
@@ -197,40 +222,43 @@ class JellyseerrClient:
|
||||
)
|
||||
return mapped
|
||||
|
||||
def requests(self, max_count: int = 500) -> list[dict[str, Any]]:
|
||||
"""Return requests (paginated), mapped for the requests table.
|
||||
def open_requests(self, max_per_filter: int = 100) -> list[dict[str, Any]]:
|
||||
"""Return open (pending + approved) requests with resolved titles.
|
||||
|
||||
Fetches up to ``max_count`` requests (no status filter, so the table
|
||||
can filter/sort client-side). The table defaults to showing "open"
|
||||
(pending/approved/processing) sorted by date added (newest first).
|
||||
Fetches pending and approved requests via Jellyseerr's filter param
|
||||
(not all 800+ historical requests), then resolves titles from
|
||||
/movie/{tmdbId} or /tv/{tmdbId}. Titles are cached on the client so
|
||||
subsequent polls are instant.
|
||||
"""
|
||||
max_count = max(1, min(int(max_count), 1000))
|
||||
results: list[dict[str, Any]] = []
|
||||
take = 100
|
||||
skip = 0
|
||||
while skip < max_count:
|
||||
payload = self.get("/request", sort="added", skip=skip, take=take)
|
||||
if not isinstance(payload, dict):
|
||||
break
|
||||
page = payload.get("results") or []
|
||||
items = [r for r in page if isinstance(r, dict)] if isinstance(page, list) else []
|
||||
for r in items:
|
||||
media = r.get("media") or {}
|
||||
results.append(
|
||||
{
|
||||
"id": r.get("id"),
|
||||
"type": _label(r.get("type"), _REQUEST_TYPE),
|
||||
"name": r.get("title") or (media or {}).get("title") or (media or {}).get("name") or "—",
|
||||
"status": _label(r.get("status"), _REQUEST_STATUS),
|
||||
"media_status": _label((media or {}).get("status"), _MEDIA_STATUS),
|
||||
"created_at": r.get("createdAt"),
|
||||
}
|
||||
)
|
||||
if len(items) < take:
|
||||
break
|
||||
skip += len(items)
|
||||
if len(results) >= max_count:
|
||||
results = results[:max_count]
|
||||
break
|
||||
logger.info("Jellyseerr returned %s requests", len(results))
|
||||
take = 50
|
||||
for filter_val in ("pending", "approved"):
|
||||
skip = 0
|
||||
while skip < max_per_filter:
|
||||
payload = self.get("/request", filter=filter_val, sort="added", skip=skip, take=take)
|
||||
if not isinstance(payload, dict):
|
||||
break
|
||||
page = payload.get("results") or []
|
||||
items = [r for r in page if isinstance(r, dict)] if isinstance(page, list) else []
|
||||
for r in items:
|
||||
media = r.get("media") or {}
|
||||
tmdb_id = media.get("tmdbId")
|
||||
name = r.get("title") or media.get("title") or media.get("name") or ""
|
||||
if not name and tmdb_id:
|
||||
name = self._resolve_title(r.get("type"), tmdb_id)
|
||||
results.append(
|
||||
{
|
||||
"id": r.get("id"),
|
||||
"type": _label(r.get("type"), _REQUEST_TYPE),
|
||||
"name": name or "—",
|
||||
"status": _label(r.get("status"), _REQUEST_STATUS),
|
||||
"media_status": _label((media or {}).get("status"), _MEDIA_STATUS),
|
||||
"created_at": r.get("createdAt"),
|
||||
}
|
||||
)
|
||||
if len(items) < take:
|
||||
break
|
||||
skip += len(items)
|
||||
results.sort(key=lambda r: r.get("created_at") or 0, reverse=True)
|
||||
logger.info("Jellyseerr returned %s open requests (with titles)", len(results))
|
||||
return results
|
||||
|
||||
Reference in New Issue
Block a user