Watchlist cards: show the channel/playlist video count, not '0 videos'
The channel card's 'N videos' was actually the count of WISHED videos (0 until you wish some) — so every channel read '0 videos'. It now shows the REMEMBERED catalog size (from youtube_channel_videos, which fills in as a channel is enriched/opened), falling back to 'Channel' when nothing's cached yet. Wished count is kept as a separate wished_count field. Playlists likewise show their video count (cached when the playlist is followed-with-videos or opened) instead of a static 'Playlist'. list_watchlist_channels/playlists return the remembered count; the playlist detail + follow endpoints cache the list so the count is known. 138 tests green.
This commit is contained in:
parent
abb9d0e89e
commit
ba7562c5f2
6 changed files with 36 additions and 15 deletions
|
|
@ -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"})
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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 '<div class="vyt-wcard" data-vyt-open-channel="' + esc(ch.youtube_id) + '" title="Open channel">' +
|
||||
'<div class="vyt-wcard-art">' + av + '</div>' +
|
||||
'<button class="vyt-wcard-unfollow" type="button" data-vyt-wunfollow="' + esc(ch.youtube_id) +
|
||||
'" title="Unfollow">✕</button>' +
|
||||
'<div class="vyt-wcard-info"><span class="vyt-wcard-title" title="' + esc(ch.title) + '">' +
|
||||
esc(ch.title) + '</span><span class="vyt-wcard-meta">' + n + ' video' + (n === 1 ? '' : 's') +
|
||||
'</span></div></div>';
|
||||
esc(ch.title) + '</span><span class="vyt-wcard-meta">' + esc(meta) + '</span></div></div>';
|
||||
}
|
||||
|
||||
// Followed playlists sit beside channels in the same grid; the ✕ unfollows.
|
||||
|
|
@ -39,7 +39,9 @@
|
|||
'<button class="vyt-wcard-unfollow" type="button" data-vyt-wunfollow-playlist="' + esc(pl.playlist_id) +
|
||||
'" title="Unfollow">✕</button>' +
|
||||
'<div class="vyt-wcard-info"><span class="vyt-wcard-title" title="' + esc(pl.title) + '">' +
|
||||
esc(pl.title) + '</span><span class="vyt-wcard-meta">Playlist</span></div></div>';
|
||||
esc(pl.title) + '</span><span class="vyt-wcard-meta">' +
|
||||
(pl.video_count > 0 ? esc(pl.video_count + ' video' + (pl.video_count === 1 ? '' : 's')) : 'Playlist') +
|
||||
'</span></div></div>';
|
||||
}
|
||||
|
||||
function $(s, r) { return (r || document).querySelector(s); }
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@
|
|||
function followPlaylist(pl) {
|
||||
var p = pl || {};
|
||||
return post('/api/video/youtube/playlist/follow', { playlist: {
|
||||
playlist_id: p.playlist_id, title: p.title, thumbnail_url: p.thumbnail_url } });
|
||||
playlist_id: p.playlist_id, title: p.title, thumbnail_url: p.thumbnail_url, videos: p.videos } });
|
||||
}
|
||||
function unfollowPlaylist(playlistId) {
|
||||
return post('/api/video/youtube/playlist/unfollow', { playlist_id: playlistId });
|
||||
|
|
|
|||
Loading…
Reference in a new issue