A favorite outlives its thumbnail. Sources sign their CDN paths and the signature expires, so a video kept months ago has a `thumb` that 404s and a card that is a grey box with a title under it. Asking the source page for a fresh picture covers the case where the page still exists; nothing covered the case where it doesn't. The video does, because playback re-resolves from the page URL every time. So once every route to the listed picture is exhausted, a frame a quarter of the way in becomes the card's picture -- past the black lead-in, and a fair guess at what the video looks like. It goes through our own proxy, so the canvas can be read back, is scaled to card width and JPEG'd to a handful of KB, and is kept: later mounts and later sessions paint it without a request, instead of asking again for a picture that is known to be gone. Only for favorites, once per item, one at a time, and never for a live stream or a manifest. Two things worth knowing, both found with the browser rather than reasoned about: a browser fires `seeked` for its own initial seek to zero, so taking the frame on the first one captures the opening of every video; and the same item is on screen in more than one place, so the captures are deduped by a promise rather than the first asker getting the picture and the rest being told there isn't one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MPZFnNdHbPGDTqQUNiE4ZN
54 lines
1.8 KiB
JavaScript
54 lines
1.8 KiB
JavaScript
window.App = window.App || {};
|
|
|
|
// Centralized runtime state for pagination, player, and UI behavior.
|
|
App.state = {
|
|
currentPage: 1,
|
|
perPage: 12,
|
|
renderedVideoIds: new Set(),
|
|
hasNextPage: true,
|
|
isLoading: false,
|
|
hlsPlayer: null,
|
|
currentLoadController: null,
|
|
errorToastTimer: null,
|
|
loadedVideos: [],
|
|
feedOpen: false,
|
|
feedMuted: true,
|
|
feedActiveIndex: -1,
|
|
feedActiveVideoId: null,
|
|
groupCursors: null
|
|
};
|
|
|
|
// Local storage keys used across modules.
|
|
App.constants = {
|
|
FAVORITES_KEY: 'favorites',
|
|
FAVORITES_VISIBILITY_KEY: 'favoritesVisible',
|
|
FAVORITES_SORT_KEY: 'favoritesSort',
|
|
PREFERRED_QUALITY_KEY: 'preferredQuality',
|
|
FEED_END_BEHAVIOR_KEY: 'feedEndBehavior',
|
|
SEARCH_HISTORY_KEY: 'searchHistory',
|
|
SAVED_SEARCHES_KEY: 'savedSearches',
|
|
// Thumbnails captured out of a video whose listed picture is gone:
|
|
// the index, plus one entry per picture under `<key>:<hash>`.
|
|
POSTER_CACHE_KEY: 'posterCache'
|
|
};
|
|
|
|
// Lazily injects hls.js the first time a stream actually needs it. Sessions
|
|
// that only browse thumbnails, or that play native/MP4, never download it.
|
|
// Resolves with window.Hls (or null if loading failed).
|
|
App.ensureHls = function() {
|
|
if (window.Hls) return Promise.resolve(window.Hls);
|
|
if (App._hlsPromise) return App._hlsPromise;
|
|
App._hlsPromise = new Promise((resolve, reject) => {
|
|
const script = document.createElement('script');
|
|
script.src = 'https://cdn.jsdelivr.net/npm/hls.js@1.5';
|
|
script.async = true;
|
|
script.onload = () => resolve(window.Hls || null);
|
|
script.onerror = () => {
|
|
App._hlsPromise = null;
|
|
reject(new Error('Failed to load hls.js'));
|
|
};
|
|
document.head.appendChild(script);
|
|
});
|
|
return App._hlsPromise;
|
|
};
|