Phase 2: Docker and OIDC auth
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
"""Jellyseerr HTTP API client.
|
||||
|
||||
Jellyseerr is optional. When configured, it can enrich the Jellyfin user list
|
||||
with email addresses, avatars, permissions, and request metadata.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class JellyseerrClient:
|
||||
"""Small wrapper around the Jellyseerr REST API."""
|
||||
|
||||
def __init__(self, base_url: str, api_key: str, timeout: int = 30):
|
||||
if not base_url:
|
||||
raise ValueError("Jellyseerr URL is required")
|
||||
if not api_key:
|
||||
raise ValueError("Jellyseerr API key is required")
|
||||
|
||||
self.base_url = base_url.rstrip("/")
|
||||
if self.base_url.endswith("/api/v1"):
|
||||
self.base_url = self.base_url[:-7]
|
||||
self.api_key = api_key
|
||||
self.timeout = timeout
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update(
|
||||
{
|
||||
"X-Api-Key": api_key,
|
||||
"Accept": "application/json",
|
||||
}
|
||||
)
|
||||
|
||||
def get(self, path: str, **params: Any) -> Any:
|
||||
"""GET a Jellyseerr endpoint and include useful response text on errors."""
|
||||
clean_params = {k: v for k, v in params.items() if v is not None and v != ""}
|
||||
logger.debug("Jellyseerr GET %s params=%s", path, sorted(clean_params.keys()))
|
||||
response = self.session.get(
|
||||
f"{self.base_url}/api/v1{path}", params=clean_params, timeout=self.timeout
|
||||
)
|
||||
try:
|
||||
response.raise_for_status()
|
||||
except requests.HTTPError as exc:
|
||||
detail = response.text[:500]
|
||||
logger.warning(
|
||||
"Jellyseerr GET %s failed status=%s url=%s", path, response.status_code, response.url
|
||||
)
|
||||
raise requests.HTTPError(
|
||||
f"{response.status_code} for {response.url}: {detail}",
|
||||
response=response,
|
||||
) from exc
|
||||
logger.debug("Jellyseerr GET %s ok status=%s", path, response.status_code)
|
||||
return response.json()
|
||||
|
||||
def absolute_url(self, path: str | None) -> str:
|
||||
"""Return an absolute URL for Jellyseerr-relative assets."""
|
||||
if not path:
|
||||
return ""
|
||||
if path.startswith("http://") or path.startswith("https://"):
|
||||
return path
|
||||
if not path.startswith("/"):
|
||||
path = f"/{path}"
|
||||
return f"{self.base_url}{path}"
|
||||
|
||||
def jellyfin_users(self) -> list[dict[str, Any]]:
|
||||
"""Return Jellyfin-linked users known to Jellyseerr.
|
||||
|
||||
Jellyseerr has used both a top-level list payload and a wrapped
|
||||
`{ "users": [...] }` payload in different versions/docs, so accept
|
||||
either shape.
|
||||
"""
|
||||
payload = self.get("/settings/jellyfin/users")
|
||||
if isinstance(payload, list):
|
||||
users = [item for item in payload if isinstance(item, dict)]
|
||||
logger.info("Jellyseerr returned %s Jellyfin-linked users", len(users))
|
||||
return users
|
||||
if isinstance(payload, dict):
|
||||
users = payload.get("users")
|
||||
if isinstance(users, list):
|
||||
mapped = [item for item in users if isinstance(item, dict)]
|
||||
logger.info("Jellyseerr returned %s Jellyfin-linked users (wrapped payload)", len(mapped))
|
||||
return mapped
|
||||
logger.info("Jellyseerr returned no Jellyfin-linked users")
|
||||
return []
|
||||
|
||||
def users(self, page_size: int = 1000) -> list[dict[str, Any]]:
|
||||
"""Return Jellyseerr users via the paginated /user list endpoint.
|
||||
|
||||
Jellyseerr's list endpoint uses ``take`` and ``skip`` query params,
|
||||
not ``page``.
|
||||
"""
|
||||
results: list[dict[str, Any]] = []
|
||||
take = max(1, int(page_size))
|
||||
skip = 0
|
||||
total_results: int | None = None
|
||||
|
||||
while True:
|
||||
payload = self.get("/user", take=take, skip=skip)
|
||||
if not isinstance(payload, dict):
|
||||
return results
|
||||
|
||||
page_results = payload.get("results") or []
|
||||
page_items = [item for item in page_results if isinstance(item, dict)] if isinstance(page_results, list) else []
|
||||
results.extend(page_items)
|
||||
|
||||
page_info = payload.get("pageInfo") or {}
|
||||
if isinstance(page_info, dict):
|
||||
try:
|
||||
page_total = int(page_info.get("results") or 0)
|
||||
if page_total:
|
||||
total_results = page_total
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
|
||||
logger.debug(
|
||||
"Jellyseerr user page skip=%s take=%s -> %s results (total=%s)",
|
||||
skip,
|
||||
take,
|
||||
len(page_items),
|
||||
total_results if total_results is not None else "unknown",
|
||||
)
|
||||
|
||||
if not page_items:
|
||||
break
|
||||
skip += len(page_items)
|
||||
if len(page_items) < take:
|
||||
break
|
||||
if total_results is not None and skip >= total_results:
|
||||
break
|
||||
|
||||
logger.info("Jellyseerr returned %s users", len(results))
|
||||
return results
|
||||
Reference in New Issue
Block a user