Files
media-episode-viewer/clients/jellyfin.py
T
2026-02-22 15:39:50 +08:00

93 lines
2.8 KiB
Python

"""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")