From 3d67f51c29732a5beea77371ef109edc4fe21eeb Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Wed, 17 Jun 2026 09:57:42 -0700 Subject: [PATCH] Search avatars: proxy with a UA + graceful initials fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Screenshot showed channel results with empty circles, not the initials chip — i.e. an avatar URL was present but the image was failing to load. Two causes addressed: - The image proxy fetched googleusercontent/yt3 with NO User-Agent, which the CDN 403s for some avatars → blank. Now sends a browser UA + Accept header. - A failed avatar now swaps to the initials chip (onerror → outerHTML) instead of hiding the img and leaving an empty circle. Also proxy protocol-relative + deeper-subdomain YouTube image hosts. img-proxy tests green. --- api/video/poster.py | 8 +++++++- webui/static/video/video-youtube.js | 11 +++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/api/video/poster.py b/api/video/poster.py index 90f14f97..d82ada3f 100644 --- a/api/video/poster.py +++ b/api/video/poster.py @@ -133,7 +133,13 @@ def register_routes(bp): abort(404) try: import requests - upstream = requests.get(url, timeout=15, stream=True) + # A browser UA — Google's image CDN (yt3/googleusercontent) 403s some + # avatars when fetched with no User-Agent, which blanked search results. + upstream = requests.get(url, timeout=15, stream=True, headers={ + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", + "Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8", + }) if upstream.status_code != 200: abort(404) resp = Response(upstream.iter_content(8192), diff --git a/webui/static/video/video-youtube.js b/webui/static/video/video-youtube.js index 87f8dcef..4234469b 100644 --- a/webui/static/video/video-youtube.js +++ b/webui/static/video/video-youtube.js @@ -37,18 +37,21 @@ // policy can't blank them out. Non-YouTube / already-proxied urls pass through. function img(url) { if (!url) return url; - if (/^https:\/\/([\w-]+\.)?(ytimg\.com|ggpht\.com|googleusercontent\.com)\//i.test(url)) + if (url.indexOf('//') === 0) url = 'https:' + url; // protocol-relative + if (/^https:\/\/([\w-]+\.)*(ytimg\.com|ggpht\.com|googleusercontent\.com)\//i.test(url)) return '/api/video/img?u=' + encodeURIComponent(url); return url; } function avatar(ch, cls) { var url = ch && (ch.poster_url || ch.avatar_url); + var ini = esc(initials(ch && ch.title)); if (url) { - return ''; + // If the image can't load, swap to the initials chip (never an empty circle). + return ''; } - return '' + esc(initials(ch && ch.title)) + ''; + return '' + ini + ''; } // A single wished video tile (thumbnail, title, date, remove).