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).