Cache video thumbnails for offline use
This commit is contained in:
@@ -697,6 +697,44 @@ async function refreshCachedIds() {
|
||||
} catch { /* shell without cache support — leave empty */ }
|
||||
}
|
||||
|
||||
// ---------- Offline thumbnail warm-up ----------
|
||||
// Saving a video stores only its media; its artwork lives on i.ytimg.com and
|
||||
// is only cached once something has actually requested it. Pull it through the
|
||||
// service worker (which caches thumbnail responses) so a video saved for
|
||||
// offline still has its art when the network is gone.
|
||||
function warmThumb(url) {
|
||||
if (!WEB || !url || !/^https?:/i.test(url)) return;
|
||||
fetch(url, { mode: 'cors', credentials: 'omit' }).catch(() => {});
|
||||
}
|
||||
|
||||
function thumbUrlFor(id, known) {
|
||||
if (known && known.thumbnail) return known.thumbnail;
|
||||
if (String(id).startsWith('edit_')) return ''; // edited copies have no YouTube art
|
||||
return `https://i.ytimg.com/vi/${id}/mqdefault.jpg`;
|
||||
}
|
||||
|
||||
// One-shot backfill on launch: everything already saved offline, plus every
|
||||
// playlist video (the home screen and sidebar render those). Cached thumbs are
|
||||
// served by the SW without touching the network, so only the first run costs
|
||||
// anything. Bounded and lightly throttled so a big library can't burst.
|
||||
const THUMB_WARM_MAX = 400;
|
||||
async function warmOfflineThumbs() {
|
||||
if (!WEB || navigator.onLine === false) return;
|
||||
const urls = new Set();
|
||||
for (const id of cachedIds) {
|
||||
const u = thumbUrlFor(id, videoById(id));
|
||||
if (u) urls.add(u);
|
||||
}
|
||||
for (const pl of data.playlists || []) {
|
||||
for (const v of pl.videos || []) if (v && v.thumbnail) urls.add(v.thumbnail);
|
||||
}
|
||||
const list = [...urls].slice(0, THUMB_WARM_MAX);
|
||||
for (let i = 0; i < list.length; i += 4) {
|
||||
await Promise.all(list.slice(i, i + 4).map((u) =>
|
||||
fetch(u, { mode: 'cors', credentials: 'omit' }).catch(() => {})));
|
||||
}
|
||||
}
|
||||
|
||||
// Download a video into the permanent offline cache. Safe to call repeatedly.
|
||||
async function preload(video, { quiet = false, mux = false } = {}) {
|
||||
const id = video.id;
|
||||
@@ -719,6 +757,7 @@ async function preload(video, { quiet = false, mux = false } = {}) {
|
||||
const res = await API.cacheDownload(id, { mux });
|
||||
if (res && res.ok && res.cached) {
|
||||
cachedIds.add(id);
|
||||
warmThumb(thumbUrlFor(id, video));
|
||||
if (!quiet) toast(`Saved “${video.title}” ✓`);
|
||||
} else if (!quiet) {
|
||||
toast('⚠ ' + ((res && res.error) || 'Could not save video'));
|
||||
@@ -802,6 +841,9 @@ async function downloadEdited(customVideo) {
|
||||
const res = await API.cacheDownloadEdited(id, customVideo.sourceId, customVideo.keep);
|
||||
if (res && res.ok && res.cached) {
|
||||
cachedIds.add(id);
|
||||
// An edited copy has no art of its own — warm its source's thumbnail,
|
||||
// which is what the Saved list falls back to.
|
||||
warmThumb(thumbUrlFor(customVideo.sourceId, videoById(customVideo.sourceId)));
|
||||
// Only persist the custom video once its media is actually stored, so a
|
||||
// failed render never leaves a dangling entry the user can't play.
|
||||
if (!(data.customVideos || []).some((v) => v.id === id)) {
|
||||
@@ -4156,6 +4198,8 @@ async function boot() {
|
||||
// Learn what's already cached, then top up any playlist videos that aren't.
|
||||
await refreshCachedIds();
|
||||
data.playlists.forEach(preloadPlaylist);
|
||||
// Not awaited — artwork backfill must never delay first paint.
|
||||
warmOfflineThumbs();
|
||||
|
||||
renderSmartSidebar();
|
||||
checkAutoBackup();
|
||||
|
||||
Reference in New Issue
Block a user