Some checks failed
CI / test (push) Failing after 1m19s
Major Features: - HD video thumbnails (hq720.jpg) with automatic fallback to lower qualities - HD channel avatars (240x240 instead of 88x88) - YouTube 2024+ lockupViewModel support for channel playlists - youtubei/v1/browse API integration for channel playlist tabs - yt-dlp integration for multi-language audio and subtitles Bug Fixes: - Fixed undefined `abort` import in playlist.py - Fixed undefined functions in proto.py (encode_varint, bytes_to_hex, succinct_encode) - Fixed missing `traceback` import in proto_debug.py - Fixed blurry playlist thumbnails using default.jpg instead of HD versions - Fixed channel playlists page using deprecated pbj=1 format Improvements: - Automatic thumbnail fallback system (hq720 → sddefault → hqdefault → mqdefault → default) - JavaScript thumbnail_fallback() handler for 404 errors - Better thumbnail quality across all pages (watch, channel, playlist, subscriptions) - Consistent HD avatar display for all channel items - Settings system automatically adds new settings without breaking user config Files Modified: - youtube/watch.py - HD thumbnails for related videos and playlist items - youtube/channel.py - HD thumbnails for channel playlists, youtubei API integration - youtube/playlist.py - HD thumbnails, fixed abort import - youtube/util.py - HD thumbnail URLs, avatar HD upgrade, prefix_url improvements - youtube/comments.py - HD video thumbnail - youtube/subscriptions.py - HD thumbnails, fixed abort import - youtube/yt_data_extract/common.py - lockupViewModel support, extract_lockup_view_model_info() - youtube/yt_data_extract/everything_else.py - HD playlist thumbnails - youtube/proto.py - Fixed undefined function references - youtube/proto_debug.py - Added traceback import - youtube/static/js/common.js - thumbnail_fallback() handler - youtube/templates/*.html - Added onerror handlers for thumbnail fallback - youtube/version.py - Bump to v0.4.0 Technical Details: - All thumbnail URLs now use hq720.jpg (1280x720) when available - Fallback handled client-side via JavaScript onerror handler - Server-side avatar upgrade via regex in util.prefix_url() - lockupViewModel parser extracts contentType, metadata, and first_video_id - Channel playlist tabs now use youtubei/v1/browse instead of deprecated pbj=1 - Settings version system ensures backward compatibility
174 lines
5.3 KiB
JavaScript
174 lines
5.3 KiB
JavaScript
const Q = document.querySelector.bind(document);
|
|
const QA = document.querySelectorAll.bind(document);
|
|
const QId = document.getElementById.bind(document);
|
|
let seconds,
|
|
minutes,
|
|
hours;
|
|
function text(msg) { return document.createTextNode(msg); }
|
|
function clearNode(node) { while (node.firstChild) node.removeChild(node.firstChild); }
|
|
function toTimestamp(seconds) {
|
|
seconds = Math.floor(seconds);
|
|
|
|
minutes = Math.floor(seconds/60);
|
|
seconds = seconds % 60;
|
|
|
|
hours = Math.floor(minutes/60);
|
|
minutes = minutes % 60;
|
|
|
|
if (hours) {
|
|
return `0${hours}:`.slice(-3) + `0${minutes}:`.slice(-3) + `0${seconds}`.slice(-2);
|
|
}
|
|
return `0${minutes}:`.slice(-3) + `0${seconds}`.slice(-2);
|
|
}
|
|
|
|
let cur_track_idx = 0;
|
|
function getActiveTranscriptTrackIdx() {
|
|
let textTracks = QId("js-video-player").textTracks;
|
|
if (!textTracks.length) return;
|
|
for (let i=0; i < textTracks.length; i++) {
|
|
if (textTracks[i].mode == "showing") {
|
|
cur_track_idx = i;
|
|
return cur_track_idx;
|
|
}
|
|
}
|
|
return cur_track_idx;
|
|
}
|
|
function getActiveTranscriptTrack() { return QId("js-video-player").textTracks[getActiveTranscriptTrackIdx()]; }
|
|
|
|
function getDefaultTranscriptTrackIdx() {
|
|
let textTracks = QId("js-video-player").textTracks;
|
|
return textTracks.length - 1;
|
|
}
|
|
|
|
function doXhr(url, callback=null) {
|
|
let xhr = new XMLHttpRequest();
|
|
xhr.open("GET", url);
|
|
xhr.onload = (e) => {
|
|
callback(e.currentTarget.response);
|
|
}
|
|
xhr.send();
|
|
return xhr;
|
|
}
|
|
|
|
// https://stackoverflow.com/a/30810322
|
|
function copyTextToClipboard(text) {
|
|
let textArea = document.createElement("textarea");
|
|
|
|
//
|
|
// *** This styling is an extra step which is likely not required. ***
|
|
//
|
|
// Why is it here? To ensure:
|
|
// 1. the element is able to have focus and selection.
|
|
// 2. if element was to flash render it has minimal visual impact.
|
|
// 3. less flakyness with selection and copying which **might** occur if
|
|
// the textarea element is not visible.
|
|
//
|
|
// The likelihood is the element won't even render, not even a
|
|
// flash, so some of these are just precautions. However in
|
|
// Internet Explorer the element is visible whilst the popup
|
|
// box asking the user for permission for the web page to
|
|
// copy to the clipboard.
|
|
//
|
|
|
|
// Place in top-left corner of screen regardless of scroll position.
|
|
textArea.style.position = 'fixed';
|
|
textArea.style.top = 0;
|
|
textArea.style.left = 0;
|
|
|
|
// Ensure it has a small width and height. Setting to 1px / 1em
|
|
// doesn't work as this gives a negative w/h on some browsers.
|
|
textArea.style.width = '2em';
|
|
textArea.style.height = '2em';
|
|
|
|
// We don't need padding, reducing the size if it does flash render.
|
|
textArea.style.padding = 0;
|
|
|
|
// Clean up any borders.
|
|
textArea.style.border = 'none';
|
|
textArea.style.outline = 'none';
|
|
textArea.style.boxShadow = 'none';
|
|
|
|
// Avoid flash of white box if rendered for any reason.
|
|
textArea.style.background = 'transparent';
|
|
|
|
|
|
textArea.value = text;
|
|
|
|
let parent_el = video.parentElement;
|
|
parent_el.appendChild(textArea);
|
|
textArea.focus();
|
|
textArea.select();
|
|
|
|
try {
|
|
let successful = document.execCommand('copy');
|
|
let msg = successful ? 'successful' : 'unsuccessful';
|
|
console.log('Copying text command was ' + msg);
|
|
} catch (err) {
|
|
console.log('Oops, unable to copy');
|
|
}
|
|
|
|
parent_el.removeChild(textArea);
|
|
}
|
|
|
|
|
|
window.addEventListener('DOMContentLoaded', function() {
|
|
cur_track_idx = getDefaultTranscriptTrackIdx();
|
|
});
|
|
|
|
/**
|
|
* Thumbnail fallback handler
|
|
* Tries lower quality thumbnails when higher quality fails (404)
|
|
* Priority: hq720.jpg -> sddefault.jpg -> hqdefault.jpg -> mqdefault.jpg -> default.jpg
|
|
*/
|
|
function thumbnail_fallback(img) {
|
|
const src = img.src || img.dataset.src;
|
|
if (!src) return;
|
|
|
|
// Handle YouTube video thumbnails
|
|
if (src.includes('/i.ytimg.com/')) {
|
|
// Extract video ID from URL
|
|
const match = src.match(/\/vi\/([^/]+)/);
|
|
if (!match) return;
|
|
|
|
const videoId = match[1];
|
|
const imgPrefix = settings_img_prefix || '';
|
|
|
|
// Define fallback order (from highest to lowest quality)
|
|
const fallbacks = [
|
|
'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;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
// 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;
|
|
}
|
|
}
|
|
}
|
|
}
|