Initial work

This commit is contained in:
Pat Teruel committed 2026-02-22 15:15:07 +08:00
commit 929088cef0
13 files changed
+613

No files matched your search

View File
Whitespace-only changes.
+68
View File
@@ -0,0 +1,68 @@
"""Jellyfin API client."""
import os
from datetime import date, datetime, timezone
import requests
JELLYFIN_HOST = os.environ["JELLYFIN_HOST"].rstrip("/")
JELLYFIN_API_KEY = os.environ["JELLYFIN_API_KEY"]
JELLYFIN_USER_ID = os.environ["JELLYFIN_USER_ID"]
HEADERS = {"X-Emby-Token": JELLYFIN_API_KEY}
def _get_one_piece_series_id():
"""Return the Jellyfin item ID for One Piece."""
url = f"{JELLYFIN_HOST}/Users/{JELLYFIN_USER_ID}/Items"
params = {
"searchTerm": "One Piece",
"IncludeItemTypes": "Series",
"Recursive": "true",
"Limit": 5,
}
resp = requests.get(url, headers=HEADERS, params=params, timeout=10)
resp.raise_for_status()
items = resp.json().get("Items", [])
if not items:
raise RuntimeError("One Piece series not found in Jellyfin")
return items[0]["Id"]
def get_last_watched_episode(up_to_date: date) -> dict:
"""Return the last watched One Piece episode up to (and including) up_to_date.
Returns a dict with keys: season, episode, name, jellyfin_id, watched_at
"""
series_id = _get_one_piece_series_id()
url = f"{JELLYFIN_HOST}/Users/{JELLYFIN_USER_ID}/Items"
params = {
"ParentId": series_id,
"IncludeItemTypes": "Episode",
"Recursive": "true",
"Filters": "IsPlayed",
"SortBy": "DatePlayed",
"SortOrder": "Descending",
"Fields": "UserData",
"Limit": 200,
}
resp = requests.get(url, headers=HEADERS, params=params, timeout=15)
resp.raise_for_status()
items = resp.json().get("Items", [])
cutoff = datetime(up_to_date.year, up_to_date.month, up_to_date.day, 23, 59, 59, tzinfo=timezone.utc)
for item in items:
played_str = item.get("UserData", {}).get("LastPlayedDate")
if not played_str:
continue
played_dt = datetime.fromisoformat(played_str.replace("Z", "+00:00"))
if played_dt <= cutoff:
return {
"season": item["ParentIndexNumber"],
"episode": item["IndexNumber"],
"name": item.get("Name", ""),
"jellyfin_id": item["Id"],
"watched_at": played_str,
}
raise RuntimeError(f"No watched episodes found up to {up_to_date}")
+42
View File
@@ -0,0 +1,42 @@
"""Sonarr API client."""
import os
import requests
SONARR_HOST = os.environ["SONARR_HOST"].rstrip("/")
SONARR_API_KEY = os.environ["SONARR_API_KEY"]
SONARR_SERIES_ID = os.environ["SONARR_SERIES_ID"]
HEADERS = {"X-Api-Key": SONARR_API_KEY}
def _get_episodes():
"""Fetch all episodes for the configured One Piece series."""
url = f"{SONARR_HOST}/api/v3/episode"
params = {"seriesId": SONARR_SERIES_ID}
resp = requests.get(url, headers=HEADERS, params=params, timeout=30)
resp.raise_for_status()
return resp.json()
def get_absolute_episode(season: int, episode: int) -> int:
"""Return the absolute (series-wide) episode number for the given season/episode."""
episodes = _get_episodes()
for ep in episodes:
if ep.get("seasonNumber") == season and ep.get("episodeNumber") == episode:
abs_num = ep.get("absoluteEpisodeNumber")
if abs_num is None:
raise RuntimeError(
f"Sonarr has no absolute episode number for S{season:02d}E{episode:02d}"
)
return abs_num
raise RuntimeError(f"Episode S{season:02d}E{episode:02d} not found in Sonarr")
def get_total_episodes() -> int:
"""Return total number of episodes in the series according to Sonarr."""
episodes = _get_episodes()
# Exclude specials (season 0)
regular = [ep for ep in episodes if ep.get("seasonNumber", 0) != 0 and ep.get("hasFile")]
if not regular:
regular = [ep for ep in episodes if ep.get("seasonNumber", 0) != 0]
return len(regular)