diff --git a/api/video/wishlist.py b/api/video/wishlist.py index d2351bea..63c3a002 100644 --- a/api/video/wishlist.py +++ b/api/video/wishlist.py @@ -108,6 +108,23 @@ def register_routes(bp): 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. diff --git a/database/video_database.py b/database/video_database.py index d25f67fa..7be32a15 100644 --- a/database/video_database.py +++ b/database/video_database.py @@ -2127,6 +2127,20 @@ class VideoDatabase: finally: conn.close() + def clear_wishlist(self, kind: str) -> int: + """Empty an entire wishlist tab in one go. ``kind`` is the user-facing tab: + 'movie' | 'show' (TV) | 'youtube'. Returns the number of rows removed.""" + dbkind = {"movie": "movie", "show": "episode", "youtube": "video"}.get(kind) + if not dbkind: + return 0 + conn = self._get_connection() + try: + cur = conn.execute("DELETE FROM video_wishlist WHERE kind=?", (dbkind,)) + conn.commit() + return cur.rowcount + finally: + conn.close() + def wishlist_counts(self) -> dict: """{'movie': n, 'show': n, 'episode': n, 'total': movies+episodes}.""" conn = self._get_connection() diff --git a/tests/test_video_wishlist_clear.py b/tests/test_video_wishlist_clear.py new file mode 100644 index 00000000..26f4fbfe --- /dev/null +++ b/tests/test_video_wishlist_clear.py @@ -0,0 +1,94 @@ +"""Wishlist 'Clear all' — empties one tab (movies / TV / YouTube) in a single call. + +The three tabs map to different rows in video_wishlist: movies=kind 'movie', +TV=kind 'episode', YouTube=kind 'video'. clear_wishlist(kind) removes exactly the +one tab's rows and leaves the others alone. +""" + +from __future__ import annotations + +import pytest + +from database.video_database import VideoDatabase + + +@pytest.fixture() +def db(tmp_path): + return VideoDatabase(database_path=str(tmp_path / "video_library.db")) + + +def _seed(db): + db.add_movie_to_wishlist(1, "M1") + db.add_movie_to_wishlist(2, "M2") + db.add_episodes_to_wishlist(9, "Show", [ + {"season_number": 1, "episode_number": 1}, + {"season_number": 1, "episode_number": 2}]) + db.add_videos_to_wishlist({"youtube_id": "ch1", "title": "Ch"}, + [{"youtube_id": "v1", "title": "V1"}, + {"youtube_id": "v2", "title": "V2"}]) + + +def test_clear_each_tab_removes_only_its_own_rows(db): + _seed(db) + assert db.wishlist_counts()["movie"] == 2 + assert db.wishlist_counts()["episode"] == 2 + assert db.youtube_wishlist_counts()["video"] == 2 + + # clear movies → TV + YouTube untouched + assert db.clear_wishlist("movie") == 2 + assert db.wishlist_counts()["movie"] == 0 + assert db.wishlist_counts()["episode"] == 2 + assert db.youtube_wishlist_counts()["video"] == 2 + + # clear TV → only episodes gone + assert db.clear_wishlist("show") == 2 + assert db.wishlist_counts()["episode"] == 0 + assert db.youtube_wishlist_counts()["video"] == 2 + + # clear YouTube + assert db.clear_wishlist("youtube") == 2 + assert db.youtube_wishlist_counts()["video"] == 0 + + +def test_clear_rejects_unknown_kind(db): + _seed(db) + assert db.clear_wishlist("nope") == 0 + assert db.wishlist_counts()["movie"] == 2 # nothing removed + + +def test_clear_empty_tab_is_a_noop(db): + assert db.clear_wishlist("movie") == 0 + + +def test_clear_endpoint(tmp_path): + from flask import Flask + import api.video as videoapi + db = VideoDatabase(database_path=str(tmp_path / "video_library.db")) + _seed(db) + videoapi._video_db = db + app = Flask(__name__) + app.register_blueprint(videoapi.create_video_blueprint(), url_prefix="/api/video") + client = app.test_client() + try: + r = client.post("/api/video/wishlist/clear", json={"kind": "movie"}).get_json() + assert r["success"] and r["removed"] == 2 and r["counts"]["movie"] == 0 + assert client.post("/api/video/wishlist/clear", json={"kind": "bad"}).status_code == 400 + finally: + videoapi._video_db = None + + +# ── frontend wiring ───────────────────────────────────────────────────────── +from pathlib import Path + +_ROOT = Path(__file__).resolve().parent.parent +_JS = (_ROOT / "webui" / "static" / "video" / "video-wishlist.js").read_text(encoding="utf-8") +_INDEX = (_ROOT / "webui" / "index.html").read_text(encoding="utf-8") + + +def test_clear_button_wired_for_all_tabs(): + assert "data-vwsh-clear" in _INDEX + assert "function clearAll(" in _JS + assert "/api/video/wishlist/clear" in _JS + assert "kind: kind" in _JS # clears the active tab + # shown only when the active tab has items + assert "function updateClearBtn(" in _JS diff --git a/webui/index.html b/webui/index.html index df9e54d4..3de50f9c 100644 --- a/webui/index.html +++ b/webui/index.html @@ -1261,6 +1261,7 @@ +