bug: the wishlist badge read /wishlist/counts 'total', which was movies+episodes only — YouTube videos (kind='video') are counted by a separate method and were left out. so a wishlist of only youtube videos showed NO number; it only lit up once tv episodes were added. fix: the /wishlist/counts endpoint now folds youtube_wishlist_counts().video into the total (+ exposes video/channel counts). the badge already live-refreshes on wishlist-changed events (which youtube add/remove fire), so it now updates for youtube too. db methods unchanged (their byte-identical contract is intact). regression tests: mixed + youtube-only.
185 lines
9.1 KiB
Python
185 lines
9.1 KiB
Python
"""Video wishlist API — the curated 'get this' list (movies + episodes).
|
|
|
|
Atomic units are movies and episodes; adding a whole show or a season just hands
|
|
us the explicit episodes to expand into rows. v1 manages membership + the tabbed
|
|
Movies/TV page; the search/download engine that fulfils a wish is a later phase.
|
|
Reads/writes only video_library.db via the shared VideoDatabase.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from flask import jsonify, request
|
|
|
|
from utils.logging_config import get_logger
|
|
|
|
logger = get_logger("video_api.wishlist")
|
|
|
|
_KINDS = ("movie", "show")
|
|
_SCOPES = ("movie", "show", "season", "episode")
|
|
|
|
|
|
def _server():
|
|
"""Active video server_source (stored on rows, informational). None on error."""
|
|
try:
|
|
from core.video.sources import resolve_video_server
|
|
return resolve_video_server()
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def register_routes(bp):
|
|
@bp.route("/wishlist", methods=["GET"])
|
|
def video_wishlist_list():
|
|
"""Paged slice for a tab (kind='movie'|'show'), or counts-only with no kind.
|
|
Shows are grouped show→season→episode with wanted/done roll-ups."""
|
|
from . import get_video_db
|
|
try:
|
|
db = get_video_db()
|
|
counts = db.wishlist_counts()
|
|
kind = request.args.get("kind")
|
|
if kind in _KINDS:
|
|
res = db.query_wishlist(
|
|
kind, search=request.args.get("search", ""), sort=request.args.get("sort", "added"),
|
|
page=request.args.get("page", 1), limit=request.args.get("limit", 60))
|
|
return jsonify({"success": True, "kind": kind, "counts": counts, **res})
|
|
return jsonify({"success": True, "counts": counts})
|
|
except Exception:
|
|
logger.exception("Failed to list video wishlist")
|
|
return jsonify({"success": False, "error": "Failed to load wishlist"}), 500
|
|
|
|
@bp.route("/wishlist/counts", methods=["GET"])
|
|
def video_wishlist_counts():
|
|
from . import get_video_db
|
|
try:
|
|
db = get_video_db()
|
|
counts = db.wishlist_counts() # {movie, show, episode, total(movie+ep)}
|
|
yt = db.youtube_wishlist_counts() # {channel, video} (its own table-shape)
|
|
counts["video"] = yt.get("video", 0)
|
|
counts["channel"] = yt.get("channel", 0)
|
|
# The header/sidebar badge is the WHOLE wishlist — movies + episodes + YouTube
|
|
# videos — so fold the YouTube count into the total (it was movies+episodes only,
|
|
# which is why a YouTube-only wishlist showed no badge).
|
|
counts["total"] = counts.get("total", 0) + counts["video"]
|
|
return jsonify({"success": True, **counts})
|
|
except Exception:
|
|
logger.exception("Failed to count video wishlist")
|
|
return jsonify({"success": False, "error": "Failed"}), 500
|
|
|
|
@bp.route("/wishlist/add", methods=["POST"])
|
|
def video_wishlist_add():
|
|
"""Add a movie or a set of a show's episodes. Body is one of:
|
|
{"movie": {tmdb_id, title, year?, poster_url?, library_id?}}
|
|
{"show": {tmdb_id, title, poster_url?, library_id?},
|
|
"episodes": [{season_number, episode_number, title?, air_date?}, …]}"""
|
|
from . import get_video_db
|
|
body = request.get_json(silent=True) or {}
|
|
srv = _server()
|
|
db = get_video_db()
|
|
try:
|
|
movie = body.get("movie")
|
|
if movie and movie.get("tmdb_id") and (movie.get("title") or "").strip():
|
|
ok = db.add_movie_to_wishlist(
|
|
int(movie["tmdb_id"]), movie["title"].strip(), year=movie.get("year"),
|
|
poster_url=movie.get("poster_url") or None,
|
|
library_id=movie.get("library_id") or None, server_source=srv)
|
|
return jsonify({"success": ok, "added": 1 if ok else 0, "counts": db.wishlist_counts()})
|
|
|
|
show = body.get("show")
|
|
episodes = body.get("episodes") or []
|
|
if show and show.get("tmdb_id") and (show.get("title") or "").strip() and episodes:
|
|
n = db.add_episodes_to_wishlist(
|
|
int(show["tmdb_id"]), show["title"].strip(), episodes,
|
|
poster_url=show.get("poster_url") or None,
|
|
library_id=show.get("library_id") or None, server_source=srv)
|
|
return jsonify({"success": n > 0, "added": n, "counts": db.wishlist_counts()})
|
|
|
|
return jsonify({"success": False, "error": "movie or show+episodes required"}), 400
|
|
except Exception:
|
|
logger.exception("Failed to add to video wishlist")
|
|
return jsonify({"success": False, "error": "Failed"}), 500
|
|
|
|
@bp.route("/wishlist/remove", methods=["POST"])
|
|
def video_wishlist_remove():
|
|
"""Remove at any granularity. Body: {scope, tmdb_id, season_number?, episode_number?}
|
|
where scope ∈ movie|show|season|episode."""
|
|
from . import get_video_db
|
|
body = request.get_json(silent=True) or {}
|
|
scope, tmdb_id = body.get("scope"), body.get("tmdb_id")
|
|
if scope not in _SCOPES or not tmdb_id:
|
|
return jsonify({"success": False, "error": "scope and tmdb_id are required"}), 400
|
|
try:
|
|
db = get_video_db()
|
|
removed = db.remove_from_wishlist(
|
|
scope, tmdb_id=int(tmdb_id),
|
|
season_number=body.get("season_number"), episode_number=body.get("episode_number"))
|
|
return jsonify({"success": True, "removed": removed, "counts": db.wishlist_counts()})
|
|
except Exception:
|
|
logger.exception("Failed to remove from video wishlist")
|
|
return jsonify({"success": False, "error": "Failed"}), 500
|
|
|
|
@bp.route("/wishlist/clear", methods=["POST"])
|
|
def video_wishlist_clear():
|
|
"""Empty an entire wishlist tab. Body: {kind} where kind ∈ movie|show|youtube."""
|
|
from . import get_video_db
|
|
body = request.get_json(silent=True) or {}
|
|
kind = body.get("kind")
|
|
if kind not in ("movie", "show", "youtube"):
|
|
return jsonify({"success": False, "error": "kind must be movie|show|youtube"}), 400
|
|
try:
|
|
db = get_video_db()
|
|
removed = db.clear_wishlist(kind)
|
|
return jsonify({"success": True, "removed": removed,
|
|
"counts": db.wishlist_counts(), "youtube_counts": db.youtube_wishlist_counts()})
|
|
except Exception:
|
|
logger.exception("Failed to clear video wishlist")
|
|
return jsonify({"success": False, "error": "Failed"}), 500
|
|
|
|
@bp.route("/wishlist/backfill-art", methods=["POST"])
|
|
def video_wishlist_backfill_art():
|
|
"""Fill episode stills + season posters for rows that predate art-capture.
|
|
One tmdb_season call per (show, season); best-effort. Returns rows filled."""
|
|
from . import get_video_db
|
|
from core.video.enrichment.engine import get_video_enrichment_engine
|
|
db = get_video_db()
|
|
eng = get_video_enrichment_engine()
|
|
updated = 0
|
|
try:
|
|
for grp in db.wishlist_art_backfill_targets():
|
|
try:
|
|
se = eng.tmdb_season(grp["tmdb_id"], grp["season_number"]) or {}
|
|
except Exception:
|
|
continue
|
|
if se.get("poster_url"):
|
|
updated += db.set_wishlist_season_poster(grp["tmdb_id"], grp["season_number"], se["poster_url"])
|
|
for ep in (se.get("episodes") or []):
|
|
en = ep.get("episode_number")
|
|
if en is None:
|
|
continue
|
|
if ep.get("still_url") and db.set_wishlist_still(grp["tmdb_id"], grp["season_number"], en, ep["still_url"]):
|
|
updated += 1
|
|
if ep.get("overview"):
|
|
db.set_wishlist_episode_overview(grp["tmdb_id"], grp["season_number"], en, ep["overview"])
|
|
return jsonify({"success": True, "updated": updated})
|
|
except Exception:
|
|
logger.exception("wishlist art backfill failed")
|
|
return jsonify({"success": False, "updated": updated})
|
|
|
|
@bp.route("/wishlist/check", methods=["POST"])
|
|
def video_wishlist_check():
|
|
"""Hydrate cards/modal. Body: {movie_ids: [...], show_tmdb_id?} →
|
|
{movies: [ids already wished], episodes: ['S_E' already wished]}."""
|
|
from . import get_video_db
|
|
body = request.get_json(silent=True) or {}
|
|
try:
|
|
db = get_video_db()
|
|
st = db.wishlist_state(
|
|
movie_ids=body.get("movie_ids") or [], show_tmdb_id=body.get("show_tmdb_id"))
|
|
out = {"success": True, "movies": sorted(st["movies"]), "episodes": sorted(st["episodes"])}
|
|
shows = body.get("shows") # multi-show membership for the calendar button
|
|
if shows:
|
|
keys = db.wishlist_keys_for_shows(shows)
|
|
out["by_show"] = {str(tid): sorted(ks) for tid, ks in keys.items()}
|
|
return jsonify(out)
|
|
except Exception:
|
|
logger.exception("Failed to check video wishlist")
|
|
return jsonify({"success": False, "error": "Failed"}), 500
|