From 1f216a8e8dc25cd071be54f9f54346e6c49671fb Mon Sep 17 00:00:00 2001 From: Pat Teruel Date: Sun, 22 Feb 2026 17:45:11 +0800 Subject: [PATCH] Finalize fixes --- app.py | 20 ++++++-- clients/jellyfin.py | 118 +++++++++++++++++++++++++++++++++++++++----- 2 files changed, 122 insertions(+), 16 deletions(-) diff --git a/app.py b/app.py index 1535d36..973c8ac 100644 --- a/app.py +++ b/app.py @@ -113,21 +113,33 @@ def index(): def report(): week_start, week_end, _ = current_week() # This is the week range for display (e.g., Feb 15-21) total_episodes = int(os.environ.get("TOTAL_EPISODES", 1155)) - latest = get_latest_sunday() # This is the cutoff for searching (e.g., Feb 21 16:00 UTC) - previous = get_previous_sunday() + latest = get_latest_sunday() # e.g., 2026-02-21T16:00:00.000Z + previous = get_previous_sunday() # e.g., 2026-02-14T16:00:00.000Z jellyfin.clear_debug_logs() - # Convert latest_sunday string back to datetime for cutoff + # Convert to datetime for cutoff latest_sunday_dt = datetime.fromisoformat(latest.replace("Z", "+00:00")) + previous_sunday_dt = datetime.fromisoformat(previous.replace("Z", "+00:00")) user_id = os.environ["JELLYFIN_USER_ID"] + # Fetch latest episode (watched before latest Sunday) try: ep_info = jellyfin.find_last_one_piece_episode(latest_sunday_dt, user_id) except Exception as e: debug_output = jellyfin.get_debug_logs() return jsonify({"error": str(e), "debug": debug_output}), 502 + # Fetch previous week episode (watched before previous Sunday) + try: + prev_ep_info = jellyfin.find_last_one_piece_before_previous_sunday(previous_sunday_dt, user_id) + previous_episode = prev_ep_info["episode_number"] + except Exception as e: + _debug.add(f"✗ Could not find previous Sunday episode: {e}") + _debug.add("✓ Falling back to DB seed or latest episode") + prev = db.get_previous_episode(week_start.isoformat()) + previous_episode = prev["episode_number"] if prev else ep_info["episode_number"] + db.save_episode( episode_number=ep_info["episode_number"], week_start=week_start.isoformat(), @@ -137,8 +149,6 @@ def report(): watched_at=ep_info.get("watched_at"), ) - prev = db.get_previous_episode(week_start.isoformat()) - previous_episode = prev["episode_number"] if prev else ep_info["episode_number"] watched_this_week = ep_info["episode_number"] - previous_episode date_range = format_date_range(week_start, week_end) diff --git a/clients/jellyfin.py b/clients/jellyfin.py index fd3882c..3ad1131 100644 --- a/clients/jellyfin.py +++ b/clients/jellyfin.py @@ -38,16 +38,22 @@ def clear_debug_logs(): _debug.logs = [] -def get_activity_logs(min_date: datetime) -> list: - """Return activity log entries since min_date (UTC datetime).""" +def get_activity_logs(min_date: datetime = None, limit: int = 200) -> list: + """Return activity log entries. Optionally filtered by min_date. + + Args: + min_date: Optional minimum date filter (UTC datetime) + limit: Number of entries to fetch (default 200) + """ 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"), + "limit": str(limit), } + if min_date: + params["minDate"] = min_date.strftime("%Y-%m-%dT%H:%M:%S.000000Z") curl_cmd = f"curl --location '{url}?" curl_params = [] @@ -57,7 +63,7 @@ def get_activity_logs(min_date: datetime) -> list: curl_cmd += "' \\\n --header 'accept: application/json'" _debug.add(f"GET {url}") - _debug.add(f" params: hasUserId=true, limit=200, minDate={params['minDate']}") + _debug.add(f" params: hasUserId=true, limit={limit}" + (f", minDate={params['minDate']}" if min_date else "")) _debug.add("") _debug.add("Curl command:") _debug.add(curl_cmd) @@ -74,6 +80,7 @@ def get_activity_logs(min_date: datetime) -> list: raise + def get_playback_info(item_id: str) -> dict: """Return the first MediaSource dict for the given item.""" c = _cfg() @@ -103,13 +110,104 @@ def extract_episode_number(name: str) -> int: return int(match.group(1)) +def _find_last_one_piece_in_entries(entries: list, cutoff_utc: datetime, user_id: str) -> dict: + """Extract the last One Piece episode from a list of entries, watched before cutoff_utc. + + Returns {episode_number, item_id, watched_at, media_name} or raises RuntimeError if not found. + """ + candidates = [] + for entry in entries: + if entry.get("Type") != "VideoPlaybackStopped": + continue + if entry.get("UserId") != user_id: + continue + if "has finished playing One Piece" not in entry.get("Name", ""): + continue + + date_str = entry.get("Date", "") + try: + entry_dt = datetime.fromisoformat(date_str.replace("Z", "+00:00")) + if entry_dt < cutoff_utc: + candidates.append((entry_dt, entry)) + except ValueError: + continue + + if not candidates: + raise RuntimeError("No matching One Piece episodes found") + + # Sort by date descending (newest first) + candidates.sort(key=lambda x: x[0], reverse=True) + latest_dt, latest_entry = candidates[0] + + name = latest_entry.get("Name", "") + date_str = latest_entry.get("Date", "") + item_id = latest_entry.get("ItemId") + + if not item_id: + raise RuntimeError("Entry has no ItemId") + + 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"], + "name": name, + } + + +def find_last_one_piece_before_previous_sunday(previous_sunday_utc: datetime, user_id: str) -> dict: + """Find the latest One Piece episode watched BEFORE the previous Sunday. + + Uses exponential backoff: starts with limit=500, doubles until found or limit >= 32000. + Returns: {episode_number, item_id, watched_at, media_name} + """ + _debug.add(f"Searching for episode watched before previous Sunday: {previous_sunday_utc.isoformat()}") + _debug.add(f"Using exponential backoff: 500 → 1000 → 2000 → 4000 → 8000 → 16000 → 32000") + _debug.add("") + + limit = 500 + max_limit = 32000 + attempt = 0 + + while limit <= max_limit: + attempt += 1 + _debug.add(f"Attempt {attempt}: Fetching {limit} activity log entries...") + + try: + entries = get_activity_logs(limit=limit) + except Exception as e: + _debug.add(f"✗ Failed to fetch: {e}") + raise + + _debug.add(f"✓ Got {len(entries)} entries, searching...") + _debug.add("") + + try: + result = _find_last_one_piece_in_entries(entries, previous_sunday_utc, user_id) + _debug.add(f"✓ Found: {result['name']}") + _debug.add(f" Watched at: {result['watched_at']}") + _debug.add(f" Extracted episode: {result['episode_number']}") + _debug.add("") + return result + except RuntimeError as e: + _debug.add(f"✗ {e} (trying next limit...)") + _debug.add("") + limit *= 2 + + _debug.add(f"✗ Exceeded max limit ({max_limit}) without finding episode") + raise RuntimeError(f"Could not find previous Sunday episode after searching up to {max_limit} entries") + + def find_last_one_piece_episode(cutoff_utc: datetime, user_id: str) -> dict: """Find the last One Piece episode watched BEFORE cutoff_utc by the given user. Filters: 1. Entry date must be strictly before cutoff_utc 2. UserId must match - 3. Name must start with '{user} has finished playing One Piece' + 3. Name must contain 'has finished playing One Piece' 4. Type must be VideoPlaybackStopped Returns the most recent matching entry. @@ -123,7 +221,7 @@ def find_last_one_piece_episode(cutoff_utc: datetime, user_id: str) -> dict: _debug.add("") try: - entries = get_activity_logs(min_date) + entries = get_activity_logs(limit=200, min_date=min_date) except Exception as e: _debug.add(f"✗ Failed to fetch activity logs: {e}") raise @@ -137,10 +235,8 @@ def find_last_one_piece_episode(cutoff_utc: datetime, user_id: str) -> dict: continue if entry.get("UserId") != user_id: continue - if not entry.get("Name", "").startswith(f"{user_id.split('@')[0] if '@' in user_id else 'pat'} has finished playing One Piece"): - # Try simple pattern: contains "has finished playing One Piece" - if "has finished playing One Piece" not in entry.get("Name", ""): - continue + if "has finished playing One Piece" not in entry.get("Name", ""): + continue date_str = entry.get("Date", "") try: