soulsync/api/video/calendar.py
BoulderBadgeDad 49222dd0b8 video calendar: watchlist-driven by default + an 'All library' toggle
The calendar pulled every airing show in the library regardless of whether you
follow it. Now it's scoped to the EFFECTIVE watchlist by default — explicit show
follows ∪ airing library shows (not muted), same logic as the Shows watchlist tab —
so it tracks what you actually care about, and you can mute a show off it. A
'Watchlist / All library' toggle on the calendar lets you flip to everything you
own (remembered in localStorage).

- calendar_upcoming(watchlist_only=) adds the watchlist filter; /calendar takes
  ?scope=watchlist|all (default watchlist).
- Calendar page gets a scope toggle (defaults watchlist, persists, refetches on
  change).

Tests: DB scope (followed-only / airing-default / mute drops out / all-library sees
all) + frontend wiring. node --check clean.
2026-06-20 14:07:40 -07:00

79 lines
3.3 KiB
Python

"""Video Calendar — upcoming TV episodes for OWNED shows.
GET /api/video/calendar?days=N → episodes airing from today through today+N-1,
grouped client-side into the agenda view. Isolated: reads only video_library.db
via VideoDatabase, writes nothing, never touches the music side.
"""
from __future__ import annotations
from datetime import date, datetime, timedelta
from flask import jsonify, request
from utils.logging_config import get_logger
logger = get_logger("video.calendar")
def register_routes(bp):
@bp.route("/calendar", methods=["GET"])
def video_calendar():
from . import get_video_db
try:
days = request.args.get("days", default=7, type=int) or 7
days = max(1, min(days, 31)) # one week (or a few) per view
today = date.today()
# Optional ?start=YYYY-MM-DD for week navigation; default = today.
start = today
start_s = request.args.get("start")
if start_s:
try:
start = datetime.strptime(start_s, "%Y-%m-%d").date()
except ValueError:
start = today
if abs((start - today).days) > 400: # sane bound around today
start = today
end = start + timedelta(days=days - 1)
db = get_video_db()
# One-time backfill: existing shows matched TVDB before air time was
# captured, so re-queue them once (background, only those missing it).
try:
if (db.get_setting("airs_time_backfill") or "") != "1":
n = db.requeue_shows_for_airtime()
db.set_setting("airs_time_backfill", "1")
if n:
logger.info("calendar: queued %d shows for TVDB air-time backfill", n)
except Exception:
logger.exception("airs_time backfill queue failed")
from core.video.sources import resolve_video_server
# scope: 'watchlist' (default — shows you follow/track) or 'all' (every
# airing show in the library). The toggle on the page sends ?scope=.
scope = (request.args.get("scope") or "watchlist").lower()
eps = db.calendar_upcoming(start.isoformat(), end.isoformat(),
server_source=resolve_video_server(),
watchlist_only=(scope != "all"))
# Per-date counts drive the day-strip dots without a second query.
counts: dict[str, int] = {}
owned = 0
for e in eps:
counts[e["air_date"]] = counts.get(e["air_date"], 0) + 1
if e.get("has_file"):
owned += 1
return jsonify({
"today": today.isoformat(), # real today (for the highlight)
"start": start.isoformat(), # window start (may be a future week)
"end": end.isoformat(),
"days": days,
"scope": "all" if scope == "all" else "watchlist",
"counts_by_date": counts,
"total": len(eps),
"owned": owned,
"episodes": eps,
})
except Exception:
logger.exception("video calendar failed")
return jsonify({"error": "calendar failed"}), 500