"""Minimal Jellyfin API client. Only three things are needed: check the server is alive, create the Shows library with internet metadata providers switched off, and trigger a refresh after we change the tree. """ from __future__ import annotations import json import logging import urllib.error import urllib.parse import urllib.request from pathlib import Path from . import config log = logging.getLogger(__name__) LIBRARY_NAME = "YouTube" COLLECTION_TYPE = "tvshows" # Metadata is supplied entirely by our own NFO sidecars, so every fetcher is # disabled for all three item types a Shows library resolves. _ITEM_TYPES = ("Series", "Season", "Episode") class JellyfinError(RuntimeError): pass class Jellyfin: def __init__(self, base_url: str, api_key: str = "", timeout: float = 30.0): self.base_url = (base_url or "").rstrip("/") self.api_key = api_key or "" self.timeout = timeout @property def configured(self) -> bool: return bool(self.base_url and self.api_key) def _request( self, method: str, path: str, params: dict | None = None, body: dict | None = None, ): url = self.base_url + path if params: url += "?" + urllib.parse.urlencode(params) data = None headers = {"User-Agent": config.USER_AGENT, "Accept": "application/json"} if self.api_key: headers["X-Emby-Token"] = self.api_key if body is not None: data = json.dumps(body).encode("utf-8") headers["Content-Type"] = "application/json" request = urllib.request.Request(url, data=data, headers=headers, method=method) try: with urllib.request.urlopen(request, timeout=self.timeout) as response: payload = response.read() except urllib.error.HTTPError as exc: raise JellyfinError(f"{method} {path} -> HTTP {exc.code}") from exc except OSError as exc: raise JellyfinError(f"{method} {path} -> {exc}") from exc if not payload: return None try: return json.loads(payload) except json.JSONDecodeError: return None def public_info(self) -> dict: """Unauthenticated liveness check.""" return self._request("GET", "/System/Info/Public") or {} def virtual_folders(self) -> list[dict]: return self._request("GET", "/Library/VirtualFolders") or [] def find_library(self, path: Path | str) -> dict | None: target = str(path).rstrip("/") for folder in self.virtual_folders(): for location in folder.get("Locations") or []: if str(location).rstrip("/") == target: return folder return None def create_library(self, path: Path | str, name: str = LIBRARY_NAME) -> None: """Create the Shows library with all internet providers disabled.""" options = { "EnableInternetProviders": False, "SaveLocalMetadata": True, "EnableRealtimeMonitor": False, "EnableChapterImageExtraction": False, "PathInfos": [{"Path": str(path)}], "TypeOptions": [ { "Type": item_type, "MetadataFetchers": [], "MetadataFetcherOrder": [], "ImageFetchers": [], "ImageFetcherOrder": [], } for item_type in _ITEM_TYPES ], } self._request( "POST", "/Library/VirtualFolders", params={ "name": name, "collectionType": COLLECTION_TYPE, "paths": str(path), "refreshLibrary": "false", }, body={"LibraryOptions": options}, ) def refresh(self) -> None: """Trigger a library scan. Best effort — never fatal to the caller.""" try: self._request("POST", "/Library/Refresh") except JellyfinError as exc: log.warning("jellyfin refresh failed: %s", exc) def from_settings(settings) -> Jellyfin: return Jellyfin( settings.get_str("jellyfin_url"), settings.get_str("jellyfin_api_key") )