diff --git a/api/video/discover.py b/api/video/discover.py index 940b9554..c32a9e42 100644 --- a/api/video/discover.py +++ b/api/video/discover.py @@ -64,28 +64,48 @@ def register_routes(bp): @bp.route("/discover/list", methods=["GET"]) def video_discover_list(): - """One shelf (rail) or one page of a filtered browse — see module docstring.""" + """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 - key = (request.args.get("key") or "").strip() 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 + sort = request.args.get("sort") or "popularity.desc" + + def fetch(p): if key == "trending": - items = eng.trending() - elif key: - items = eng.discover_curated(key, page=page) - else: - items = eng.discover_filter( - 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), - sort_by=(request.args.get("sort") or "popularity.desc"), - page=page) - return jsonify({"items": items or [], "page": page}) + return eng.trending() + if key: + return eng.discover_curated(key, page=p) + return eng.discover_filter(kind, genre=genre, year=year, decade=decade, + 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}) diff --git a/tests/test_video_api.py b/tests/test_video_api.py index 0cf87bf4..f0055ce2 100644 --- a/tests/test_video_api.py +++ b/tests/test_video_api.py @@ -54,6 +54,39 @@ def test_blueprint_exposes_dashboard_route(): assert "/api/video/episode///" in rules assert any(r.startswith("/api/video/backdrop/") for r in rules) assert "/api/video/img" in rules + assert "/api/video/discover/hero" in rules + assert "/api/video/discover/genres" in rules + assert "/api/video/discover/taste" in rules + assert "/api/video/discover/list" in rules + + +def test_discover_list_pages_concatenates_and_dedupes(tmp_path, monkeypatch): + client, _ = _make_client(tmp_path) + import core.video.enrichment.engine as eng_mod + + class FakeEng: + def discover_curated(self, key, page=1): + # page 1 → ids 1,2 ; page 2 → ids 2,3 (overlap on 2) + return ([{"kind": "movie", "tmdb_id": 1}, {"kind": "movie", "tmdb_id": 2}] if page == 1 + else [{"kind": "movie", "tmdb_id": 2}, {"kind": "movie", "tmdb_id": 3}]) + monkeypatch.setattr(eng_mod, "get_video_enrichment_engine", lambda: FakeEng()) + r = client.get("/api/video/discover/list?key=popular_movies&pages=2") + assert [it["tmdb_id"] for it in r.get_json()["items"]] == [1, 2, 3] # concatenated + deduped + + +def test_discover_list_trending_fetches_once_despite_pages(tmp_path, monkeypatch): + client, _ = _make_client(tmp_path) + import core.video.enrichment.engine as eng_mod + calls = {"n": 0} + + class FakeEng: + def trending(self): + calls["n"] += 1 + return [{"kind": "movie", "tmdb_id": 9}] + monkeypatch.setattr(eng_mod, "get_video_enrichment_engine", lambda: FakeEng()) + r = client.get("/api/video/discover/list?key=trending&pages=3") + assert calls["n"] == 1 # fixed list — not refetched per page + assert [it["tmdb_id"] for it in r.get_json()["items"]] == [9] def test_img_proxy_rejects_non_tmdb(tmp_path): diff --git a/webui/index.html b/webui/index.html index c6ac5b4a..8c960418 100644 --- a/webui/index.html +++ b/webui/index.html @@ -871,34 +871,37 @@
-
- Browse - - - - - - +
+
+
+ + +
+
+ + + +
+ +
+
+ +
+
+
+ + + + + + + +
+ +