fixing output

This commit is contained in:
Pat Teruel committed 2026-02-22 16:01:12 +08:00
1 parent 9e799049a7
commit 2698fe7114
4 files changed
+234 -62

No files matched your search

+68 -6
View File
@@ -13,6 +13,37 @@ app = Flask(__name__)
PHT = timezone(timedelta(hours=8)) PHT = timezone(timedelta(hours=8))
def _sunday_midnight_pht_for(now_pht=None):
"""Return the Sunday midnight (start of day) in PHT used for week-start logic.
This function treats Sunday as belonging to the previous week (used for producing
week ranges like "Feb 15-21" when today is Sunday Feb 22).
"""
now_pht = now_pht or datetime.now(PHT)
wd = now_pht.weekday()
days_since_sunday = (wd + 1) % 7
if wd == 6:
days_since_sunday = 7
sunday_midnight_pht = (now_pht - timedelta(days=days_since_sunday)).replace(
hour=0, minute=0, second=0, microsecond=0
)
return sunday_midnight_pht
def _latest_sunday_midnight_pht(now_pht=None):
"""Return the latest Sunday midnight (start of day) in PHT including today if it's Sunday.
This is for computing the exact UTC cutoff timestamps for a given latest Sunday.
"""
now_pht = now_pht or datetime.now(PHT)
wd = now_pht.weekday()
days_since_sunday = (wd + 1) % 7 # Sunday -> 0
sunday_midnight_pht = (now_pht - timedelta(days=days_since_sunday)).replace(
hour=0, minute=0, second=0, microsecond=0
)
return sunday_midnight_pht
def current_week(): def current_week():
"""Return (week_start, week_end, cutoff_utc) for the current week in PHT. """Return (week_start, week_end, cutoff_utc) for the current week in PHT.
@@ -20,17 +51,34 @@ def current_week():
week_end = that Sunday + 6 days (Saturday) week_end = that Sunday + 6 days (Saturday)
cutoff_utc = Sunday 00:00 PHT expressed in UTC (= Saturday 16:00 UTC) cutoff_utc = Sunday 00:00 PHT expressed in UTC (= Saturday 16:00 UTC)
""" """
now_pht = datetime.now(PHT) sunday_midnight_pht = _sunday_midnight_pht_for()
days_since_sunday = (now_pht.weekday() + 1) % 7
sunday_midnight_pht = (now_pht - timedelta(days=days_since_sunday)).replace(
hour=0, minute=0, second=0, microsecond=0
)
week_start = sunday_midnight_pht.date() week_start = sunday_midnight_pht.date()
week_end = week_start + timedelta(days=6) week_end = week_start + timedelta(days=6)
cutoff_utc = sunday_midnight_pht.astimezone(timezone.utc) cutoff_utc = sunday_midnight_pht.astimezone(timezone.utc)
return week_start, week_end, cutoff_utc return week_start, week_end, cutoff_utc
def _iso_z(dt):
"""Format timezone-aware datetime to 'YYYY-MM-DDTHH:MM:SS.mmmZ' in UTC."""
s = dt.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")
return s[:-3] + "Z"
def get_latest_sunday():
"""Return latest Sunday start-of-day PHT expressed in UTC ISO Z format (inclusive)."""
sunday_midnight_pht = _latest_sunday_midnight_pht()
cutoff_utc = sunday_midnight_pht.astimezone(timezone.utc)
return _iso_z(cutoff_utc)
def get_previous_sunday():
"""Return the Sunday prior to the latest Sunday (UTC ISO Z)."""
sunday_midnight_pht = _latest_sunday_midnight_pht()
prev_midnight = sunday_midnight_pht - timedelta(weeks=1)
prev_cutoff_utc = prev_midnight.astimezone(timezone.utc)
return _iso_z(prev_cutoff_utc)
def format_date_range(week_start, week_end) -> str: def format_date_range(week_start, week_end) -> str:
"""Format date range: 'Feb 8-14' (same month) or 'Mar 29 - Apr 4' (different).""" """Format date range: 'Feb 8-14' (same month) or 'Mar 29 - Apr 4' (different)."""
if week_start.month == week_end.month: if week_start.month == week_end.month:
@@ -48,12 +96,16 @@ def index():
"TOTAL_EPISODES": os.environ.get("TOTAL_EPISODES", "1155"), "TOTAL_EPISODES": os.environ.get("TOTAL_EPISODES", "1155"),
"DB_PATH": os.environ.get("DB_PATH", "/app/data/episodes.db"), "DB_PATH": os.environ.get("DB_PATH", "/app/data/episodes.db"),
} }
latest = get_latest_sunday()
previous = get_previous_sunday()
return render_template( return render_template(
"index.html", "index.html",
week_start=week_start.isoformat(), week_start=week_start.isoformat(),
week_end=week_end.isoformat(), week_end=week_end.isoformat(),
date_range=format_date_range(week_start, week_end), date_range=format_date_range(week_start, week_end),
config=config, config=config,
latest_sunday=latest,
previous_sunday=previous,
) )
@@ -61,11 +113,16 @@ def index():
def report(): def report():
week_start, week_end, cutoff_utc = current_week() week_start, week_end, cutoff_utc = current_week()
total_episodes = int(os.environ.get("TOTAL_EPISODES", 1155)) total_episodes = int(os.environ.get("TOTAL_EPISODES", 1155))
latest = get_latest_sunday()
previous = get_previous_sunday()
jellyfin.clear_debug_logs()
try: try:
ep_info = jellyfin.find_last_one_piece_before(cutoff_utc) ep_info = jellyfin.find_last_one_piece_before(cutoff_utc)
except Exception as e: except Exception as e:
return jsonify({"error": str(e)}), 502 debug_output = jellyfin.get_debug_logs()
return jsonify({"error": str(e), "debug": debug_output}), 502
db.save_episode( db.save_episode(
episode_number=ep_info["episode_number"], episode_number=ep_info["episode_number"],
@@ -87,6 +144,8 @@ def report():
f"Around {watched_this_week} episodes watched this week!" f"Around {watched_this_week} episodes watched this week!"
) )
debug_output = jellyfin.get_debug_logs()
return jsonify({ return jsonify({
"week_start": week_start.isoformat(), "week_start": week_start.isoformat(),
"week_end": week_end.isoformat(), "week_end": week_end.isoformat(),
@@ -98,6 +157,9 @@ def report():
"media_name": ep_info.get("media_name"), "media_name": ep_info.get("media_name"),
"watched_at": ep_info.get("watched_at"), "watched_at": ep_info.get("watched_at"),
"report": report_text, "report": report_text,
"latest_sunday": latest,
"previous_sunday": previous,
"debug": debug_output,
}) })
+120 -42
View File
@@ -4,6 +4,22 @@ import re
from datetime import datetime, timezone, timedelta from datetime import datetime, timezone, timedelta
import requests import requests
class JellyfinDebug:
"""Accumulate debug messages from API calls."""
def __init__(self):
self.logs = []
def add(self, msg):
self.logs.append(msg)
def get_text(self):
return '\n'.join(self.logs)
_debug = JellyfinDebug()
def _cfg(): def _cfg():
return { return {
"host": os.environ["JELLYFIN_HOST"].rstrip("/"), "host": os.environ["JELLYFIN_HOST"].rstrip("/"),
@@ -12,6 +28,16 @@ def _cfg():
} }
def get_debug_logs():
"""Return accumulated debug messages."""
return _debug.get_text()
def clear_debug_logs():
"""Clear debug logs."""
_debug.logs = []
def get_activity_logs(min_date: datetime) -> list: def get_activity_logs(min_date: datetime) -> list:
"""Return activity log entries since min_date (UTC datetime).""" """Return activity log entries since min_date (UTC datetime)."""
c = _cfg() c = _cfg()
@@ -19,12 +45,33 @@ def get_activity_logs(min_date: datetime) -> list:
params = { params = {
"api_key": c["key"], "api_key": c["key"],
"hasUserId": "true", "hasUserId": "true",
"limit": 200, "limit": "200",
"minDate": min_date.strftime("%Y-%m-%dT%H:%M:%S.000000Z"), "minDate": min_date.strftime("%Y-%m-%dT%H:%M:%S.000000Z"),
} }
resp = requests.get(url, params=params, timeout=15)
resp.raise_for_status() curl_cmd = f"curl --location '{url}?"
return resp.json().get("Items", []) curl_params = []
for k, v in params.items():
curl_params.append(f"{k}={v}")
curl_cmd += "&".join(curl_params)
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("")
_debug.add("Curl command:")
_debug.add(curl_cmd)
_debug.add("")
try:
resp = requests.get(url, params=params, timeout=15)
resp.raise_for_status()
items = resp.json().get("Items", [])
_debug.add(f"✓ Got {len(items)} activity log entries")
return items
except Exception as e:
_debug.add(f"✗ Error: {e}")
raise
def get_playback_info(item_id: str) -> dict: def get_playback_info(item_id: str) -> dict:
@@ -32,12 +79,20 @@ def get_playback_info(item_id: str) -> dict:
c = _cfg() c = _cfg()
url = f"{c['host']}/Items/{item_id}/PlaybackInfo" url = f"{c['host']}/Items/{item_id}/PlaybackInfo"
params = {"api_key": c["key"]} params = {"api_key": c["key"]}
resp = requests.get(url, params=params, timeout=10)
resp.raise_for_status() _debug.add(f"GET {url}")
sources = resp.json().get("MediaSources", [])
if not sources: try:
raise RuntimeError(f"No MediaSources for item {item_id}") resp = requests.get(url, params=params, timeout=10)
return sources[0] resp.raise_for_status()
sources = resp.json().get("MediaSources", [])
if not sources:
raise RuntimeError(f"No MediaSources for item {item_id}")
_debug.add(f" ✓ Got media source: {sources[0].get('Name', 'unknown')}")
return sources[0]
except Exception as e:
_debug.add(f" ✗ Error: {e}")
raise
def extract_episode_number(name: str) -> int: def extract_episode_number(name: str) -> int:
@@ -55,38 +110,61 @@ def find_last_one_piece_before(cutoff_utc: datetime) -> dict:
""" """
c = _cfg() c = _cfg()
min_date = cutoff_utc - timedelta(days=14) min_date = cutoff_utc - timedelta(days=14)
entries = get_activity_logs(min_date) cutoff_utc_str = cutoff_utc.isoformat()
_debug.add(f"Searching for One Piece episodes before {cutoff_utc_str}")
_debug.add(f"User ID: {c['user']}")
_debug.add(f"Search window: {min_date.isoformat()} to {cutoff_utc_str}")
_debug.add("")
try:
entries = get_activity_logs(min_date)
except Exception as e:
_debug.add(f"✗ Failed to fetch activity logs: {e}")
raise
_debug.add("")
one_piece_count = 0
for entry in entries: for entry in entries:
if entry.get("Type") != "VideoPlaybackStopped": entry_type = entry.get("Type")
continue is_playback = entry_type == "VideoPlaybackStopped"
if entry.get("UserId") != c["user"]: user_id = entry.get("UserId")
continue is_user = user_id == c["user"]
if "One Piece" not in entry.get("Name", ""): name = entry.get("Name", "")
continue is_one_piece = "One Piece" in name
date_str = entry.get("Date", "") if is_playback and is_user and is_one_piece:
try: one_piece_count += 1
entry_dt = datetime.fromisoformat(date_str.replace("Z", "+00:00")) date_str = entry.get("Date", "")
except ValueError: try:
continue entry_dt = datetime.fromisoformat(date_str.replace("Z", "+00:00"))
is_before_cutoff = entry_dt < cutoff_utc
if entry_dt >= cutoff_utc:
continue if is_before_cutoff:
_debug.add(f"✓ Found One Piece entry: {name}")
item_id = entry.get("ItemId") _debug.add(f" Date: {date_str} (before cutoff)")
if not item_id:
continue item_id = entry.get("ItemId")
if item_id:
source = get_playback_info(item_id) try:
ep_num = extract_episode_number(source["Name"]) source = get_playback_info(item_id)
ep_num = extract_episode_number(source["Name"])
return { _debug.add(f" Extracted episode: {ep_num}")
"episode_number": ep_num, _debug.add("")
"item_id": item_id, return {
"watched_at": date_str, "episode_number": ep_num,
"media_name": source["Name"], "item_id": item_id,
} "watched_at": date_str,
"media_name": source["Name"],
}
except Exception as e:
_debug.add(f" ✗ Error extracting episode: {e}")
else:
_debug.add(f" No ItemId in entry")
except ValueError as ve:
_debug.add(f"✗ Failed to parse date {date_str}: {ve}")
_debug.add(f"✗ No One Piece episode found before cutoff")
_debug.add(f" Found {one_piece_count} One Piece entries total (some may be after cutoff)")
raise RuntimeError("No One Piece episode found before the Sunday cutoff in the last 14 days") raise RuntimeError("No One Piece episode found before the Sunday cutoff in the last 14 days")
+4 -1
View File
@@ -45,7 +45,10 @@ def _seed(con):
from datetime import date from datetime import date
PHT = timezone(timedelta(hours=8)) PHT = timezone(timedelta(hours=8))
now_pht = datetime.now(PHT) now_pht = datetime.now(PHT)
days_since_sunday = (now_pht.weekday() + 1) % 7 wd = now_pht.weekday()
days_since_sunday = (wd + 1) % 7
if wd == 6:
days_since_sunday = 7
current_sunday = (now_pht - timedelta(days=days_since_sunday)).date() current_sunday = (now_pht - timedelta(days=days_since_sunday)).date()
prev_sunday = current_sunday - timedelta(weeks=1) prev_sunday = current_sunday - timedelta(weeks=1)
prev_saturday = prev_sunday + timedelta(days=6) prev_saturday = prev_sunday + timedelta(days=6)
+42 -13
View File
@@ -24,7 +24,7 @@
border-radius: 10px; border-radius: 10px;
padding: 1.4rem; padding: 1.4rem;
width: 100%; width: 100%;
max-width: 500px; max-width: 1000px;
margin-bottom: 1rem; margin-bottom: 1rem;
} }
button { button {
@@ -76,17 +76,6 @@
<h1>🏴‍☠️ One Piece Progress Reporter</h1> <h1>🏴‍☠️ One Piece Progress Reporter</h1>
<div class="week-label">Week: <strong>{{ date_range }}</strong> &nbsp;({{ week_start }} → {{ week_end }})</div> <div class="week-label">Week: <strong>{{ date_range }}</strong> &nbsp;({{ week_start }} → {{ week_end }})</div>
<div class="card" style="margin-top:0.8rem;">
<h2 style="font-size:0.95rem;color:#aaa;margin-bottom:0.5rem;">Environment</h2>
<pre id="env" style="white-space:pre-wrap;font-family:monospace;font-size:0.85rem;color:#ddd;background:#0b0b0b;border-radius:6px;padding:0.6rem;">
JELLYFIN_HOST={{ config.JELLYFIN_HOST }}
JELLYFIN_API_KEY={{ config.JELLYFIN_API_KEY }}
JELLYFIN_USER_ID={{ config.JELLYFIN_USER_ID }}
TOTAL_EPISODES={{ config.TOTAL_EPISODES }}
DB_PATH={{ config.DB_PATH }}
</pre>
</div>
<div class="card"> <div class="card">
<button id="btn-report">Generate Report</button> <button id="btn-report">Generate Report</button>
<div id="status" class="status"></div> <div id="status" class="status"></div>
@@ -104,6 +93,20 @@ DB_PATH={{ config.DB_PATH }}
<div id="media-name" class="media-name hidden"></div> <div id="media-name" class="media-name hidden"></div>
</div> </div>
<div class="card" style="margin-top:0.8rem;">
<h2 style="font-size:0.95rem;color:#aaa;margin-bottom:0.5rem;">Debug</h2>
<pre id="debug" style="white-space:pre-wrap;font-family:monospace;font-size:0.85rem;color:#ddd;background:#0b0b0b;border-radius:6px;padding:0.6rem;">
LATEST_SUNDAY={{ latest_sunday }}
PREVIOUS_SUNDAY={{ previous_sunday }}
JELLYFIN_HOST={{ config.JELLYFIN_HOST }}
JELLYFIN_USER_ID={{ config.JELLYFIN_USER_ID }}
TOTAL_EPISODES={{ config.TOTAL_EPISODES }}
DB_PATH={{ config.DB_PATH }}
(Report output will appear here after Generate Report is clicked)
</pre>
</div>
<script> <script>
const $ = id => document.getElementById(id); const $ = id => document.getElementById(id);
@@ -121,7 +124,13 @@ DB_PATH={{ config.DB_PATH }}
try { try {
const resp = await fetch('/api/report'); const resp = await fetch('/api/report');
const data = await resp.json(); const data = await resp.json();
if (data.error) throw new Error(data.error);
if (data.error) {
if (data.debug) {
$('debug').textContent += '\n\n' + data.debug;
}
throw new Error(data.error);
}
$('d-latest').textContent = data.latest_episode; $('d-latest').textContent = data.latest_episode;
$('d-previous').textContent = data.previous_episode; $('d-previous').textContent = data.previous_episode;
@@ -134,6 +143,26 @@ DB_PATH={{ config.DB_PATH }}
$('media-name').classList.remove('hidden'); $('media-name').classList.remove('hidden');
} }
const debugLines = [];
if (data.debug) {
debugLines.push('\n' + data.debug);
}
debugLines.push('');
debugLines.push('✓ Fetched report from /api/report');
debugLines.push(`✓ Latest Sunday (UTC): ${data.latest_sunday}`);
debugLines.push(`✓ Previous Sunday (UTC): ${data.previous_sunday}`);
debugLines.push(`✓ Activity cutoff: before ${data.latest_sunday}`);
debugLines.push(`✓ Found: "${data.media_name}"`);
debugLines.push(`✓ Extracted episode: ${data.latest_episode}`);
debugLines.push(`✓ Previous week episode: ${data.previous_episode}`);
debugLines.push(`✓ Episodes watched: ${data.watched_this_week}`);
debugLines.push(`✓ Total episodes in series: ${data.total_episodes}`);
debugLines.push(`✓ Saved to DB: week_start=${data.week_start}, week_end=${data.week_end}`);
debugLines.push('');
debugLines.push('Final report:');
debugLines.push(data.report);
$('debug').textContent += debugLines.join('\n');
$('details-card').classList.remove('hidden'); $('details-card').classList.remove('hidden');
setStatus(''); setStatus('');
} catch (e) { } catch (e) {