fix: use YouTube-provided thumbnail URLs instead of hardcoded hq720.jpg
Videos without hq720.jpg thumbnails caused mass 404 errors. Now preserves the actual thumbnail URL from YouTube's API response, falls back to hqdefault.jpg only when no thumbnail is provided. Also picks highest quality thumbnail from API (thumbnails[-1]) and adds progressive fallback for subscription/download functions.
This commit is contained in:
@@ -99,7 +99,6 @@ def proxy_site(env, start_response, video=False):
|
|||||||
if response.status >= 400:
|
if response.status >= 400:
|
||||||
print('Error: YouTube returned "%d %s" while routing %s' % (
|
print('Error: YouTube returned "%d %s" while routing %s' % (
|
||||||
response.status, response.reason, url.split('?')[0]))
|
response.status, response.reason, url.split('?')[0]))
|
||||||
|
|
||||||
total_received = 0
|
total_received = 0
|
||||||
retry = False
|
retry = False
|
||||||
while True:
|
while True:
|
||||||
|
|||||||
@@ -406,12 +406,12 @@ def post_process_channel_info(info):
|
|||||||
info['avatar'] = util.prefix_url(info['avatar'])
|
info['avatar'] = util.prefix_url(info['avatar'])
|
||||||
info['channel_url'] = util.prefix_url(info['channel_url'])
|
info['channel_url'] = util.prefix_url(info['channel_url'])
|
||||||
for item in info['items']:
|
for item in info['items']:
|
||||||
# For playlists, use first_video_id for thumbnail, not playlist id
|
# Only set thumbnail if YouTube didn't provide one
|
||||||
|
if not item.get('thumbnail'):
|
||||||
if item.get('type') == 'playlist' and item.get('first_video_id'):
|
if item.get('type') == 'playlist' and item.get('first_video_id'):
|
||||||
item['thumbnail'] = "https://i.ytimg.com/vi/{}/hq720.jpg".format(item['first_video_id'])
|
item['thumbnail'] = "https://i.ytimg.com/vi/{}/hqdefault.jpg".format(item['first_video_id'])
|
||||||
elif item.get('type') == 'video':
|
elif item.get('type') == 'video' and item.get('id'):
|
||||||
item['thumbnail'] = "https://i.ytimg.com/vi/{}/hq720.jpg".format(item['id'])
|
item['thumbnail'] = "https://i.ytimg.com/vi/{}/hqdefault.jpg".format(item['id'])
|
||||||
# For channels and other types, keep existing thumbnail
|
|
||||||
util.prefix_urls(item)
|
util.prefix_urls(item)
|
||||||
util.add_extra_html_info(item)
|
util.add_extra_html_info(item)
|
||||||
if info['current_tab'] == 'about':
|
if info['current_tab'] == 'about':
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ def post_process_comments_info(comments_info):
|
|||||||
util.URL_ORIGIN, '/watch?v=', comments_info['video_id'])
|
util.URL_ORIGIN, '/watch?v=', comments_info['video_id'])
|
||||||
comments_info['video_thumbnail'] = concat_or_none(
|
comments_info['video_thumbnail'] = concat_or_none(
|
||||||
settings.img_prefix, 'https://i.ytimg.com/vi/',
|
settings.img_prefix, 'https://i.ytimg.com/vi/',
|
||||||
comments_info['video_id'], '/hq720.jpg'
|
comments_info['video_id'], '/hqdefault.jpg'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -106,8 +106,8 @@ def get_playlist_page():
|
|||||||
for item in info.get('items', ()):
|
for item in info.get('items', ()):
|
||||||
util.prefix_urls(item)
|
util.prefix_urls(item)
|
||||||
util.add_extra_html_info(item)
|
util.add_extra_html_info(item)
|
||||||
if 'id' in item:
|
if 'id' in item and not item.get('thumbnail'):
|
||||||
item['thumbnail'] = f"{settings.img_prefix}https://i.ytimg.com/vi/{item['id']}/hq720.jpg"
|
item['thumbnail'] = f"{settings.img_prefix}https://i.ytimg.com/vi/{item['id']}/hqdefault.jpg"
|
||||||
|
|
||||||
item['url'] += '&list=' + playlist_id
|
item['url'] += '&list=' + playlist_id
|
||||||
if item['index']:
|
if item['index']:
|
||||||
|
|||||||
@@ -121,11 +121,12 @@ window.addEventListener('DOMContentLoaded', function() {
|
|||||||
* Priority: hq720.jpg -> sddefault.jpg -> hqdefault.jpg -> mqdefault.jpg -> default.jpg
|
* Priority: hq720.jpg -> sddefault.jpg -> hqdefault.jpg -> mqdefault.jpg -> default.jpg
|
||||||
*/
|
*/
|
||||||
function thumbnail_fallback(img) {
|
function thumbnail_fallback(img) {
|
||||||
const src = img.src || img.dataset.src;
|
// Once src is set (image was loaded or attempted), always work with src
|
||||||
|
const src = img.src;
|
||||||
if (!src) return;
|
if (!src) return;
|
||||||
|
|
||||||
// Handle YouTube video thumbnails
|
// Handle YouTube video thumbnails
|
||||||
if (src.includes('/i.ytimg.com/')) {
|
if (src.includes('/i.ytimg.com/') || src.includes('/i.ytimg.com%2F')) {
|
||||||
// Extract video ID from URL
|
// Extract video ID from URL
|
||||||
const match = src.match(/\/vi\/([^/]+)/);
|
const match = src.match(/\/vi\/([^/]+)/);
|
||||||
if (!match) return;
|
if (!match) return;
|
||||||
@@ -138,36 +139,32 @@ function thumbnail_fallback(img) {
|
|||||||
'hq720.jpg',
|
'hq720.jpg',
|
||||||
'sddefault.jpg',
|
'sddefault.jpg',
|
||||||
'hqdefault.jpg',
|
'hqdefault.jpg',
|
||||||
'mqdefault.jpg',
|
|
||||||
'default.jpg'
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// Find current quality and try next fallback
|
// Find current quality and try next fallback
|
||||||
for (let i = 0; i < fallbacks.length; i++) {
|
for (let i = 0; i < fallbacks.length; i++) {
|
||||||
if (src.includes(fallbacks[i])) {
|
if (src.includes(fallbacks[i])) {
|
||||||
// Try next quality
|
|
||||||
if (i < fallbacks.length - 1) {
|
if (i < fallbacks.length - 1) {
|
||||||
const newSrc = imgPrefix + 'https://i.ytimg.com/vi/' + videoId + '/' + fallbacks[i + 1];
|
img.src = imgPrefix + 'https://i.ytimg.com/vi/' + videoId + '/' + fallbacks[i + 1];
|
||||||
if (img.dataset.src) {
|
|
||||||
img.dataset.src = newSrc;
|
|
||||||
} else {
|
} else {
|
||||||
img.src = newSrc;
|
// Last fallback failed, stop retrying
|
||||||
}
|
img.onerror = null;
|
||||||
}
|
}
|
||||||
break;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Unknown quality format, stop retrying
|
||||||
|
img.onerror = null;
|
||||||
}
|
}
|
||||||
// Handle YouTube channel avatars (ggpht.com)
|
// Handle YouTube channel avatars (ggpht.com)
|
||||||
else if (src.includes('ggpht.com') || src.includes('yt3.ggpht.com')) {
|
else if (src.includes('ggpht.com') || src.includes('yt3.ggpht.com')) {
|
||||||
// Try to increase avatar size (s88 -> s240)
|
|
||||||
const newSrc = src.replace(/=s\d+-c-k/, '=s240-c-k-c0x00ffffff-no-rj');
|
const newSrc = src.replace(/=s\d+-c-k/, '=s240-c-k-c0x00ffffff-no-rj');
|
||||||
if (newSrc !== src) {
|
if (newSrc !== src) {
|
||||||
if (img.dataset.src) {
|
|
||||||
img.dataset.src = newSrc;
|
|
||||||
} else {
|
|
||||||
img.src = newSrc;
|
img.src = newSrc;
|
||||||
}
|
} else {
|
||||||
}
|
img.onerror = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
img.onerror = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1089,12 +1089,26 @@ def serve_subscription_thumbnail(thumbnail):
|
|||||||
f.close()
|
f.close()
|
||||||
return flask.Response(image, mimetype='image/jpeg')
|
return flask.Response(image, mimetype='image/jpeg')
|
||||||
|
|
||||||
url = f"https://i.ytimg.com/vi/{video_id}/hq720.jpg"
|
image = None
|
||||||
|
for quality in ('hq720.jpg', 'sddefault.jpg', 'hqdefault.jpg'):
|
||||||
|
url = f"https://i.ytimg.com/vi/{video_id}/{quality}"
|
||||||
try:
|
try:
|
||||||
image = util.fetch_url(url, report_text="Saved thumbnail: " + video_id)
|
image = util.fetch_url(url, report_text="Saved thumbnail: " + video_id)
|
||||||
|
break
|
||||||
|
except util.FetchError as e:
|
||||||
|
if '404' in str(e):
|
||||||
|
continue
|
||||||
|
print("Failed to download thumbnail for " + video_id + ": " + str(e))
|
||||||
|
flask.abort(500)
|
||||||
except urllib.error.HTTPError as e:
|
except urllib.error.HTTPError as e:
|
||||||
|
if e.code == 404:
|
||||||
|
continue
|
||||||
print("Failed to download thumbnail for " + video_id + ": " + str(e))
|
print("Failed to download thumbnail for " + video_id + ": " + str(e))
|
||||||
flask.abort(e.code)
|
flask.abort(e.code)
|
||||||
|
|
||||||
|
if image is None:
|
||||||
|
flask.abort(404)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
f = open(thumbnail_path, 'wb')
|
f = open(thumbnail_path, 'wb')
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
|
|||||||
@@ -542,11 +542,19 @@ class RateLimitedQueue(gevent.queue.Queue):
|
|||||||
|
|
||||||
|
|
||||||
def download_thumbnail(save_directory, video_id):
|
def download_thumbnail(save_directory, video_id):
|
||||||
url = f"https://i.ytimg.com/vi/{video_id}/hq720.jpg"
|
|
||||||
save_location = os.path.join(save_directory, video_id + ".jpg")
|
save_location = os.path.join(save_directory, video_id + ".jpg")
|
||||||
|
for quality in ('hq720.jpg', 'sddefault.jpg', 'hqdefault.jpg'):
|
||||||
|
url = f"https://i.ytimg.com/vi/{video_id}/{quality}"
|
||||||
try:
|
try:
|
||||||
thumbnail = fetch_url(url, report_text="Saved thumbnail: " + video_id)
|
thumbnail = fetch_url(url, report_text="Saved thumbnail: " + video_id)
|
||||||
|
except FetchError as e:
|
||||||
|
if '404' in str(e):
|
||||||
|
continue
|
||||||
|
print("Failed to download thumbnail for " + video_id + ": " + str(e))
|
||||||
|
return False
|
||||||
except urllib.error.HTTPError as e:
|
except urllib.error.HTTPError as e:
|
||||||
|
if e.code == 404:
|
||||||
|
continue
|
||||||
print("Failed to download thumbnail for " + video_id + ": " + str(e))
|
print("Failed to download thumbnail for " + video_id + ": " + str(e))
|
||||||
return False
|
return False
|
||||||
try:
|
try:
|
||||||
@@ -557,6 +565,8 @@ def download_thumbnail(save_directory, video_id):
|
|||||||
f.write(thumbnail)
|
f.write(thumbnail)
|
||||||
f.close()
|
f.close()
|
||||||
return True
|
return True
|
||||||
|
print("No thumbnail available for " + video_id)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def download_thumbnails(save_directory, ids):
|
def download_thumbnails(save_directory, ids):
|
||||||
|
|||||||
@@ -628,12 +628,12 @@ def get_watch_page(video_id=None):
|
|||||||
|
|
||||||
# prefix urls, and other post-processing not handled by yt_data_extract
|
# prefix urls, and other post-processing not handled by yt_data_extract
|
||||||
for item in info['related_videos']:
|
for item in info['related_videos']:
|
||||||
# For playlists, use first_video_id for thumbnail, not playlist id
|
# Only set thumbnail if YouTube didn't provide one
|
||||||
|
if not item.get('thumbnail'):
|
||||||
if item.get('type') == 'playlist' and item.get('first_video_id'):
|
if item.get('type') == 'playlist' and item.get('first_video_id'):
|
||||||
item['thumbnail'] = "https://i.ytimg.com/vi/{}/hq720.jpg".format(item['first_video_id'])
|
item['thumbnail'] = "https://i.ytimg.com/vi/{}/hqdefault.jpg".format(item['first_video_id'])
|
||||||
elif item.get('type') == 'video':
|
elif item.get('type') == 'video' and item.get('id'):
|
||||||
item['thumbnail'] = "https://i.ytimg.com/vi/{}/hq720.jpg".format(item['id'])
|
item['thumbnail'] = "https://i.ytimg.com/vi/{}/hqdefault.jpg".format(item['id'])
|
||||||
# For other types, keep existing thumbnail or skip
|
|
||||||
util.prefix_urls(item)
|
util.prefix_urls(item)
|
||||||
util.add_extra_html_info(item)
|
util.add_extra_html_info(item)
|
||||||
for song in info['music_list']:
|
for song in info['music_list']:
|
||||||
@@ -641,9 +641,9 @@ def get_watch_page(video_id=None):
|
|||||||
if info['playlist']:
|
if info['playlist']:
|
||||||
playlist_id = info['playlist']['id']
|
playlist_id = info['playlist']['id']
|
||||||
for item in info['playlist']['items']:
|
for item in info['playlist']['items']:
|
||||||
# Set high quality thumbnail for playlist videos
|
# Only set thumbnail if YouTube didn't provide one
|
||||||
if item.get('type') == 'video' and item.get('id'):
|
if not item.get('thumbnail') and item.get('type') == 'video' and item.get('id'):
|
||||||
item['thumbnail'] = "https://i.ytimg.com/vi/{}/hq720.jpg".format(item['id'])
|
item['thumbnail'] = "https://i.ytimg.com/vi/{}/hqdefault.jpg".format(item['id'])
|
||||||
util.prefix_urls(item)
|
util.prefix_urls(item)
|
||||||
util.add_extra_html_info(item)
|
util.add_extra_html_info(item)
|
||||||
if playlist_id:
|
if playlist_id:
|
||||||
|
|||||||
@@ -369,9 +369,9 @@ def extract_item_info(item, additional_info={}):
|
|||||||
['detailedMetadataSnippets', 0, 'snippetText'],
|
['detailedMetadataSnippets', 0, 'snippetText'],
|
||||||
))
|
))
|
||||||
info['thumbnail'] = normalize_url(multi_deep_get(item,
|
info['thumbnail'] = normalize_url(multi_deep_get(item,
|
||||||
['thumbnail', 'thumbnails', 0, 'url'], # videos
|
['thumbnail', 'thumbnails', -1, 'url'], # videos (highest quality)
|
||||||
['thumbnails', 0, 'thumbnails', 0, 'url'], # playlists
|
['thumbnails', 0, 'thumbnails', -1, 'url'], # playlists
|
||||||
['thumbnailRenderer', 'showCustomThumbnailRenderer', 'thumbnail', 'thumbnails', 0, 'url'], # shows
|
['thumbnailRenderer', 'showCustomThumbnailRenderer', 'thumbnail', 'thumbnails', -1, 'url'], # shows
|
||||||
))
|
))
|
||||||
|
|
||||||
info['badges'] = []
|
info['badges'] = []
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ def extract_playlist_metadata(polymer_json):
|
|||||||
if metadata['first_video_id'] is None:
|
if metadata['first_video_id'] is None:
|
||||||
metadata['thumbnail'] = None
|
metadata['thumbnail'] = None
|
||||||
else:
|
else:
|
||||||
metadata['thumbnail'] = f"https://i.ytimg.com/vi/{metadata['first_video_id']}/hq720.jpg"
|
metadata['thumbnail'] = f"https://i.ytimg.com/vi/{metadata['first_video_id']}/hqdefault.jpg"
|
||||||
|
|
||||||
metadata['video_count'] = extract_int(header.get('numVideosText'))
|
metadata['video_count'] = extract_int(header.get('numVideosText'))
|
||||||
metadata['description'] = extract_str(header.get('descriptionText'), default='')
|
metadata['description'] = extract_str(header.get('descriptionText'), default='')
|
||||||
|
|||||||
Reference in New Issue
Block a user