Still-picture songs save and play as audio plus one picture
This commit is contained in:
@@ -52,6 +52,12 @@
|
||||
|
||||
const _blobUrls = new Set();
|
||||
|
||||
// Pictures of still-picture songs saved as audio: art/<videoId>.jpg
|
||||
async function getArtDir() {
|
||||
const root = await navigator.storage.getDirectory();
|
||||
return root.getDirectoryHandle('art', { create: true });
|
||||
}
|
||||
|
||||
window.OPFS = {
|
||||
// Does a file exist for this videoId?
|
||||
async hasVideo(videoId) {
|
||||
@@ -84,6 +90,50 @@
|
||||
}
|
||||
},
|
||||
|
||||
// The saved file's name (<id>.<ext>), or null — an .m4a for a YouTube id
|
||||
// is a still-picture song saved as audio + picture.
|
||||
async fileName(videoId) {
|
||||
try {
|
||||
const found = await findHandle(videoId);
|
||||
return found ? found[1] : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
// Store a still-picture song's picture. Main-thread writable first, else
|
||||
// the worker's sync handle (Safari has no createWritable).
|
||||
async saveArt(videoId, buffer) {
|
||||
const dir = await getArtDir();
|
||||
const handle = await dir.getFileHandle(videoId + '.jpg', { create: true });
|
||||
if ('createWritable' in handle) {
|
||||
const w = await handle.createWritable();
|
||||
await w.write(buffer);
|
||||
await w.close();
|
||||
return;
|
||||
}
|
||||
await new Promise((resolve, reject) => {
|
||||
const w = window.Lazy ? window.Lazy.worker('/opfs-worker.js') : new Worker('/opfs-worker.js');
|
||||
w.onmessage = (e) => { w.terminate(); (e.data && e.data.type === 'written') ? resolve() : reject(new Error((e.data && e.data.error) || 'write failed')); };
|
||||
w.onerror = () => { w.terminate(); reject(new Error('worker failed')); };
|
||||
w.postMessage({ op: 'write', dir: 'art', name: videoId + '.jpg', buffer }, [buffer]);
|
||||
});
|
||||
},
|
||||
|
||||
// blob: URL of a saved picture, or null. Revoke with revokeUrl().
|
||||
async getArtUrl(videoId) {
|
||||
try {
|
||||
const dir = await getArtDir();
|
||||
const file = await (await dir.getFileHandle(videoId + '.jpg')).getFile();
|
||||
if (!file.size) return null;
|
||||
const url = URL.createObjectURL(new Blob([file], { type: 'image/jpeg' }));
|
||||
_blobUrls.add(url);
|
||||
return url;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
// Revoke an object URL previously returned by getFileUrl().
|
||||
// 'hevc' | 'avc' | null for a stored mp4. Server copies are faststart, so
|
||||
// the sample description (hvc1/hev1 vs avc1) sits in the first megabyte.
|
||||
@@ -318,6 +368,7 @@
|
||||
const dir = await getRoot();
|
||||
await dir.removeEntry(name);
|
||||
} catch { /* already gone */ }
|
||||
try { await (await getArtDir()).removeEntry(videoId + '.jpg'); } catch { /* no picture */ }
|
||||
},
|
||||
|
||||
// Delete all cached videos
|
||||
@@ -327,6 +378,7 @@
|
||||
const names = [];
|
||||
for await (const [name] of dir.entries()) names.push(name);
|
||||
await Promise.all(names.map((n) => dir.removeEntry(n).catch(() => {})));
|
||||
try { await (await navigator.storage.getDirectory()).removeEntry('art', { recursive: true }); } catch { /* no pictures */ }
|
||||
// Revoke any outstanding blob URLs
|
||||
for (const url of _blobUrls) URL.revokeObjectURL(url);
|
||||
_blobUrls.clear();
|
||||
|
||||
Reference in New Issue
Block a user