import httpx from app.config import settings _headers = { "X-MediaBrowser-Token": settings.jellyfin_api_key, } _client: httpx.AsyncClient | None = None def _get_client() -> httpx.AsyncClient: global _client if _client is None or _client.is_closed: _client = httpx.AsyncClient( base_url=settings.jellyfin_url, headers=_headers, timeout=30.0, ) return _client async def authenticate_user(username: str, password: str) -> dict | None: """Authenticate a user via Jellyfin. Returns user info + token or None.""" client = _get_client() auth_header = ( 'MediaBrowser Client="Movie Night", Device="Web", DeviceId="movie-night-app", Version="1.0.0"' ) try: res = await client.post( "/Users/AuthenticateByName", json={"Username": username, "Pw": password}, headers={"X-Emby-Authorization": auth_header}, ) if res.status_code == 200: data = res.json() return { "user_id": data["User"]["Id"], "username": data["User"]["Name"], "token": data["AccessToken"], } except httpx.HTTPError: pass return None async def get_users() -> list[dict]: """Get all Jellyfin users.""" client = _get_client() try: res = await client.get("/Users") if res.status_code == 200: return [{"id": u["Id"], "name": u["Name"]} for u in res.json()] except httpx.HTTPError: pass return [] async def get_all_movies(user_id: str) -> list[dict]: """Fetch all movies from Jellyfin with full metadata, paginated.""" client = _get_client() movies = [] start_index = 0 page_size = 200 fields = "Genres,Overview,CommunityRating,CriticRating,OfficialRating,Studios,People,Tags,RunTimeTicks" while True: try: res = await client.get( f"/Users/{user_id}/Items", params={ "IncludeItemTypes": "Movie", "Recursive": "true", "Fields": fields, "Limit": page_size, "StartIndex": start_index, "SortBy": "SortName", "SortOrder": "Ascending", }, ) if res.status_code != 200: break data = res.json() items = data.get("Items", []) if not items: break for item in items: runtime_ticks = item.get("RunTimeTicks") runtime_minutes = int(runtime_ticks / 600_000_000) if runtime_ticks else None people = [] for person in (item.get("People") or [])[:5]: people.append({ "name": person.get("Name", ""), "role": person.get("Role", person.get("Type", "")), }) movies.append({ "jellyfin_id": item["Id"], "title": item.get("Name", "Unknown"), "sort_title": item.get("SortName"), "year": item.get("ProductionYear"), "genres": item.get("Genres", []), "overview": item.get("Overview"), "community_rating": item.get("CommunityRating"), "critic_rating": item.get("CriticRating"), "runtime_minutes": runtime_minutes, "content_rating": item.get("OfficialRating"), "studios": [s.get("Name", "") for s in (item.get("Studios") or [])], "people": people, "tags": item.get("Tags", []), "is_played": item.get("UserData", {}).get("Played", False), }) total = data.get("TotalRecordCount", 0) start_index += page_size if start_index >= total: break except httpx.HTTPError: break return movies async def get_played_movie_ids(user_id: str) -> set[str]: """Get set of movie IDs that have been played by a user.""" client = _get_client() played_ids = set() start_index = 0 page_size = 200 while True: try: res = await client.get( f"/Users/{user_id}/Items", params={ "IncludeItemTypes": "Movie", "Recursive": "true", "IsPlayed": "true", "Limit": page_size, "StartIndex": start_index, "Fields": "", }, ) if res.status_code != 200: break data = res.json() items = data.get("Items", []) if not items: break for item in items: played_ids.add(item["Id"]) total = data.get("TotalRecordCount", 0) start_index += page_size if start_index >= total: break except httpx.HTTPError: break return played_ids async def get_poster(item_id: str) -> bytes | None: """Fetch poster image bytes for a movie.""" client = _get_client() try: res = await client.get( f"/Items/{item_id}/Images/Primary", params={"maxWidth": 400, "quality": 80}, ) if res.status_code == 200: return res.content except httpx.HTTPError: pass return None def get_poster_url(item_id: str) -> str: """Get the proxied poster URL.""" return f"/api/poster/{item_id}" def get_deep_link(item_id: str) -> str: """Get a direct link to the movie in Jellyfin.""" return f"{settings.jellyfin_external_url}/web/index.html#!/details?id={item_id}"