diff --git a/api/video/youtube.py b/api/video/youtube.py index 29b3509f..5155c09c 100644 --- a/api/video/youtube.py +++ b/api/video/youtube.py @@ -147,6 +147,12 @@ def register_routes(bp): return jsonify({"success": False, "error": "Channel not found"}), 404 cid = channel["youtube_id"] following = bool(db.channel_watch_state([cid])) + # backfill the real avatar onto wished rows (flat listing often omits it) + if channel.get("avatar_url"): + try: + db.set_wishlist_channel_poster(cid, channel["avatar_url"]) + except Exception: + pass wished = db.youtube_video_wish_state([v.get("youtube_id") for v in channel.get("videos") or []]) for v in channel.get("videos") or []: v["wished"] = v.get("youtube_id") in wished diff --git a/database/video_database.py b/database/video_database.py index 3e95b2ce..2224cb53 100644 --- a/database/video_database.py +++ b/database/video_database.py @@ -1975,6 +1975,22 @@ class VideoDatabase: finally: conn.close() + def set_wishlist_channel_poster(self, channel_id, poster_url) -> int: + """Refresh the channel avatar on all of a channel's wished video rows — used + to backfill avatars that flat listing didn't surface (channel page resolves + the real avatar). Returns rows updated.""" + if not poster_url or not channel_id: + return 0 + conn = self._get_connection() + try: + cur = conn.execute( + "UPDATE video_wishlist SET poster_url=? WHERE kind='video' AND parent_source_id=?", + (poster_url, str(channel_id))) + conn.commit() + return cur.rowcount + finally: + conn.close() + def youtube_wishlist_counts(self) -> dict: """{'channel': n distinct channels, 'video': n videos} in the wishlist.""" conn = self._get_connection() diff --git a/tests/test_video_database.py b/tests/test_video_database.py index 12892e8b..82133d15 100644 --- a/tests/test_video_database.py +++ b/tests/test_video_database.py @@ -1144,3 +1144,16 @@ def test_upgrade_from_pre_source_schema(tmp_path): assert db.add_videos_to_wishlist({"youtube_id": "UCx", "title": "Chan"}, [{"youtube_id": "x1", "title": "X1"}]) == 1 assert db.youtube_wishlist_counts() == {"channel": 1, "video": 1} + + +def test_set_wishlist_channel_poster_backfills_avatar(db): + # videos added without an avatar (flat listing omitted it) → poster_url null + db.add_videos_to_wishlist({"youtube_id": "UCx", "title": "X"}, + [{"youtube_id": "a", "title": "A", "published_at": "2024-01-01"}, + {"youtube_id": "b", "title": "B", "published_at": "2024-02-01"}]) + grp = db.query_youtube_wishlist()["items"][0] + assert not grp["poster_url"] + # backfilling the resolved avatar fills every row → the orb gets its poster + assert db.set_wishlist_channel_poster("UCx", "http://yt/avatar.jpg") == 2 + grp = db.query_youtube_wishlist()["items"][0] + assert grp["poster_url"] == "http://yt/avatar.jpg" diff --git a/webui/static/video/video-wishlist.js b/webui/static/video/video-wishlist.js index dfcdcc44..89efad89 100644 --- a/webui/static/video/video-wishlist.js +++ b/webui/static/video/video-wishlist.js @@ -82,8 +82,11 @@ : 'data-vwsh-open-show data-vwsh-src="' + src + '" data-vwsh-id="' + esc(openId) + '"'; var hue = hueOf(sh.title); var total = sh.wanted || 0; - var img = sh.poster_url - ? '' : '
' + esc(initials(sh.title)) + '
'; // Episodes are shown grouped under a clickable season header (header → @@ -113,7 +116,7 @@ // --orb-hue on the GROUP so the music orb styles + my cinematic-expand // backdrop (--vwsh-poster) both resolve; poster bleeds in only when expanded. var gstyle = 'animation-delay:' + Math.min(idx * 45, 700) + 'ms;--orb-hue:' + hue + - (sh.poster_url ? ";--vwsh-poster:url('" + esc(sh.poster_url) + "')" : ''); + (poster ? ";--vwsh-poster:url('" + esc(poster) + "')" : ''); var prog = total ? Math.max(0, Math.min(1, (sh.done || 0) / total)) : 0; // #4 acquisition progress // Header is a 3-column row that FLANKS the poster: synopsis (left) · poster // (middle) · cast (right). When collapsed (or no data) the side columns are