Phase 2: Docker and OIDC auth
This commit is contained in:
@@ -7,10 +7,13 @@ future FastAPI/React frontend can reuse the same client.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
import requests
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Jellyfin validates Fields against its ItemFields enum. Keep this list to
|
||||
# documented/commonly supported optional fields; invalid names cause 400s.
|
||||
@@ -54,9 +57,10 @@ class JellyfinClient:
|
||||
}
|
||||
)
|
||||
|
||||
def get(self, path: str, **params: Any) -> dict[str, Any]:
|
||||
def get(self, path: str, **params: Any) -> Any:
|
||||
"""GET a Jellyfin 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("Jellyfin GET %s params=%s", path, sorted(clean_params.keys()))
|
||||
response = self.session.get(
|
||||
f"{self.base_url}{path}", params=clean_params, timeout=self.timeout
|
||||
)
|
||||
@@ -64,10 +68,12 @@ class JellyfinClient:
|
||||
response.raise_for_status()
|
||||
except requests.HTTPError as exc:
|
||||
detail = response.text[:500]
|
||||
logger.warning("Jellyfin 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("Jellyfin GET %s ok status=%s", path, response.status_code)
|
||||
return response.json()
|
||||
|
||||
def users(self) -> list[dict[str, Any]]:
|
||||
@@ -77,11 +83,15 @@ class JellyfinClient:
|
||||
/Users/Me often fails with API-key auth. The user id selected here is
|
||||
then used for user-scoped library endpoints.
|
||||
"""
|
||||
return self.get("/Users")
|
||||
users = self.get("/Users")
|
||||
logger.info("Jellyfin returned %s visible users", len(users))
|
||||
return users
|
||||
|
||||
def libraries(self, user_id: str) -> list[dict[str, Any]]:
|
||||
"""Return top-level library views visible to the selected Jellyfin user."""
|
||||
return self.get(f"/Users/{user_id}/Views").get("Items", [])
|
||||
items = self.get(f"/Users/{user_id}/Views").get("Items", [])
|
||||
logger.info("Jellyfin returned %s libraries for user_id=%s", len(items), user_id)
|
||||
return items
|
||||
|
||||
def items(
|
||||
self,
|
||||
@@ -101,6 +111,17 @@ class JellyfinClient:
|
||||
builder. Keep arguments close to Jellyfin's own query parameters so the
|
||||
service layer can request server-side pagination and basic sorting.
|
||||
"""
|
||||
logger.debug(
|
||||
"Jellyfin items user_id=%s parent_id=%s start=%s limit=%s types=%s search=%s sort=%s/%s",
|
||||
user_id,
|
||||
parent_id or "<root>",
|
||||
start_index,
|
||||
limit,
|
||||
include_item_types or "<all>",
|
||||
search or "<none>",
|
||||
sort_by,
|
||||
sort_order,
|
||||
)
|
||||
return self.get(
|
||||
f"/Users/{user_id}/Items",
|
||||
ParentId=parent_id,
|
||||
@@ -123,7 +144,15 @@ class JellyfinClient:
|
||||
IncludeItemTypes=include_item_types,
|
||||
Limit=0,
|
||||
)
|
||||
return int(response.get("TotalRecordCount", 0))
|
||||
count = int(response.get("TotalRecordCount", 0))
|
||||
logger.debug(
|
||||
"Jellyfin item count user_id=%s parent_id=%s types=%s count=%s",
|
||||
user_id,
|
||||
parent_id or "<root>",
|
||||
include_item_types,
|
||||
count,
|
||||
)
|
||||
return count
|
||||
|
||||
def media_counts(self, user_id: str) -> dict[str, int]:
|
||||
"""Return dashboard-level counts for the main media types."""
|
||||
@@ -156,11 +185,20 @@ class JellyfinClient:
|
||||
})
|
||||
return results
|
||||
|
||||
def sessions(self, active_within_seconds: int | None = None) -> list[dict[str, Any]]:
|
||||
"""Return Jellyfin sessions (playing and idle/logged-in).
|
||||
|
||||
When ``active_within_seconds`` is None, no recency filter is sent and
|
||||
Jellyfin decides which sessions to include.
|
||||
"""
|
||||
payload: Any = self.get("/Sessions", ActiveWithinSeconds=active_within_seconds)
|
||||
return cast(list[dict[str, Any]], payload) if isinstance(payload, list) else []
|
||||
|
||||
def active_sessions(self, active_within_seconds: int = 300) -> list[dict[str, Any]]:
|
||||
"""Return currently active sessions that have a now-playing item."""
|
||||
payload = self.get("/Sessions", ActiveWithinSeconds=active_within_seconds)
|
||||
sessions = payload if isinstance(payload, list) else []
|
||||
return [session for session in sessions if session.get("NowPlayingItem")]
|
||||
"""Return sessions that currently have a now-playing item."""
|
||||
sessions = [session for session in self.sessions(active_within_seconds) if session.get("NowPlayingItem")]
|
||||
logger.info("Jellyfin active sessions within %ss: %s", active_within_seconds, len(sessions))
|
||||
return sessions
|
||||
|
||||
def image_url(self, item_id: str, image_type: str = "Primary") -> str:
|
||||
"""Build an authenticated image URL suitable for st.image/browser use."""
|
||||
|
||||
Reference in New Issue
Block a user