"""Video discover API — browse TMDB (movies/TV the user doesn't own yet). GET /api/video/discover/hero → trending titles w/ backdrops (slideshow) GET /api/video/discover/genres → {movie:[{id,name}], show:[{id,name}]} GET /api/video/discover/list?... → one shelf/grid of items, e.g. ?key=trending → trending movies + shows ?key=&page= → a canned list (popular_movies, top_shows…) ?kind=movie|show&genre=&year=&decade=&sort=&page= → a filtered browse Items are annotated with ``library_id`` when already owned (so the card links to the owned detail, not the TMDB preview). Reads only the enrichment engine + video.db; isolated from the music API. """ from __future__ import annotations from flask import jsonify, request from utils.logging_config import get_logger logger = get_logger("video_api.discover") def register_routes(bp): @bp.route("/discover/hero", methods=["GET"]) def video_discover_hero(): """A few trending titles that have a backdrop — drives the hero slideshow.""" from core.video.enrichment.engine import get_video_enrichment_engine try: items = [x for x in (get_video_enrichment_engine().trending() or []) if x.get("backdrop")][:6] except Exception: logger.exception("discover hero failed") items = [] return jsonify({"items": items}) @bp.route("/discover/taste", methods=["GET"]) def video_discover_taste(): """The user's most-owned genres (movies + shows) → personalized rails.""" from . import get_video_db try: from core.video.sources import resolve_video_server srv = resolve_video_server() except Exception: srv = None db = get_video_db() try: return jsonify({"movie": db.top_owned_genres("movie", srv, 6), "show": db.top_owned_genres("show", srv, 6)}) except Exception: logger.exception("discover taste failed") return jsonify({"movie": [], "show": []}) @bp.route("/discover/morelike", methods=["GET"]) def video_discover_morelike(): """'More like ' rails — TMDB recommendations seeded from a few random titles you already own (interleaved movie/show, max 3 rails).""" from . import get_video_db from core.video.enrichment.engine import get_video_enrichment_engine try: from core.video.sources import resolve_video_server srv = resolve_video_server() except Exception: srv = None db = get_video_db() eng = get_video_enrichment_engine() try: seeds = db.random_owned_titles(2, srv) movies = [s for s in seeds if s["kind"] == "movie"] shows = [s for s in seeds if s["kind"] == "show"] ordered = [] while (movies or shows) and len(ordered) < 3: if movies: ordered.append(movies.pop(0)) if shows and len(ordered) < 3: ordered.append(shows.pop(0)) rails = [] for s in ordered: items = [it for it in eng.recommendations(s["kind"], s["tmdb_id"]) if it.get("tmdb_id") != s["tmdb_id"]] if len(items) >= 4: rails.append({"title": "More like " + s["title"], "items": items[:30]}) return jsonify({"rails": rails}) except Exception: logger.exception("discover morelike failed") return jsonify({"rails": []}) @bp.route("/discover/trailer", methods=["GET"]) def video_discover_trailer(): """Best YouTube trailer {key,name} for a tmdb title (hero 'Trailer' button).""" from core.video.enrichment.engine import get_video_enrichment_engine kind = request.args.get("kind", "movie") try: tmdb_id = int(request.args.get("tmdb_id")) except (TypeError, ValueError): return jsonify({"trailer": None}) try: tr = get_video_enrichment_engine().trailer(kind, tmdb_id) except Exception: logger.exception("discover trailer failed") tr = None return jsonify({"trailer": tr or None}) @bp.route("/discover/genres", methods=["GET"]) def video_discover_genres(): """Genre id→name maps for both kinds (powers the genre rails + filter).""" from core.video.enrichment.engine import get_video_enrichment_engine eng = get_video_enrichment_engine() try: return jsonify({"movie": eng.genre_list("movie"), "show": eng.genre_list("show")}) except Exception: logger.exception("discover genres failed") return jsonify({"movie": [], "show": []}) @bp.route("/discover/list", methods=["GET"]) def video_discover_list(): """One shelf (rail) or one page of a filtered browse — see module docstring. ``pages`` (1–3, default 1) fetches that many consecutive TMDB pages and concatenates them (deduped) in one response — so a rail can show ~40 items and still look full after 'Hide owned' drops the ones you have.""" from core.video.enrichment.engine import get_video_enrichment_engine eng = get_video_enrichment_engine() try: page = max(1, int(request.args.get("page", 1) or 1)) except (TypeError, ValueError): page = 1 try: pages = min(3, max(1, int(request.args.get("pages", 1) or 1))) except (TypeError, ValueError): pages = 1 key = (request.args.get("key") or "").strip() kind = request.args.get("kind", "movie") genre = request.args.get("genre") or None year = request.args.get("year") or None decade = request.args.get("decade") or None providers = request.args.get("providers") or None sort = request.args.get("sort") or "popularity.desc" def fetch(p): if key == "trending": return eng.trending() if key: return eng.discover_curated(key, page=p) return eng.discover_filter(kind, genre=genre, year=year, decade=decade, providers=providers, sort_by=sort, page=p) try: items, seen = [], set() for p in range(page, page + pages): for it in (fetch(p) or []): dk = (it.get("kind"), it.get("tmdb_id")) if dk in seen: continue seen.add(dk) items.append(it) if key == "trending": break # trending is a fixed list — extra pages just repeat it return jsonify({"items": items, "page": page}) except Exception: logger.exception("discover list failed (key=%s)", key) return jsonify({"items": [], "page": page})