Files
jacuzzi/frontend/js/state.js
Simon 6dac1028a3 Take a thumbnail out of the video when the listed one is gone
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
2026-10-20 07:33:38 +00:00

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;
};