Add personal monthly viewing reports and CSV exports
Magent CI/CD / verify (push) Successful in 10m58s
Magent CI/CD / deploy-prod (push) Skipped
Magent CI/CD / deploy-beta (push) Successful in 1m42s

This commit is contained in:
2026-09-09 16:25:35 +12:00
parent 437836243c
commit 333a799e21
13 changed files with 836 additions and 92 deletions
+8 -6
View File
@@ -99,8 +99,9 @@ async def resolve_identity(user: dict, runtime) -> str | None:
return await asyncio.to_thread(linked_user_id, user["username"], runtime.jellyfin_base_url)
def request_summary(user: dict, start: datetime, end: datetime) -> dict:
clause = "julianday(created_at) >= julianday(?) AND julianday(created_at) <= julianday(?)"
def request_summary(user: dict, start: datetime, end: datetime, *, end_exclusive: bool = False) -> dict:
operator = "<" if end_exclusive else "<="
clause = f"julianday(created_at) >= julianday(?) AND julianday(created_at) {operator} julianday(?)"
params = [start.isoformat(), end.isoformat()]
if user.get("jellyseerr_user_id") is not None:
clause += " AND requested_by_id = ?"
@@ -121,7 +122,7 @@ def request_summary(user: dict, start: datetime, end: datetime) -> dict:
return {**dict(counts), "recent": [dict(row) for row in recent]}
def summarize(history: list, libraries: list, start: datetime, end: datetime) -> dict:
def summarize(history: list, libraries: list, start: datetime, end: datetime, *, end_exclusive: bool = False) -> dict:
library_types = {str(row.get("Id")): str(row.get("CollectionType") or "").lower() for row in libraries}
daily_seconds = defaultdict(float)
clients = defaultdict(float)
@@ -142,7 +143,7 @@ def summarize(history: list, libraries: list, start: datetime, end: datetime) ->
seen.add(row_id)
date = _date(row.get("ActivityDateInserted"))
# Defend against older upstream versions ignoring the range filter.
if not start <= date <= end:
if date < start or (date >= end if end_exclusive else date > end):
continue
duration = _duration(row.get("PlaybackDuration"))
if duration <= 0:
@@ -172,7 +173,8 @@ def summarize(history: list, libraries: list, start: datetime, end: datetime) ->
"episode": f"S{row.get('SeasonNumber', '?')} · E{row.get('EpisodeNumber', '?')}" if episode_id else None,
"minutes": round(duration / 60, 1), "played_at": date.isoformat(), "client": client,
"method": method, "artwork_item_id": artwork_item_id(row.get("NowPlayingItemId"))})
count = (end.date() - start.date()).days + 1
last_date = (end - timedelta(microseconds=1)).date() if end_exclusive and end > start else end.date()
count = (last_date - start.date()).days + 1
daily = [{"date": (start.date() + timedelta(days=i)).isoformat(),
"minutes": round(daily_seconds.get((start.date() + timedelta(days=i)).isoformat(), 0) / 60, 2)} for i in range(count)]
active_days = {day for day, duration in daily_seconds.items() if duration >= 60}
@@ -181,7 +183,7 @@ def summarize(history: list, libraries: list, start: datetime, end: datetime) ->
run = run + 1 if day["date"] in active_days else 0
longest = max(longest, run)
current = 0
cursor = end.date() if end.date().isoformat() in active_days else end.date() - timedelta(days=1)
cursor = last_date if last_date.isoformat() in active_days else last_date - timedelta(days=1)
while cursor.isoformat() in active_days:
current += 1
cursor -= timedelta(days=1)