From b3fd6037bb31cad39732e8db6de4479ed6482d90 Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Wed, 17 Jun 2026 08:32:47 -0700 Subject: [PATCH] YouTube: fix missing channel poster on the wishlist orb MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flat channel listing doesn't always surface the avatar, so it could be stored null → the orb fell back to plain initials (looked like a missing poster). Two fixes: - Orb falls back to the channel's newest video thumbnail when the avatar is absent, so it's never blank. - Opening the channel page (which resolves the real avatar) now backfills it onto every wished row via set_wishlist_channel_poster — so the actual channel avatar appears on the wishlist orb thereafter. 6 tests green. --- api/video/youtube.py | 6 ++++++ database/video_database.py | 16 ++++++++++++++++ tests/test_video_database.py | 13 +++++++++++++ webui/static/video/video-wishlist.js | 9 ++++++--- 4 files changed, 41 insertions(+), 3 deletions(-) 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