major fix
This commit is contained in:
1 parent
929088cef0
commit
9e799049a7
10 files changed
+339
-281
No files matched your search
+80
-56
@@ -1,68 +1,92 @@
|
||||
"""Jellyfin API client."""
|
||||
"""Jellyfin API client — uses Activity Log + PlaybackInfo."""
|
||||
import os
|
||||
from datetime import date, datetime, timezone
|
||||
import re
|
||||
from datetime import datetime, timezone, timedelta
|
||||
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,
|
||||
def _cfg():
|
||||
return {
|
||||
"host": os.environ["JELLYFIN_HOST"].rstrip("/"),
|
||||
"key": os.environ["JELLYFIN_API_KEY"],
|
||||
"user": os.environ["JELLYFIN_USER_ID"],
|
||||
}
|
||||
resp = requests.get(url, headers=HEADERS, params=params, timeout=10)
|
||||
|
||||
|
||||
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()
|
||||
items = resp.json().get("Items", [])
|
||||
if not items:
|
||||
raise RuntimeError("One Piece series not found in Jellyfin")
|
||||
return items[0]["Id"]
|
||||
return resp.json().get("Items", [])
|
||||
|
||||
|
||||
def get_last_watched_episode(up_to_date: date) -> dict:
|
||||
"""Return the last watched One Piece episode up to (and including) up_to_date.
|
||||
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]
|
||||
|
||||
Returns a dict with keys: season, episode, name, jellyfin_id, watched_at
|
||||
|
||||
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}
|
||||
"""
|
||||
series_id = _get_one_piece_series_id()
|
||||
c = _cfg()
|
||||
min_date = cutoff_utc - timedelta(days=14)
|
||||
entries = get_activity_logs(min_date)
|
||||
|
||||
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:
|
||||
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
|
||||
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}")
|
||||
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")
|
||||
|
||||
Reference in new issue
Block a user