aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--server.py1
-rw-r--r--youtube/channel.py12
-rw-r--r--youtube/comments.py2
-rw-r--r--youtube/playlist.py4
-rw-r--r--youtube/static/js/common.js33
-rw-r--r--youtube/subscriptions.py26
-rw-r--r--youtube/util.py38
-rw-r--r--youtube/watch.py18
-rw-r--r--youtube/yt_data_extract/common.py6
-rw-r--r--youtube/yt_data_extract/everything_else.py2
10 files changed, 81 insertions, 61 deletions
diff --git a/server.py b/server.py
index 8da5411..eadcffe 100644
--- a/server.py
+++ b/server.py
@@ -99,7 +99,6 @@ def proxy_site(env, start_response, video=False):
if response.status >= 400:
print('Error: YouTube returned "%d %s" while routing %s' % (
response.status, response.reason, url.split('?')[0]))
-
total_received = 0
retry = False
while True:
diff --git a/youtube/channel.py b/youtube/channel.py
index 72fac07..55c1124 100644
--- a/youtube/channel.py
+++ b/youtube/channel.py
@@ -406,12 +406,12 @@ def post_process_channel_info(info):
info['avatar'] = util.prefix_url(info['avatar'])
info['channel_url'] = util.prefix_url(info['channel_url'])
for item in info['items']:
- # For playlists, use first_video_id for thumbnail, not playlist 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'])
- elif item.get('type') == 'video':
- item['thumbnail'] = "https://i.ytimg.com/vi/{}/hq720.jpg".format(item['id'])
- # For channels and other types, keep existing thumbnail
+ # 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'):
+ item['thumbnail'] = "https://i.ytimg.com/vi/{}/hqdefault.jpg".format(item['first_video_id'])
+ elif item.get('type') == 'video' and item.get('id'):
+ item['thumbnail'] = "https://i.ytimg.com/vi/{}/hqdefault.jpg".format(item['id'])
util.prefix_urls(item)
util.add_extra_html_info(item)
if info['current_tab'] == 'about':
diff --git a/youtube/comments.py b/youtube/comments.py
index 5e40b14..1ff1a21 100644
--- a/youtube/comments.py
+++ b/youtube/comments.py
@@ -150,7 +150,7 @@ def post_process_comments_info(comments_info):
util.URL_ORIGIN, '/watch?v=', comments_info['video_id'])
comments_info['video_thumbnail'] = concat_or_none(
settings.img_prefix, 'https://i.ytimg.com/vi/',
- comments_info['video_id'], '/hq720.jpg'
+ comments_info['video_id'], '/hqdefault.jpg'
)
diff --git a/youtube/playlist.py b/youtube/playlist.py
index 2765a30..bedf2d2 100644
--- a/youtube/playlist.py
+++ b/youtube/playlist.py
@@ -106,8 +106,8 @@ def get_playlist_page():
for item in info.get('items', ()):
util.prefix_urls(item)
util.add_extra_html_info(item)
- if 'id' in item:
- item['thumbnail'] = f"{settings.img_prefix}https://i.ytimg.com/vi/{item['id']}/hq720.jpg"
+ if 'id' in item and not item.get('thumbnail'):
+ item['thumbnail'] = f"{settings.img_prefix}https://i.ytimg.com/vi/{item['id']}/hqdefault.jpg"
item['url'] += '&list=' + playlist_id
if item['index']:
diff --git a/youtube/static/js/common.js b/youtube/static/js/common.js
index bcd1539..ac4413b 100644
--- a/youtube/static/js/common.js
+++ b/youtube/static/js/common.js
@@ -121,11 +121,12 @@ window.addEventListener('DOMContentLoaded', function() {
* Priority: hq720.jpg -> sddefault.jpg -> hqdefault.jpg -> mqdefault.jpg -> default.jpg
*/
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;
// 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
const match = src.match(/\/vi\/([^/]+)/);
if (!match) return;
@@ -138,36 +139,32 @@ function thumbnail_fallback(img) {
'hq720.jpg',
'sddefault.jpg',
'hqdefault.jpg',
- 'mqdefault.jpg',
- 'default.jpg'
];
// Find current quality and try next fallback
for (let i = 0; i < fallbacks.length; i++) {
if (src.includes(fallbacks[i])) {
- // Try next quality
if (i < fallbacks.length - 1) {
- const newSrc = imgPrefix + 'https://i.ytimg.com/vi/' + videoId + '/' + fallbacks[i + 1];
- if (img.dataset.src) {
- img.dataset.src = newSrc;
- } else {
- img.src = newSrc;
- }
+ img.src = imgPrefix + 'https://i.ytimg.com/vi/' + videoId + '/' + fallbacks[i + 1];
+ } else {
+ // Last fallback failed, stop retrying
+ img.onerror = null;
}
- break;
+ return;
}
}
+ // Unknown quality format, stop retrying
+ img.onerror = null;
}
// Handle YouTube channel avatars (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');
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;
}
}
diff --git a/youtube/subscriptions.py b/youtube/subscriptions.py
index 0cb5e95..3326a51 100644
--- a/youtube/subscriptions.py
+++ b/youtube/subscriptions.py
@@ -1089,12 +1089,26 @@ def serve_subscription_thumbnail(thumbnail):
f.close()
return flask.Response(image, mimetype='image/jpeg')
- url = f"https://i.ytimg.com/vi/{video_id}/hq720.jpg"
- try:
- image = util.fetch_url(url, report_text="Saved thumbnail: " + video_id)
- except urllib.error.HTTPError as e:
- print("Failed to download thumbnail for " + video_id + ": " + str(e))
- flask.abort(e.code)
+ image = None
+ for quality in ('hq720.jpg', 'sddefault.jpg', 'hqdefault.jpg'):
+ url = f"https://i.ytimg.com/vi/{video_id}/{quality}"
+ try:
+ 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:
+ if e.code == 404:
+ continue
+ print("Failed to download thumbnail for " + video_id + ": " + str(e))
+ flask.abort(e.code)
+
+ if image is None:
+ flask.abort(404)
+
try:
f = open(thumbnail_path, 'wb')
except FileNotFoundError:
diff --git a/youtube/util.py b/youtube/util.py
index e6f1961..ae948ae 100644
--- a/youtube/util.py
+++ b/youtube/util.py
@@ -542,21 +542,31 @@ class RateLimitedQueue(gevent.queue.Queue):
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")
- try:
- thumbnail = fetch_url(url, report_text="Saved thumbnail: " + video_id)
- except urllib.error.HTTPError as e:
- print("Failed to download thumbnail for " + video_id + ": " + str(e))
- return False
- try:
- f = open(save_location, 'wb')
- except FileNotFoundError:
- os.makedirs(save_directory, exist_ok=True)
- f = open(save_location, 'wb')
- f.write(thumbnail)
- f.close()
- return True
+ for quality in ('hq720.jpg', 'sddefault.jpg', 'hqdefault.jpg'):
+ url = f"https://i.ytimg.com/vi/{video_id}/{quality}"
+ try:
+ 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:
+ if e.code == 404:
+ continue
+ print("Failed to download thumbnail for " + video_id + ": " + str(e))
+ return False
+ try:
+ f = open(save_location, 'wb')
+ except FileNotFoundError:
+ os.makedirs(save_directory, exist_ok=True)
+ f = open(save_location, 'wb')
+ f.write(thumbnail)
+ f.close()
+ return True
+ print("No thumbnail available for " + video_id)
+ return False
def download_thumbnails(save_directory, ids):
diff --git a/youtube/watch.py b/youtube/watch.py
index 14f1dae..b76a462 100644
--- a/youtube/watch.py
+++ b/youtube/watch.py
@@ -628,12 +628,12 @@ def get_watch_page(video_id=None):
# prefix urls, and other post-processing not handled by yt_data_extract
for item in info['related_videos']:
- # For playlists, use first_video_id for thumbnail, not playlist 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'])
- elif item.get('type') == 'video':
- item['thumbnail'] = "https://i.ytimg.com/vi/{}/hq720.jpg".format(item['id'])
- # For other types, keep existing thumbnail or skip
+ # 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'):
+ item['thumbnail'] = "https://i.ytimg.com/vi/{}/hqdefault.jpg".format(item['first_video_id'])
+ elif item.get('type') == 'video' and item.get('id'):
+ item['thumbnail'] = "https://i.ytimg.com/vi/{}/hqdefault.jpg".format(item['id'])
util.prefix_urls(item)
util.add_extra_html_info(item)
for song in info['music_list']:
@@ -641,9 +641,9 @@ def get_watch_page(video_id=None):
if info['playlist']:
playlist_id = info['playlist']['id']
for item in info['playlist']['items']:
- # Set high quality thumbnail for playlist videos
- if item.get('type') == 'video' and item.get('id'):
- item['thumbnail'] = "https://i.ytimg.com/vi/{}/hq720.jpg".format(item['id'])
+ # Only set thumbnail if YouTube didn't provide one
+ if not item.get('thumbnail') and item.get('type') == 'video' and item.get('id'):
+ item['thumbnail'] = "https://i.ytimg.com/vi/{}/hqdefault.jpg".format(item['id'])
util.prefix_urls(item)
util.add_extra_html_info(item)
if playlist_id:
diff --git a/youtube/yt_data_extract/common.py b/youtube/yt_data_extract/common.py
index 6a98280..7d44fae 100644
--- a/youtube/yt_data_extract/common.py
+++ b/youtube/yt_data_extract/common.py
@@ -369,9 +369,9 @@ def extract_item_info(item, additional_info={}):
['detailedMetadataSnippets', 0, 'snippetText'],
))
info['thumbnail'] = normalize_url(multi_deep_get(item,
- ['thumbnail', 'thumbnails', 0, 'url'], # videos
- ['thumbnails', 0, 'thumbnails', 0, 'url'], # playlists
- ['thumbnailRenderer', 'showCustomThumbnailRenderer', 'thumbnail', 'thumbnails', 0, 'url'], # shows
+ ['thumbnail', 'thumbnails', -1, 'url'], # videos (highest quality)
+ ['thumbnails', 0, 'thumbnails', -1, 'url'], # playlists
+ ['thumbnailRenderer', 'showCustomThumbnailRenderer', 'thumbnail', 'thumbnails', -1, 'url'], # shows
))
info['badges'] = []
diff --git a/youtube/yt_data_extract/everything_else.py b/youtube/yt_data_extract/everything_else.py
index 1f5b6a2..0f64649 100644
--- a/youtube/yt_data_extract/everything_else.py
+++ b/youtube/yt_data_extract/everything_else.py
@@ -229,7 +229,7 @@ def extract_playlist_metadata(polymer_json):
if metadata['first_video_id'] is None:
metadata['thumbnail'] = None
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['description'] = extract_str(header.get('descriptionText'), default='')