diff --git a/api/video/youtube.py b/api/video/youtube.py index 2a26c15a..ecf12fe6 100644 --- a/api/video/youtube.py +++ b/api/video/youtube.py @@ -131,6 +131,11 @@ def register_routes(bp): if not playlist or not playlist.get("playlist_id"): return jsonify({"success": False, "error": "Could not resolve playlist"}), 404 ok = db.add_playlist_to_watchlist(playlist) + if ok and playlist.get("videos"): # remember the count straight away + try: + db.cache_channel_videos(playlist["playlist_id"], playlist["videos"]) + except Exception: + pass return jsonify({"success": ok, "following": ok, "playlist": {k: playlist.get(k) for k in ("playlist_id", "title", "thumbnail_url")}}) except Exception: @@ -385,6 +390,10 @@ def register_routes(bp): v["wished"] = v.get("youtube_id") in wished if not v.get("published_at") and dates.get(v.get("youtube_id")): v["published_at"] = dates[v["youtube_id"]] + try: + db.cache_channel_videos(playlist_id, vids) # remember the count for the watchlist card + except Exception: + pass return jsonify({"success": True, "videos": vids, "playlist": pl, "following": bool(db.playlist_watch_state([pl["playlist_id"]])), "kind": "playlist", "source": "youtube"}) diff --git a/database/video_database.py b/database/video_database.py index 79294083..dd4f1ad4 100644 --- a/database/video_database.py +++ b/database/video_database.py @@ -1846,18 +1846,21 @@ class VideoDatabase: conn.close() def list_watchlist_channels(self) -> list[dict]: - """Followed channels (newest first), each with a wished-video count for the card.""" + """Followed channels (newest first): ``video_count`` is the REMEMBERED catalog + size (from the cache, fills in as the channel is enriched/opened), plus how + many of its videos are wished.""" conn = self._get_connection() try: rows = conn.execute( "SELECT w.title, w.poster_url, w.source_id, w.date_added, " + "(SELECT COUNT(*) FROM youtube_channel_videos cv WHERE cv.channel_id = w.source_id) AS video_count, " "(SELECT COUNT(*) FROM video_wishlist v WHERE v.kind='video' " - " AND v.parent_source_id = w.source_id) AS video_count " + " AND v.parent_source_id = w.source_id) AS wished_count " "FROM video_watchlist w WHERE w.kind='channel' AND w.state='follow' " "ORDER BY w.date_added DESC, w.id DESC").fetchall() return [{"kind": "channel", "youtube_id": r["source_id"], "title": r["title"], "poster_url": r["poster_url"], "video_count": r["video_count"], - "date_added": r["date_added"]} for r in rows] + "wished_count": r["wished_count"], "date_added": r["date_added"]} for r in rows] finally: conn.close() @@ -1918,14 +1921,18 @@ class VideoDatabase: conn.close() def list_watchlist_playlists(self) -> list[dict]: - """Followed playlists (newest first).""" + """Followed playlists (newest first), each with its remembered video count + (cached when the playlist is followed/opened).""" conn = self._get_connection() try: rows = conn.execute( - "SELECT title, poster_url, source_id, date_added FROM video_watchlist " - "WHERE kind='playlist' AND state='follow' ORDER BY date_added DESC, id DESC").fetchall() + "SELECT w.title, w.poster_url, w.source_id, w.date_added, " + "(SELECT COUNT(*) FROM youtube_channel_videos cv WHERE cv.channel_id = w.source_id) AS video_count " + "FROM video_watchlist w WHERE w.kind='playlist' AND w.state='follow' " + "ORDER BY w.date_added DESC, w.id DESC").fetchall() return [{"kind": "playlist", "playlist_id": r["source_id"], "title": r["title"], - "poster_url": r["poster_url"], "date_added": r["date_added"]} for r in rows] + "poster_url": r["poster_url"], "video_count": r["video_count"], + "date_added": r["date_added"]} for r in rows] finally: conn.close() diff --git a/tests/test_video_api.py b/tests/test_video_api.py index c1f418ef..ba39aafd 100644 --- a/tests/test_video_api.py +++ b/tests/test_video_api.py @@ -564,10 +564,12 @@ def test_youtube_follow_then_channels_and_wishlist(tmp_path): assert data["added_videos"] == 2 assert data["counts"] == {"channel": 1, "video": 2} - # appears on the watchlist channels list with a video count + # appears on the watchlist channels list: wished_count = the 2 wished videos, + # video_count = the remembered catalog (0 here — nothing cached in this test). chans = client.get("/api/video/youtube/channels").get_json() assert chans["channels"][0]["youtube_id"] == "UCPlay" - assert chans["channels"][0]["video_count"] == 2 + assert chans["channels"][0]["wished_count"] == 2 + assert chans["channels"][0]["video_count"] == 0 # appears in the youtube wishlist as a nebula channel (year=season, video=episode) wl = client.get("/api/video/youtube/wishlist").get_json() diff --git a/tests/test_video_database.py b/tests/test_video_database.py index 4877dada..9478682e 100644 --- a/tests/test_video_database.py +++ b/tests/test_video_database.py @@ -1105,7 +1105,8 @@ def test_youtube_rows_do_not_disturb_tmdb_counts(db): assert db.watchlist_counts() == {"show": 1, "person": 0, "total": 1} # youtube counts live on their own surface assert db.youtube_wishlist_counts() == {"channel": 1, "video": 1} - assert db.list_watchlist_channels()[0]["video_count"] == 1 + assert db.list_watchlist_channels()[0]["wished_count"] == 1 # 1 wished video + assert db.list_watchlist_channels()[0]["video_count"] == 0 # remembered catalog (nothing cached) def test_upgrade_from_pre_source_schema(tmp_path): diff --git a/webui/static/video/video-watchlist.js b/webui/static/video/video-watchlist.js index 44207c1b..3e7ac55d 100644 --- a/webui/static/video/video-watchlist.js +++ b/webui/static/video/video-watchlist.js @@ -20,14 +20,14 @@ // (like any show/movie); the ✕ unfollows. function channelCard(ch) { var av = window.VideoYoutube ? VideoYoutube.avatar(ch, 'vyt-wcard-avatar') : ''; - var n = ch.video_count || 0; + var n = ch.video_count || 0; // remembered catalog size (fills in as enriched) + var meta = n > 0 ? (n + ' video' + (n === 1 ? '' : 's')) : 'Channel'; return '