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"):
|
if not playlist or not playlist.get("playlist_id"):
|
||||||
return jsonify({"success": False, "error": "Could not resolve playlist"}), 404
|
return jsonify({"success": False, "error": "Could not resolve playlist"}), 404
|
||||||
ok = db.add_playlist_to_watchlist(playlist)
|
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,
|
return jsonify({"success": ok, "following": ok,
|
||||||
"playlist": {k: playlist.get(k) for k in ("playlist_id", "title", "thumbnail_url")}})
|
"playlist": {k: playlist.get(k) for k in ("playlist_id", "title", "thumbnail_url")}})
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|
@ -385,6 +390,10 @@ def register_routes(bp):
|
||||||
v["wished"] = v.get("youtube_id") in wished
|
v["wished"] = v.get("youtube_id") in wished
|
||||||
if not v.get("published_at") and dates.get(v.get("youtube_id")):
|
if not v.get("published_at") and dates.get(v.get("youtube_id")):
|
||||||
v["published_at"] = dates[v["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,
|
return jsonify({"success": True, "videos": vids, "playlist": pl,
|
||||||
"following": bool(db.playlist_watch_state([pl["playlist_id"]])),
|
"following": bool(db.playlist_watch_state([pl["playlist_id"]])),
|
||||||
"kind": "playlist", "source": "youtube"})
|
"kind": "playlist", "source": "youtube"})
|
||||||
|
|
|
||||||
|
|
@ -1846,18 +1846,21 @@ class VideoDatabase:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
def list_watchlist_channels(self) -> list[dict]:
|
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()
|
conn = self._get_connection()
|
||||||
try:
|
try:
|
||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
"SELECT w.title, w.poster_url, w.source_id, w.date_added, "
|
"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' "
|
"(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' "
|
"FROM video_watchlist w WHERE w.kind='channel' AND w.state='follow' "
|
||||||
"ORDER BY w.date_added DESC, w.id DESC").fetchall()
|
"ORDER BY w.date_added DESC, w.id DESC").fetchall()
|
||||||
return [{"kind": "channel", "youtube_id": r["source_id"], "title": r["title"],
|
return [{"kind": "channel", "youtube_id": r["source_id"], "title": r["title"],
|
||||||
"poster_url": r["poster_url"], "video_count": r["video_count"],
|
"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:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
@ -1918,14 +1921,18 @@ class VideoDatabase:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
def list_watchlist_playlists(self) -> list[dict]:
|
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()
|
conn = self._get_connection()
|
||||||
try:
|
try:
|
||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
"SELECT title, poster_url, source_id, date_added FROM video_watchlist "
|
"SELECT w.title, w.poster_url, w.source_id, w.date_added, "
|
||||||
"WHERE kind='playlist' AND state='follow' ORDER BY date_added DESC, id DESC").fetchall()
|
"(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"],
|
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:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -564,10 +564,12 @@ def test_youtube_follow_then_channels_and_wishlist(tmp_path):
|
||||||
assert data["added_videos"] == 2
|
assert data["added_videos"] == 2
|
||||||
assert data["counts"] == {"channel": 1, "video": 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()
|
chans = client.get("/api/video/youtube/channels").get_json()
|
||||||
assert chans["channels"][0]["youtube_id"] == "UCPlay"
|
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)
|
# appears in the youtube wishlist as a nebula channel (year=season, video=episode)
|
||||||
wl = client.get("/api/video/youtube/wishlist").get_json()
|
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}
|
assert db.watchlist_counts() == {"show": 1, "person": 0, "total": 1}
|
||||||
# youtube counts live on their own surface
|
# youtube counts live on their own surface
|
||||||
assert db.youtube_wishlist_counts() == {"channel": 1, "video": 1}
|
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):
|
def test_upgrade_from_pre_source_schema(tmp_path):
|
||||||
|
|
|
||||||
|
|
@ -20,14 +20,14 @@
|
||||||
// (like any show/movie); the ✕ unfollows.
|
// (like any show/movie); the ✕ unfollows.
|
||||||
function channelCard(ch) {
|
function channelCard(ch) {
|
||||||
var av = window.VideoYoutube ? VideoYoutube.avatar(ch, 'vyt-wcard-avatar') : '';
|
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">' +
|
return '<div class="vyt-wcard" data-vyt-open-channel="' + esc(ch.youtube_id) + '" title="Open channel">' +
|
||||||
'<div class="vyt-wcard-art">' + av + '</div>' +
|
'<div class="vyt-wcard-art">' + av + '</div>' +
|
||||||
'<button class="vyt-wcard-unfollow" type="button" data-vyt-wunfollow="' + esc(ch.youtube_id) +
|
'<button class="vyt-wcard-unfollow" type="button" data-vyt-wunfollow="' + esc(ch.youtube_id) +
|
||||||
'" title="Unfollow">✕</button>' +
|
'" title="Unfollow">✕</button>' +
|
||||||
'<div class="vyt-wcard-info"><span class="vyt-wcard-title" title="' + esc(ch.title) + '">' +
|
'<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') +
|
esc(ch.title) + '</span><span class="vyt-wcard-meta">' + esc(meta) + '</span></div></div>';
|
||||||
'</span></div></div>';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Followed playlists sit beside channels in the same grid; the ✕ unfollows.
|
// 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) +
|
'<button class="vyt-wcard-unfollow" type="button" data-vyt-wunfollow-playlist="' + esc(pl.playlist_id) +
|
||||||
'" title="Unfollow">✕</button>' +
|
'" title="Unfollow">✕</button>' +
|
||||||
'<div class="vyt-wcard-info"><span class="vyt-wcard-title" title="' + esc(pl.title) + '">' +
|
'<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); }
|
function $(s, r) { return (r || document).querySelector(s); }
|
||||||
|
|
|
||||||
|
|
@ -151,7 +151,7 @@
|
||||||
function followPlaylist(pl) {
|
function followPlaylist(pl) {
|
||||||
var p = pl || {};
|
var p = pl || {};
|
||||||
return post('/api/video/youtube/playlist/follow', { playlist: {
|
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) {
|
function unfollowPlaylist(playlistId) {
|
||||||
return post('/api/video/youtube/playlist/unfollow', { playlist_id: playlistId });
|
return post('/api/video/youtube/playlist/unfollow', { playlist_id: playlistId });
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue