/* ============================================================================ * device-db.js — this device's own file registry (IndexedDB "ytp-device") * * One record per saved video, next to the bytes in OPFS: * { videoId, cid, size, savedAt, lastCheckedAt, state } * cid SHA-256 of the stored file (P2P content id) or null * state 'verified' hash computed on save and equal to the server's * 'unverified' hash computed, but the server sent none to compare * 'unhashed' saved before hashing existed / main-thread fallback * The P2P client (p2p-client.js) reads this to report holdings; OPFS stays the * source of truth for what is playable (a record without a file is ignored). * Every call resolves (null / [] on failure) — private windows can block IDB. * See docs/p2p-architecture.md. * ========================================================================== */ (function () { 'use strict'; const DB_NAME = 'ytp-device'; const VERSION = 1; let _open = null; function open() { if (!_open) { _open = new Promise((resolve, reject) => { const r = indexedDB.open(DB_NAME, VERSION); r.onupgradeneeded = () => { const d = r.result; if (!d.objectStoreNames.contains('files')) { const s = d.createObjectStore('files', { keyPath: 'videoId' }); s.createIndex('cid', 'cid', { unique: false }); } }; r.onsuccess = () => resolve(r.result); r.onerror = () => reject(r.error); r.onblocked = () => reject(new Error('device-db blocked')); }); _open.catch(() => { _open = null; }); } return _open; } const done = (req) => new Promise((resolve, reject) => { req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); }); async function store(mode) { const d = await open(); return d.transaction('files', mode).objectStore('files'); } async function safe(fn, fallback) { try { return await fn(); } catch { return fallback; } } window.DeviceDB = { isSupported: () => typeof indexedDB !== 'undefined', putFile: (rec) => safe(async () => { if (!rec || !rec.videoId) return null; await done((await store('readwrite')).put({ videoId: String(rec.videoId), cid: rec.cid || null, size: Number(rec.size) || 0, savedAt: Number(rec.savedAt) || Date.now(), lastCheckedAt: Number(rec.lastCheckedAt) || Date.now(), state: rec.state || 'unhashed', })); return true; }, null), getFile: (videoId) => safe(async () => (await done((await store('readonly')).get(String(videoId)))) || null, null), getByCid: (cid) => safe(async () => (await done((await store('readonly')).index('cid').get(String(cid)))) || null, null), listFiles: () => safe(async () => (await done((await store('readonly')).getAll())) || [], []), deleteFile: (videoId) => safe(async () => { await done((await store('readwrite')).delete(String(videoId))); return true; }, null), clear: () => safe(async () => { await done((await store('readwrite')).clear()); return true; }, null), }; }());