Files
KiraTV/backend/core/jellyfin_client.py
T
joaquin f5d6745ed0 Initial commit: KiraTV IPTV proxy server
Full-stack IPTV management system with Xtream Codes API compatibility,
multi-account provider fan-out, Jellyfin local library integration,
real-time dashboard, EPG support, and A/V-sync-safe stream recovery.
2026-05-17 23:22:05 +02:00

121 lines
4.3 KiB
Python

"""Async Jellyfin API client — discovery, libraries, items, streaming."""
import logging
import aiohttp
logger = logging.getLogger(__name__)
_SHORT = aiohttp.ClientTimeout(total=10)
_MEDIUM = aiohttp.ClientTimeout(total=30)
_LONG = aiohttp.ClientTimeout(total=60)
class JellyfinClient:
def __init__(self, url: str, api_key: str):
self.url = url.rstrip("/")
self.api_key = api_key
@property
def _headers(self) -> dict:
return {"X-Emby-Token": self.api_key, "Accept": "application/json"}
# ------------------------------------------------------------------
# Discovery
# ------------------------------------------------------------------
async def test_connection(self) -> dict:
async with aiohttp.ClientSession() as s:
async with s.get(
f"{self.url}/System/Info/Public", timeout=_SHORT
) as r:
r.raise_for_status()
return await r.json()
async def get_libraries(self) -> list[dict]:
async with aiohttp.ClientSession() as s:
async with s.get(
f"{self.url}/Library/VirtualFolders",
headers=self._headers,
timeout=_SHORT,
) as r:
r.raise_for_status()
data = await r.json()
return data if isinstance(data, list) else []
# ------------------------------------------------------------------
# Items (movies / series)
# ------------------------------------------------------------------
async def get_items(self, library_id: str, item_type: str) -> list[dict]:
"""Fetch all items of a given type from one library (paginated)."""
all_items: list[dict] = []
start = 0
limit = 200
while True:
params = {
"ParentId": library_id,
"IncludeItemTypes": item_type,
"Recursive": "true",
"Fields": "Overview,Genres,OfficialRating,ProductionYear",
"Limit": str(limit),
"StartIndex": str(start),
}
async with aiohttp.ClientSession() as s:
async with s.get(
f"{self.url}/Items",
params=params,
headers=self._headers,
timeout=_LONG,
) as r:
r.raise_for_status()
data = await r.json()
batch = data.get("Items", [])
all_items.extend(batch)
total = data.get("TotalRecordCount", 0)
if len(all_items) >= total or not batch:
break
start += len(batch)
return all_items
# ------------------------------------------------------------------
# Series structure (seasons + episodes)
# ------------------------------------------------------------------
async def get_seasons(self, series_id: str) -> list[dict]:
async with aiohttp.ClientSession() as s:
async with s.get(
f"{self.url}/Shows/{series_id}/Seasons",
params={"Fields": "Overview"},
headers=self._headers,
timeout=_MEDIUM,
) as r:
r.raise_for_status()
return (await r.json()).get("Items", [])
async def get_episodes(self, series_id: str, season_id: str) -> list[dict]:
async with aiohttp.ClientSession() as s:
async with s.get(
f"{self.url}/Shows/{series_id}/Episodes",
params={"SeasonId": season_id, "Fields": "Overview"},
headers=self._headers,
timeout=_MEDIUM,
) as r:
r.raise_for_status()
return (await r.json()).get("Items", [])
# ------------------------------------------------------------------
# URL builders
# ------------------------------------------------------------------
def build_stream_url(self, item_id: str) -> str:
return (
f"{self.url}/Videos/{item_id}/stream"
f"?static=true&api_key={self.api_key}&Container=mkv"
)
def build_image_url(self, item_id: str, max_width: int = 400) -> str:
return (
f"{self.url}/Items/{item_id}/Images/Primary"
f"?api_key={self.api_key}&MaxWidth={max_width}"
)