// dl-sw.js — downloads za background (Background Fetch) kwa JAYNES MAX // Weka mstari huu JUU kabisa kwenye sw.js yako: importScripts('dl-sw.js'); const JM_OFFLINE_DIR = 'jm-offline'; // lazima iwe sawa na OFFLINE_DIR kwenye index.html async function jmDir(){ const root = await navigator.storage.getDirectory(); return root.getDirectoryHandle(JM_OFFLINE_DIR, { create: true }); } async function jmReadMeta(dir, oid){ try { const fh = await dir.getFileHandle(oid + '.json'); return JSON.parse(await (await fh.getFile()).text()); } catch (_) { return null; } } async function jmWriteMeta(dir, oid, obj){ const fh = await dir.getFileHandle(oid + '.json', { create: true }); const w = await fh.createWritable(); await w.write(JSON.stringify(obj)); await w.close(); } async function jmBroadcast(msg){ const all = await self.clients.matchAll({ type: 'window', includeUncontrolled: true }); all.forEach(c => c.postMessage(msg)); } async function jmOpenDownloads(){ const all = await self.clients.matchAll({ type: 'window', includeUncontrolled: true }); for (const c of all) { if ('focus' in c) { try { await c.focus(); } catch (_) {} c.postMessage({ type: 'jm-open-downloads' }); return; } } await self.clients.openWindow('./?nav=downloads'); } // Ukurasa unauliza "je dl-sw.js ipo?" self.addEventListener('message', (e) => { if (e.data && e.data.type === 'jm-ping' && e.source) e.source.postMessage({ type: 'jm-pong' }); }); // Download imekamilika (hata app ikiwa imefungwa): hamisha faili kwenda OPFS self.addEventListener('backgroundfetchsuccess', (event) => { const bgf = event.registration; event.waitUntil((async () => { const oid = bgf.id.replace(/^jm-/, ''); const dir = await jmDir(); const meta = (await jmReadMeta(dir, oid)) || { id: oid, title: 'Video' }; let ok = false; try { const records = await bgf.matchAll(); const res = await records[0].responseReady; const ct = (res.headers.get('content-type') || '').toLowerCase(); if (!res.ok || !res.body || /mpegurl|text\/|html|json/.test(ct)) throw new Error('not_direct_video'); const fh = await dir.getFileHandle(oid + '.bin', { create: true }); const w = await fh.createWritable(); await res.body.pipeTo(w); // stream — haitumii RAM const size = (await fh.getFile()).size; // Thibitisha ukubwa: usikubali "imehifadhiwa" kwa faili lililokatika nusu let expected = 0; const cr = /bytes\s+\d+-\d+\/(\d+)/i.exec(res.headers.get('content-range') || ''); if (cr) expected = Number(cr[1]); else if (!res.headers.get('content-encoding')) expected = Number(res.headers.get('content-length')) || 0; if (!size) throw new Error('empty_file'); if (expected && size !== expected) throw new Error('size_mismatch'); if (bgf.downloadTotal && size < bgf.downloadTotal) throw new Error('incomplete'); await jmWriteMeta(dir, oid, { ...meta, state: 'done', size }); ok = true; } catch (e) { try { await dir.removeEntry(oid + '.bin'); } catch (_) {} await jmWriteMeta(dir, oid, { ...meta, state: 'failed', reason: String((e && e.message) || e) }); } try { event.updateUI({ title: (meta.title || 'Video') + (ok ? ' — imehifadhiwa ✔' : ' — imeshindwa') }); } catch (_) {} await jmBroadcast({ type: 'jm-bg-update', bgId: bgf.id }); })()); }); async function jmMarkFailed(bgf, reason){ const oid = bgf.id.replace(/^jm-/, ''); const dir = await jmDir(); const meta = (await jmReadMeta(dir, oid)) || { id: oid }; await jmWriteMeta(dir, oid, { ...meta, state: 'failed', reason }); await jmBroadcast({ type: 'jm-bg-update', bgId: bgf.id }); } self.addEventListener('backgroundfetchfail', (e) => { e.waitUntil(jmMarkFailed(e.registration, e.registration.failureReason || 'failed')); }); self.addEventListener('backgroundfetchabort', (e) => { e.waitUntil(jmMarkFailed(e.registration, 'aborted')); }); self.addEventListener('backgroundfetchclick', (e) => { e.waitUntil(jmOpenDownloads()); }); // Bonyeza notification ya "xx%" / "Imekamilika" (njia ya ndani ya ukurasa) → fungua Downloads self.addEventListener('notificationclick', (e) => { if (!/^jm-dl-/.test((e.notification && e.notification.tag) || '')) return; e.notification.close(); e.waitUntil(jmOpenDownloads()); });