YouTube: fix missing channel poster on the wishlist orb

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.
This commit is contained in:
BoulderBadgeDad 2026-06-17 08:32:47 -07:00
parent 591138a8a1
commit b3fd6037bb
4 changed files with 41 additions and 3 deletions

View file

@ -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

View file

@ -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()

View file

@ -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"

View file

@ -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
? '<img class="wl-orb-img" src="' + esc(sh.poster_url) + '" alt="" ' +
// Channel avatar can be missing (flat listing doesn't always surface it) —
// fall back to the newest video's thumbnail so the orb is never blank.
var poster = sh.poster_url || (yt && (sh.seasons || [])[0] ? sh.seasons[0].poster_url : null);
var img = poster
? '<img class="wl-orb-img" src="' + esc(poster) + '" alt="" ' +
'onerror="this.outerHTML=\'<div class=&quot;wl-orb-initials&quot;>' + esc(initials(sh.title)) + '</div>\'">'
: '<div class="wl-orb-initials">' + esc(initials(sh.title)) + '</div>';
// 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