"""Jellyfin API client — uses Activity Log + PlaybackInfo.""" import os import re from datetime import datetime, timezone, timedelta import requests def _cfg(): return { "host": os.environ["JELLYFIN_HOST"].rstrip("/"), "key": os.environ["JELLYFIN_API_KEY"], "user": os.environ["JELLYFIN_USER_ID"], } def get_activity_logs(min_date: datetime) -> list: """Return activity log entries since min_date (UTC datetime).""" c = _cfg() url = f"{c['host']}/System/ActivityLog/Entries" params = { "api_key": c["key"], "hasUserId": "true", "limit": 200, "minDate": min_date.strftime("%Y-%m-%dT%H:%M:%S.000000Z"), } resp = requests.get(url, params=params, timeout=15) resp.raise_for_status() return resp.json().get("Items", []) def get_playback_info(item_id: str) -> dict: """Return the first MediaSource dict for the given item.""" c = _cfg() url = f"{c['host']}/Items/{item_id}/PlaybackInfo" params = {"api_key": c["key"]} resp = requests.get(url, params=params, timeout=10) resp.raise_for_status() sources = resp.json().get("MediaSources", []) if not sources: raise RuntimeError(f"No MediaSources for item {item_id}") return sources[0] def extract_episode_number(name: str) -> int: """Parse the absolute episode number from a name like 'One Piece - S21E48 (939) - Title'.""" match = re.search(r'\((\d+)\)', name) if not match: raise RuntimeError(f"Could not extract episode number from: {name!r}") return int(match.group(1)) def find_last_one_piece_before(cutoff_utc: datetime) -> dict: """Return info about the last One Piece episode watched before cutoff_utc. Returns: {episode_number, item_id, watched_at (ISO str), media_name} """ c = _cfg() min_date = cutoff_utc - timedelta(days=14) entries = get_activity_logs(min_date) for entry in entries: if entry.get("Type") != "VideoPlaybackStopped": continue if entry.get("UserId") != c["user"]: continue if "One Piece" not in entry.get("Name", ""): continue date_str = entry.get("Date", "") try: entry_dt = datetime.fromisoformat(date_str.replace("Z", "+00:00")) except ValueError: continue if entry_dt >= cutoff_utc: continue item_id = entry.get("ItemId") if not item_id: continue source = get_playback_info(item_id) ep_num = extract_episode_number(source["Name"]) return { "episode_number": ep_num, "item_id": item_id, "watched_at": date_str, "media_name": source["Name"], } raise RuntimeError("No One Piece episode found before the Sunday cutoff in the last 14 days")