Files
yt-local/youtube/static/js/common.js
Astounds 56ecd6cb1b
All checks were successful
git-sync-with-mirror / git-sync (push) Successful in 15s
CI / test (push) Successful in 58s
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.
2026-03-27 19:22:12 -05:00

171 lines
5.2 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) {
// 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/') || src.includes('/i.ytimg.com%2F')) {
// 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',
];
// Find current quality and try next fallback
for (let i = 0; i < fallbacks.length; i++) {
if (src.includes(fallbacks[i])) {
if (i < fallbacks.length - 1) {
img.src = imgPrefix + 'https://i.ytimg.com/vi/' + videoId + '/' + fallbacks[i + 1];
} else {
// Last fallback failed, stop retrying
img.onerror = null;
}
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')) {
const newSrc = src.replace(/=s\d+-c-k/, '=s240-c-k-c0x00ffffff-no-rj');
if (newSrc !== src) {
img.src = newSrc;
} else {
img.onerror = null;
}
} else {
img.onerror = null;
}
}