Spaces:
Running
Running
Upload 2 files
Browse files- dl-sw.js +8 -0
- index.html +286 -35
dl-sw.js
CHANGED
|
@@ -44,6 +44,14 @@ self.addEventListener('backgroundfetchsuccess', (event) => {
|
|
| 44 |
const w = await fh.createWritable();
|
| 45 |
await res.body.pipeTo(w); // stream β haitumii RAM
|
| 46 |
const size = (await fh.getFile()).size;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
await jmWriteMeta(dir, oid, { ...meta, state: 'done', size });
|
| 48 |
ok = true;
|
| 49 |
} catch (e) {
|
|
|
|
| 44 |
const w = await fh.createWritable();
|
| 45 |
await res.body.pipeTo(w); // stream β haitumii RAM
|
| 46 |
const size = (await fh.getFile()).size;
|
| 47 |
+
// Thibitisha ukubwa: usikubali "imehifadhiwa" kwa faili lililokatika nusu
|
| 48 |
+
let expected = 0;
|
| 49 |
+
const cr = /bytes\s+\d+-\d+\/(\d+)/i.exec(res.headers.get('content-range') || '');
|
| 50 |
+
if (cr) expected = Number(cr[1]);
|
| 51 |
+
else if (!res.headers.get('content-encoding')) expected = Number(res.headers.get('content-length')) || 0;
|
| 52 |
+
if (!size) throw new Error('empty_file');
|
| 53 |
+
if (expected && size !== expected) throw new Error('size_mismatch');
|
| 54 |
+
if (bgf.downloadTotal && size < bgf.downloadTotal) throw new Error('incomplete');
|
| 55 |
await jmWriteMeta(dir, oid, { ...meta, state: 'done', size });
|
| 56 |
ok = true;
|
| 57 |
} catch (e) {
|
index.html
CHANGED
|
@@ -2288,6 +2288,8 @@ const I18N = {
|
|
| 2288 |
dq_saving: 'Inahifadhi kwenye appβ¦',
|
| 2289 |
dq_saved_app: 'Imehifadhiwa kwenye app β',
|
| 2290 |
dq_sent_browser: 'Imetumwa kwa kivinjari (angalia Downloads za kivinjari)',
|
|
|
|
|
|
|
| 2291 |
dq_tab_saved: 'βΆ Zimehifadhiwa',
|
| 2292 |
dq_play: 'Cheza',
|
| 2293 |
dq_delete: 'Futa',
|
|
@@ -2425,6 +2427,8 @@ const I18N = {
|
|
| 2425 |
dq_saving: 'Saving to appβ¦',
|
| 2426 |
dq_saved_app: 'Saved in the app β',
|
| 2427 |
dq_sent_browser: 'Sent to the browser (check your browser Downloads)',
|
|
|
|
|
|
|
| 2428 |
dq_tab_saved: 'βΆ Saved',
|
| 2429 |
dq_play: 'Play',
|
| 2430 |
dq_delete: 'Delete',
|
|
@@ -2818,6 +2822,13 @@ const I18N_EXTRA7 = {"sw": {"not_live_msg": "Haipatikani kwa LIVE", "vol_warn":
|
|
| 2818 |
Object.keys(I18N_EXTRA7).forEach(l => { I18N[l] = Object.assign(I18N[l] || {}, I18N_EXTRA7[l]); });
|
| 2819 |
const I18N_EXTRA8 = {"sw": {"dq_paused_resume": "Imesimamishwa β gusa β» kuendelea"}, "en": {"dq_paused_resume": "Paused β tap β» to resume"}, "ar": {"dq_paused_resume": "Ω
ΨͺΩΩΩ Ω
Ψ€ΩΨͺΨ§Ω β Ψ§ΨΆΨΊΨ· β» ΩΩΩ
ΨͺΨ§Ψ¨ΨΉΨ©"}, "fr": {"dq_paused_resume": "En pause β touchez β» pour reprendre"}};
|
| 2820 |
Object.keys(I18N_EXTRA8).forEach(l => { I18N[l] = Object.assign(I18N[l] || {}, I18N_EXTRA8[l]); });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2821 |
const I18N_EXTRA10 = {"sw": {}, "en": {}, "ar": {}, "fr": {}};
|
| 2822 |
Object.keys(I18N_EXTRA10).forEach(l => { I18N[l] = Object.assign(I18N[l] || {}, I18N_EXTRA10[l]); });
|
| 2823 |
function T(k, v) { return t(k, v); }
|
|
@@ -7793,29 +7804,203 @@ async function offlineCheckQuota(needBytes){
|
|
| 7793 |
}
|
| 7794 |
|
| 7795 |
// Kutoka kwenye foleni ya downloads: pakua moja kwa moja kwenda OPFS
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7796 |
async function saveItemOffline(item, url, filename){
|
| 7797 |
-
const
|
| 7798 |
-
|
| 7799 |
-
const
|
| 7800 |
-
|
| 7801 |
-
|
| 7802 |
-
|
| 7803 |
-
|
| 7804 |
-
|
| 7805 |
-
|
| 7806 |
-
item.
|
| 7807 |
-
|
| 7808 |
-
|
| 7809 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7810 |
item.progressText = t('dq_saving');
|
| 7811 |
-
|
| 7812 |
-
|
| 7813 |
-
|
| 7814 |
-
|
| 7815 |
-
|
| 7816 |
-
|
| 7817 |
-
|
| 7818 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7819 |
}
|
| 7820 |
|
| 7821 |
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -7860,6 +8045,16 @@ async function bgMetaWrite(oid, obj){
|
|
| 7860 |
}
|
| 7861 |
async function bgMetaDelete(oid){ try { const d = await offlineDirHandle(); await d.removeEntry(oid + '.json'); } catch (_) {} }
|
| 7862 |
async function bgMergeDone(oid, m){
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7863 |
const list = getOfflineMeta();
|
| 7864 |
if (!list.some(x => x.id === oid)) {
|
| 7865 |
list.push({ id: oid, title: m.title, sub: m.sub || '', poster: m.poster || '', thumbLabel: m.thumbLabel || '',
|
|
@@ -7906,7 +8101,7 @@ function bgTrack(item, bgf, oid, total){
|
|
| 7906 |
const check = async () => {
|
| 7907 |
if (finished) return;
|
| 7908 |
const m = await bgMetaRead(oid);
|
| 7909 |
-
if (m && m.state === 'done') finish(resolve, await bgMergeDone(oid, m));
|
| 7910 |
else if (m && m.state === 'failed') {
|
| 7911 |
await bgMetaDelete(oid);
|
| 7912 |
const err = new Error(m.reason === 'aborted' ? t('dq_status_error') : (m.reason || 'bg_failed'));
|
|
@@ -7978,8 +8173,12 @@ async function bgSyncAll(){
|
|
| 7978 |
const m = await bgMetaRead(oid); if (!m) continue;
|
| 7979 |
const item = dlQueueItems.find(x => x.bgId === 'jm-' + oid);
|
| 7980 |
if (m.state === 'done') {
|
| 7981 |
-
|
| 7982 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7983 |
changed = true;
|
| 7984 |
} else if (m.state === 'failed') {
|
| 7985 |
await bgMetaDelete(oid);
|
|
@@ -7997,7 +8196,7 @@ async function bgResumeItems(){
|
|
| 7997 |
for (const item of pend) {
|
| 7998 |
const oid = item.bgId.replace(/^jm-/, '');
|
| 7999 |
const m = await bgMetaRead(oid);
|
| 8000 |
-
if (m && m.state === 'done') { bgFinishItem(item, await bgMergeDone(oid, m)); continue; }
|
| 8001 |
let bgf = null;
|
| 8002 |
try { bgf = reg && reg.backgroundFetch ? await reg.backgroundFetch.get(item.bgId) : null; } catch (_) {}
|
| 8003 |
if (bgf && !(m && m.state === 'failed')) {
|
|
@@ -8874,7 +9073,16 @@ function loadDlQueueFromStorage(){
|
|
| 8874 |
// Upakuaji uliokuwa 'downloading' ulipofungwa ukurasa haukuendelea kweli β
|
| 8875 |
// urudishe kwenye 'waiting' ili foleni iuanze upya.
|
| 8876 |
// Usianze upakuaji kiotomatiki app ikifunguliwa: vilivyokatizwa/vilivyokuwa vinasubiri vinasimamishwa hadi mtumiaji aguse β».
|
| 8877 |
-
dlQueueItems = saved.map(it =>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8878 |
dlIdSeq = dlQueueItems.reduce((max, it) => Math.max(max, it.id || 0), 0) + 1;
|
| 8879 |
} catch(_) {}
|
| 8880 |
}
|
|
@@ -8893,7 +9101,7 @@ async function notifyDownloadDone(item, ok){
|
|
| 8893 |
}
|
| 8894 |
|
| 8895 |
function updateDownloadsBadge(){
|
| 8896 |
-
const n = dlQueueItems.filter(it => it.status === 'waiting' || it.status === 'downloading').length;
|
| 8897 |
const badge = document.getElementById('dqNavBadge');
|
| 8898 |
if (!badge) return;
|
| 8899 |
if (n > 0) { badge.style.display = 'flex'; badge.textContent = n > 99 ? '99+' : String(n); }
|
|
@@ -9039,17 +9247,19 @@ async function processQueueItem(item){
|
|
| 9039 |
if (!videoUrl) throw new Error(t('dq_err_no_video'));
|
| 9040 |
dlSubCache.set(item.id, await fetchSubSidecars(subtitles)); // nakala za .vtt kwa ajili ya CC ya kicheza
|
| 9041 |
// HATUA 1: muunganisho (mux) wa video + manukuu β hii SIO download yenyewe
|
| 9042 |
-
const
|
|
|
|
| 9043 |
item.phase = 'mux';
|
| 9044 |
item.progressText = msg;
|
| 9045 |
updateDqProgressDom(item);
|
| 9046 |
if (!document.querySelector('[data-dq-id="' + item.id + '"]')) renderDownloadsScreenIfActive();
|
| 9047 |
});
|
|
|
|
| 9048 |
// HATUA 2: download halisi β inaonyesha speed (MB/s) na ukubwa (kilichopakuliwa / jumla)
|
| 9049 |
let saved = null;
|
| 9050 |
if (offlineAutoOn() && offlineCanSave()) {
|
| 9051 |
// Njia bora: Background Fetch β inaendelea hata ukifunga app, na Android inaonyesha asilimia kwenye notification
|
| 9052 |
-
if (bgFetchSupported() && await dlSwReady()) {
|
| 9053 |
try { saved = await saveItemViaBackgroundFetch(item, finalUrl, filename); }
|
| 9054 |
catch (e) {
|
| 9055 |
if (item.cancelled) return;
|
|
@@ -9059,10 +9269,19 @@ async function processQueueItem(item){
|
|
| 9059 |
}
|
| 9060 |
if (!saved) {
|
| 9061 |
try { saved = await saveItemOffline(item, finalUrl, filename); }
|
| 9062 |
-
catch (e) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9063 |
}
|
| 9064 |
}
|
| 9065 |
item.bgId = null;
|
|
|
|
| 9066 |
item.phase = 'done'; item.dlSpeed = 0;
|
| 9067 |
if (saved) {
|
| 9068 |
item.offlineId = saved.id; item.dlBytes = saved.size; item.dlTotal = saved.size;
|
|
@@ -9076,6 +9295,11 @@ async function processQueueItem(item){
|
|
| 9076 |
dlSubCache.delete(item.id);
|
| 9077 |
if (!(saved && saved.viaBg)) notifyDownloadDone(item, true);
|
| 9078 |
} catch (e) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9079 |
const isPermanent = !!(e && e.permanent) || /DRM/i.test(e.message || '');
|
| 9080 |
item.phase = null; item.dlSpeed = 0;
|
| 9081 |
item.autoRetries = item.autoRetries || 0;
|
|
@@ -9137,13 +9361,24 @@ function dqStatusShort(it){
|
|
| 9137 |
if (it.phase === 'mux') return t('dq_phase_mux');
|
| 9138 |
return t('dq_status_downloading');
|
| 9139 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9140 |
function dqProgressInner(it){
|
| 9141 |
if (it.phase === 'download') {
|
| 9142 |
const p = dqPercent(it);
|
|
|
|
| 9143 |
const bar = p != null ? `<div class="dq-bar-fill det" style="width:${p}%"></div>` : `<div class="dq-bar-fill"></div>`;
|
| 9144 |
const sizeTxt = fmtSize(it.dlBytes) + ' ' + t('dq_of') + ' ' + (it.dlTotal ? fmtSize(it.dlTotal) : '?');
|
| 9145 |
-
|
| 9146 |
-
|
|
|
|
|
|
|
| 9147 |
}
|
| 9148 |
return `<div class="dq-bar-track"><div class="dq-bar-fill"></div></div>
|
| 9149 |
<div class="dq-meta"><span>${esc(it.progressText || t('dq_status_downloading'))}</span></div>`;
|
|
@@ -9165,6 +9400,7 @@ function dqItemNode(it){
|
|
| 9165 |
if (dqSelectMode) div.style.cursor = 'pointer';
|
| 9166 |
const statusClass = it.status === 'error' ? 'err' : it.status === 'done' ? 'ok' : '';
|
| 9167 |
const statusLabel = it.status === 'waiting' ? (it.heldByWifi ? t('dq_waiting_wifi') : t('dq_status_waiting'))
|
|
|
|
| 9168 |
: it.status === 'downloading' ? dqStatusShort(it)
|
| 9169 |
: it.status === 'done' ? t('dq_status_done')
|
| 9170 |
: (it.progressText || t('dq_status_error'));
|
|
@@ -9173,7 +9409,7 @@ function dqItemNode(it){
|
|
| 9173 |
<div class="dq-text">
|
| 9174 |
<div class="dq-title">${esc(it.title)}</div>
|
| 9175 |
<div class="dq-sub">${esc(it.sub)}${it.sizeBytes ? ' Β· ' + esc(formatBytes(it.sizeBytes)) : ''}</div>
|
| 9176 |
-
${it.status === 'downloading' ? `<div class="dq-prog">${dqProgressInner(it)}</div>` : ''}
|
| 9177 |
${it.status === 'done' ? `<div class="dq-meta"><span>${esc(it.progressText || '')}</span>${it.dlBytes ? `<span>${esc(fmtSize(it.dlBytes))}</span>` : ''}</div>` : ''}
|
| 9178 |
</div>
|
| 9179 |
<div class="dq-status">${dqSelectMode
|
|
@@ -9192,6 +9428,19 @@ function dqItemNode(it){
|
|
| 9192 |
btn.className = 'dq-action-btn'; btn.textContent = 'β»'; btn.title = t('dq_retry');
|
| 9193 |
btn.onclick = () => { it.status = 'waiting'; it.autoRetries = 0; it.progressText = ''; saveDlQueue(); renderDownloadsScreen(); pumpDownloadQueue(); };
|
| 9194 |
statusWrap.appendChild(btn);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9195 |
} else if (it.status === 'downloading' && it.bgId) {
|
| 9196 |
const cb = document.createElement('button');
|
| 9197 |
cb.className = 'dq-action-btn'; cb.textContent = 'β'; cb.title = t('dq_remove');
|
|
@@ -9209,7 +9458,7 @@ function dqItemNode(it){
|
|
| 9209 |
}
|
| 9210 |
const btn = document.createElement('button');
|
| 9211 |
btn.className = 'dq-action-btn'; btn.textContent = 'β'; btn.title = t('dq_remove');
|
| 9212 |
-
btn.onclick = (ev) => { ev.stopPropagation(); dlQueueItems = dlQueueItems.filter(x => x.id !== it.id); saveDlQueue(); renderDownloadsScreen(); updateDownloadsBadge(); };
|
| 9213 |
statusWrap.appendChild(btn);
|
| 9214 |
}
|
| 9215 |
return div;
|
|
@@ -9217,7 +9466,7 @@ function dqItemNode(it){
|
|
| 9217 |
|
| 9218 |
function renderDownloadsScreen(){
|
| 9219 |
const waiting = dlQueueItems.filter(it => it.status === 'waiting');
|
| 9220 |
-
const downloading = dlQueueItems.filter(it => it.status === 'downloading');
|
| 9221 |
const done = dlQueueItems.filter(it => it.status === 'done' || it.status === 'error');
|
| 9222 |
if (dqActiveTab === 'offline') { renderOfflineTab(waiting.length, downloading.length, done.length); updateDownloadsBadge(); return; }
|
| 9223 |
const list = dqActiveTab === 'waiting' ? waiting : dqActiveTab === 'downloading' ? downloading : done;
|
|
@@ -9263,6 +9512,7 @@ function renderDownloadsScreen(){
|
|
| 9263 |
});
|
| 9264 |
const clearDoneBtn = document.getElementById('dqClearDoneBtn');
|
| 9265 |
if (clearDoneBtn) clearDoneBtn.onclick = () => {
|
|
|
|
| 9266 |
dlQueueItems = dlQueueItems.filter(x => x.status !== 'done' && x.status !== 'error');
|
| 9267 |
saveDlQueue();
|
| 9268 |
renderDownloadsScreen();
|
|
@@ -9275,6 +9525,7 @@ function renderDownloadsScreen(){
|
|
| 9275 |
const deleteSelectedBtn = document.getElementById('dqDeleteSelectedBtn');
|
| 9276 |
if (deleteSelectedBtn) deleteSelectedBtn.onclick = () => {
|
| 9277 |
dlQueueItems.filter(x => dqSelection.has(x.id)).forEach(bgAbortItem);
|
|
|
|
| 9278 |
dlQueueItems = dlQueueItems.filter(x => !dqSelection.has(x.id));
|
| 9279 |
dqSelectMode = false; dqSelection.clear();
|
| 9280 |
saveDlQueue(); renderDownloadsScreen(); updateDownloadsBadge();
|
|
|
|
| 2288 |
dq_saving: 'Inahifadhi kwenye appβ¦',
|
| 2289 |
dq_saved_app: 'Imehifadhiwa kwenye app β',
|
| 2290 |
dq_sent_browser: 'Imetumwa kwa kivinjari (angalia Downloads za kivinjari)',
|
| 2291 |
+
dq_err_net: 'Mtandao ulikatika β download haijakamilika. Gusa β» kujaribu tena.',
|
| 2292 |
+
dq_reconnecting: 'Mtandao umekatika, inaunganisha tenaβ¦',
|
| 2293 |
dq_tab_saved: 'βΆ Zimehifadhiwa',
|
| 2294 |
dq_play: 'Cheza',
|
| 2295 |
dq_delete: 'Futa',
|
|
|
|
| 2427 |
dq_saving: 'Saving to appβ¦',
|
| 2428 |
dq_saved_app: 'Saved in the app β',
|
| 2429 |
dq_sent_browser: 'Sent to the browser (check your browser Downloads)',
|
| 2430 |
+
dq_err_net: 'Connection lost β download not complete. Tap β» to retry.',
|
| 2431 |
+
dq_reconnecting: 'Connection lost, reconnectingβ¦',
|
| 2432 |
dq_tab_saved: 'βΆ Saved',
|
| 2433 |
dq_play: 'Play',
|
| 2434 |
dq_delete: 'Delete',
|
|
|
|
| 2822 |
Object.keys(I18N_EXTRA7).forEach(l => { I18N[l] = Object.assign(I18N[l] || {}, I18N_EXTRA7[l]); });
|
| 2823 |
const I18N_EXTRA8 = {"sw": {"dq_paused_resume": "Imesimamishwa β gusa β» kuendelea"}, "en": {"dq_paused_resume": "Paused β tap β» to resume"}, "ar": {"dq_paused_resume": "Ω
ΨͺΩΩΩ Ω
Ψ€ΩΨͺΨ§Ω β Ψ§ΨΆΨΊΨ· β» ΩΩΩ
ΨͺΨ§Ψ¨ΨΉΨ©"}, "fr": {"dq_paused_resume": "En pause β touchez β» pour reprendre"}};
|
| 2824 |
Object.keys(I18N_EXTRA8).forEach(l => { I18N[l] = Object.assign(I18N[l] || {}, I18N_EXTRA8[l]); });
|
| 2825 |
+
const I18N_EXTRA9 = {
|
| 2826 |
+
sw: { dq_pause: 'Simamisha', dq_continue: 'Endelea', dq_paused: 'Imesimamishwa', dq_eta_left: 'Imebaki ~{x}', dq_sec: 'sek', dq_min: 'dk', dq_hr: 'saa' },
|
| 2827 |
+
en: { dq_pause: 'Pause', dq_continue: 'Resume', dq_paused: 'Paused', dq_eta_left: '~{x} left', dq_sec: 's', dq_min: 'min', dq_hr: 'h' },
|
| 2828 |
+
ar: { dq_pause: 'Ψ₯ΩΩΨ§Ω Ω
Ψ€ΩΨͺ', dq_continue: 'Ω
ΨͺΨ§Ψ¨ΨΉΨ©', dq_paused: 'Ω
ΨͺΩΩΩ Ω
Ψ€ΩΨͺΨ§Ω', dq_eta_left: 'Ω
ΨͺΨ¨ΩΩ ~{x}', dq_sec: 'Ψ«', dq_min: 'Ψ―', dq_hr: 'Ψ³' },
|
| 2829 |
+
fr: { dq_pause: 'Pause', dq_continue: 'Reprendre', dq_paused: 'En pause', dq_eta_left: '~{x} restantes', dq_sec: 's', dq_min: 'min', dq_hr: 'h' }
|
| 2830 |
+
};
|
| 2831 |
+
Object.keys(I18N_EXTRA9).forEach(l => { I18N[l] = Object.assign(I18N[l] || {}, I18N_EXTRA9[l]); });
|
| 2832 |
const I18N_EXTRA10 = {"sw": {}, "en": {}, "ar": {}, "fr": {}};
|
| 2833 |
Object.keys(I18N_EXTRA10).forEach(l => { I18N[l] = Object.assign(I18N[l] || {}, I18N_EXTRA10[l]); });
|
| 2834 |
function T(k, v) { return t(k, v); }
|
|
|
|
| 7804 |
}
|
| 7805 |
|
| 7806 |
// Kutoka kwenye foleni ya downloads: pakua moja kwa moja kwenda OPFS
|
| 7807 |
+
// β’ Sehemu nyingi (Range) kwa wakati mmoja = kasi zaidi β’ Simamisha/Endelea β’ resume mtandao ukikatika
|
| 7808 |
+
// β’ "Imekamilika" tu kama kila sehemu imefika na ukubwa unalingana
|
| 7809 |
+
const JM_DL_USE_BG = false; // true = rudisha Background Fetch (inaendelea app ikifungwa, lakini haina Simamisha wala kasi ya ziada)
|
| 7810 |
+
const DL_SEG = 8 * 1024 * 1024;
|
| 7811 |
+
const dlRuns = new Map(); // item.id -> { ac, pause, cancel, fail }
|
| 7812 |
+
function dlConns(){ return Math.max(2, Math.min(6, Math.floor(8 / Math.max(1, dlActiveCount)))); }
|
| 7813 |
+
function _dlWaitOnline(ms){
|
| 7814 |
+
return new Promise(res => {
|
| 7815 |
+
if (navigator.onLine !== false) return setTimeout(res, ms);
|
| 7816 |
+
const h = () => { window.removeEventListener('online', h); setTimeout(res, 1000); };
|
| 7817 |
+
window.addEventListener('online', h);
|
| 7818 |
+
setTimeout(() => { window.removeEventListener('online', h); res(); }, 60000);
|
| 7819 |
+
});
|
| 7820 |
+
}
|
| 7821 |
+
function dlResetPartialFields(item){ item.partId = null; item.segDone = null; item.partTotal = 0; item.partEtag = null; item.finalUrl = null; }
|
| 7822 |
+
async function dlDiscardPartial(item){
|
| 7823 |
+
if (!item) return;
|
| 7824 |
+
const run = dlRuns.get(item.id);
|
| 7825 |
+
if (run) { run.cancel = true; try { run.ac.abort(); } catch (_) {} }
|
| 7826 |
+
const pid = item.partId;
|
| 7827 |
+
dlResetPartialFields(item);
|
| 7828 |
+
if (pid) { try { const d = await offlineDirHandle(); await d.removeEntry(pid + '.bin'); } catch (_) {} }
|
| 7829 |
+
}
|
| 7830 |
+
function dlPause(item){
|
| 7831 |
+
const run = dlRuns.get(item.id);
|
| 7832 |
+
if (!run || item.noPause) return;
|
| 7833 |
+
run.pause = true; item.progressText = t('dq_paused');
|
| 7834 |
+
try { run.ac.abort(); } catch (_) {}
|
| 7835 |
+
}
|
| 7836 |
+
function dlContinue(item){
|
| 7837 |
+
if (!item || item.status !== 'paused') return;
|
| 7838 |
+
item.status = 'waiting'; item.autoRetries = 0; item.progressText = '';
|
| 7839 |
+
saveDlQueue(); renderDownloadsScreenIfActive(); pumpDownloadQueue();
|
| 7840 |
+
}
|
| 7841 |
+
|
| 7842 |
async function saveItemOffline(item, url, filename){
|
| 7843 |
+
const src = proxiedDownloadUrl(url, filename);
|
| 7844 |
+
const dir = await offlineDirHandle();
|
| 7845 |
+
const ac = new AbortController();
|
| 7846 |
+
const run = { ac, pause: false, cancel: false, fail: false };
|
| 7847 |
+
dlRuns.set(item.id, run);
|
| 7848 |
+
item.noPause = false;
|
| 7849 |
+
let id = item.partId || offlineNewId();
|
| 7850 |
+
let segDone = new Set(Array.isArray(item.segDone) ? item.segDone : []);
|
| 7851 |
+
let resumed = !!(item.partId && segDone.size);
|
| 7852 |
+
const SEG = item.segSize || DL_SEG;
|
| 7853 |
+
let total = 0, w = null, fh = null, chain = Promise.resolve(), tick = null, single = false;
|
| 7854 |
+
let doneBytes = 0; const inflight = new Map(); let singleBytes = 0;
|
| 7855 |
+
const segLen = (i) => Math.min(SEG, total - i * SEG);
|
| 7856 |
+
const bytesNow = () => { let b = single ? singleBytes : doneBytes; inflight.forEach(v => { b += v; }); return b; };
|
| 7857 |
+
const writeAt = (pos, data) => (chain = chain.then(() => w.write({ type: 'write', position: pos, data })));
|
| 7858 |
+
const closeW = async () => { try { await chain; } catch (_) {} try { if (w) await w.close(); } catch (_) {} w = null; };
|
| 7859 |
+
const abortW = async () => { try { await chain; } catch (_) {} try { if (w) await w.abort(); } catch (_) {} w = null; };
|
| 7860 |
+
const removeFile = async (fid) => { try { await dir.removeEntry(fid + '.bin'); } catch (_) {} };
|
| 7861 |
+
const stop = () => run.pause || run.cancel || run.fail;
|
| 7862 |
+
try {
|
| 7863 |
+
// 1) Uchunguzi: ukubwa kamili + je seva inaunga mkono Range?
|
| 7864 |
+
const probe = await fetchRetry(src, { mode: 'cors', headers: { Range: 'bytes=0-0' }, signal: ac.signal });
|
| 7865 |
+
if (!(probe.ok || probe.status === 206)) { const e = new Error('http_' + probe.status); e.fatal = probe.status >= 400 && probe.status < 500 && probe.status !== 429; throw e; }
|
| 7866 |
+
const ct = (probe.headers.get('content-type') || '').toLowerCase();
|
| 7867 |
+
if (/mpegurl|text\/|html|json/.test(ct)) throw new Error('not_direct_video');
|
| 7868 |
+
let rangeOK = false;
|
| 7869 |
+
if (probe.status === 206) {
|
| 7870 |
+
const m = /bytes\s+\d+-\d+\/(\d+)/i.exec(probe.headers.get('content-range') || '');
|
| 7871 |
+
if (m) { rangeOK = true; total = Number(m[1]); }
|
| 7872 |
+
}
|
| 7873 |
+
if (!rangeOK) total = Number(probe.headers.get('content-length')) || Number(item.sizeBytes) || 0;
|
| 7874 |
+
const etag = probe.headers.get('etag') || '';
|
| 7875 |
+
try { if (probe.body) probe.body.cancel(); } catch (_) {}
|
| 7876 |
+
|
| 7877 |
+
// 2) Resume: thibitisha kipande cha zamani bado ni cha faili lilelile
|
| 7878 |
+
if (resumed) {
|
| 7879 |
+
let okPart = rangeOK && total && total === item.partTotal && (!item.partEtag || !etag || item.partEtag === etag);
|
| 7880 |
+
if (okPart) {
|
| 7881 |
+
try {
|
| 7882 |
+
const f = await (await dir.getFileHandle(id + '.bin')).getFile();
|
| 7883 |
+
let maxEnd = 0; segDone.forEach(i => { maxEnd = Math.max(maxEnd, Math.min(total, (i + 1) * SEG)); });
|
| 7884 |
+
if (f.size < maxEnd) okPart = false;
|
| 7885 |
+
} catch (_) { okPart = false; }
|
| 7886 |
+
}
|
| 7887 |
+
if (!okPart) { await removeFile(id); id = offlineNewId(); segDone = new Set(); resumed = false; dlResetPartialFields(item); }
|
| 7888 |
+
}
|
| 7889 |
+
segDone.forEach(i => { doneBytes += segLen(i); });
|
| 7890 |
+
|
| 7891 |
+
if (!resumed) { await offlineCheckQuota(total); await requestPersistentStorage(); }
|
| 7892 |
+
fh = await dir.getFileHandle(id + '.bin', { create: true });
|
| 7893 |
+
w = await fh.createWritable({ keepExistingData: resumed });
|
| 7894 |
+
item.partId = id; item.partTotal = total; item.segSize = SEG; item.partEtag = etag; item.segDone = [...segDone];
|
| 7895 |
+
item.phase = 'download'; item.dlBytes = doneBytes; item.dlTotal = total || 0; item.dlSpeed = 0;
|
| 7896 |
item.progressText = t('dq_saving');
|
| 7897 |
+
saveDlQueue(); renderDownloadsScreenIfActive();
|
| 7898 |
+
|
| 7899 |
+
// 3) Ticker: speed + ETA
|
| 7900 |
+
let lastT = Date.now(), lastB = bytesNow(), sp = 0;
|
| 7901 |
+
tick = setInterval(() => {
|
| 7902 |
+
const n = Date.now(), bb = bytesNow(), dt = (n - lastT) / 1000;
|
| 7903 |
+
if (dt > 0) { const inst = (bb - lastB) / dt; sp = sp ? sp * 0.7 + inst * 0.3 : inst; }
|
| 7904 |
+
lastT = n; lastB = bb;
|
| 7905 |
+
item.dlBytes = bb; item.dlTotal = total || item.dlTotal || 0; item.dlSpeed = sp;
|
| 7906 |
+
item.progressText = t('dq_saving');
|
| 7907 |
+
updateDqProgressDom(item);
|
| 7908 |
+
}, 700);
|
| 7909 |
+
|
| 7910 |
+
if (rangeOK && total > 0) {
|
| 7911 |
+
// ββ Sehemu nyingi kwa wakati mmoja ββ
|
| 7912 |
+
const nSeg = Math.ceil(total / SEG);
|
| 7913 |
+
const todo = []; for (let i = 0; i < nSeg; i++) if (!segDone.has(i)) todo.push(i);
|
| 7914 |
+
let next = 0;
|
| 7915 |
+
const ckptTimer = setInterval(() => { // hifadhi kwa kudumu kila dakika 5 (kuokoa data app ikifungwa ghafla)
|
| 7916 |
+
if (stop() || !w) return;
|
| 7917 |
+
chain = chain.then(async () => { await w.close(); w = await fh.createWritable({ keepExistingData: true }); item.segDone = [...segDone]; saveDlQueue(); }).catch(() => {});
|
| 7918 |
+
}, 300000);
|
| 7919 |
+
const worker = async () => {
|
| 7920 |
+
try {
|
| 7921 |
+
while (!stop()) {
|
| 7922 |
+
const i = todo[next++]; if (i === undefined) return;
|
| 7923 |
+
const a = i * SEG, len = segLen(i), b2 = a + len - 1;
|
| 7924 |
+
let ok = false, lastE = null;
|
| 7925 |
+
for (let tr = 0; tr < 6 && !stop(); tr++) {
|
| 7926 |
+
try {
|
| 7927 |
+
inflight.set(i, 0);
|
| 7928 |
+
const res = await fetch(src, { mode: 'cors', headers: { Range: 'bytes=' + a + '-' + b2 }, signal: ac.signal });
|
| 7929 |
+
if (res.status !== 206 || !res.body) {
|
| 7930 |
+
const e = new Error(res.status === 200 ? 'range_unsupported' : 'http_' + res.status);
|
| 7931 |
+
e.fatal = (res.status >= 400 && res.status < 500 && res.status !== 429) || res.status === 200;
|
| 7932 |
+
throw e;
|
| 7933 |
+
}
|
| 7934 |
+
const buf = new Uint8Array(len); let off = 0;
|
| 7935 |
+
const reader = res.body.getReader();
|
| 7936 |
+
while (true) {
|
| 7937 |
+
const { value, done } = await reader.read(); if (done) break;
|
| 7938 |
+
if (off + value.byteLength > len) throw new Error('overrun');
|
| 7939 |
+
buf.set(value, off); off += value.byteLength; inflight.set(i, off);
|
| 7940 |
+
}
|
| 7941 |
+
if (off !== len) throw new Error('incomplete');
|
| 7942 |
+
await writeAt(a, buf);
|
| 7943 |
+
segDone.add(i); doneBytes += len; inflight.delete(i); ok = true; break;
|
| 7944 |
+
} catch (e) {
|
| 7945 |
+
inflight.delete(i);
|
| 7946 |
+
if (stop()) return;
|
| 7947 |
+
lastE = e;
|
| 7948 |
+
if (e && e.fatal) throw e;
|
| 7949 |
+
await _dlWaitOnline(1500 * (tr + 1));
|
| 7950 |
+
}
|
| 7951 |
+
}
|
| 7952 |
+
if (!ok) { if (stop()) return; throw lastE || new Error('incomplete'); }
|
| 7953 |
+
}
|
| 7954 |
+
} catch (e) { run.fail = true; try { ac.abort(); } catch (_) {} throw e; }
|
| 7955 |
+
};
|
| 7956 |
+
const results = await Promise.allSettled(Array.from({ length: Math.min(dlConns(), todo.length) }, worker));
|
| 7957 |
+
clearInterval(ckptTimer);
|
| 7958 |
+
const bad = results.find(r => r.status === 'rejected');
|
| 7959 |
+
if (run.cancel) { await abortW(); await removeFile(id); dlResetPartialFields(item); const e = new Error('cancelled'); e.cancelled = true; throw e; }
|
| 7960 |
+
if (run.pause && !bad) { await closeW(); item.segDone = [...segDone]; saveDlQueue(); const e = new Error('paused'); e.paused = true; throw e; }
|
| 7961 |
+
if (bad) {
|
| 7962 |
+
const e = bad.reason;
|
| 7963 |
+
if (e && e.fatal) { await abortW(); await removeFile(id); dlResetPartialFields(item); }
|
| 7964 |
+
else { await closeW(); item.segDone = [...segDone]; saveDlQueue(); } // acha kipande β jaribio lijalo litaendelea
|
| 7965 |
+
throw e;
|
| 7966 |
+
}
|
| 7967 |
+
} else {
|
| 7968 |
+
// ββ Seva haiungi Range: mtiririko mmoja (hakuna Simamisha) ββ
|
| 7969 |
+
single = true; item.noPause = true; renderDownloadsScreenIfActive();
|
| 7970 |
+
const res = await fetchRetry(src, { mode: 'cors', signal: ac.signal });
|
| 7971 |
+
if (!res.ok || !res.body) throw new Error('no_stream');
|
| 7972 |
+
const reader = res.body.getReader();
|
| 7973 |
+
let pos = 0;
|
| 7974 |
+
while (true) {
|
| 7975 |
+
const { value, done } = await reader.read(); if (done) break;
|
| 7976 |
+
await writeAt(pos, value); pos += value.byteLength; singleBytes = pos;
|
| 7977 |
+
}
|
| 7978 |
+
if (total && pos < total) throw new Error('incomplete');
|
| 7979 |
+
total = pos;
|
| 7980 |
+
}
|
| 7981 |
+
|
| 7982 |
+
// 4) Maliza + thibitisha
|
| 7983 |
+
clearInterval(tick); tick = null;
|
| 7984 |
+
await chain; await w.close(); w = null;
|
| 7985 |
+
const real = (await (await dir.getFileHandle(id + '.bin')).getFile()).size;
|
| 7986 |
+
if (!real || (total && real !== total)) throw new Error('size_mismatch');
|
| 7987 |
+
const size = real;
|
| 7988 |
+
const list = getOfflineMeta();
|
| 7989 |
+
list.push({ id, title: item.title, sub: item.sub || '', poster: item.poster || '', thumbLabel: item.thumbLabel || '',
|
| 7990 |
+
filename, size, mime: 'video/mp4', addedAt: Date.now(), source: 'download' });
|
| 7991 |
+
setOfflineMeta(list);
|
| 7992 |
+
await offlineStoreSubEntries(id, dlSubCache.get(item.id));
|
| 7993 |
+
return { id, size };
|
| 7994 |
+
} catch (e) {
|
| 7995 |
+
if (!(e && (e.paused || e.cancelled))) {
|
| 7996 |
+
await abortW();
|
| 7997 |
+
if (single || !item.segDone || !item.segDone.length) { await removeFile(id); dlResetPartialFields(item); }
|
| 7998 |
+
}
|
| 7999 |
+
throw e;
|
| 8000 |
+
} finally {
|
| 8001 |
+
if (tick) clearInterval(tick);
|
| 8002 |
+
dlRuns.delete(item.id);
|
| 8003 |
+
}
|
| 8004 |
}
|
| 8005 |
|
| 8006 |
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 8045 |
}
|
| 8046 |
async function bgMetaDelete(oid){ try { const d = await offlineDirHandle(); await d.removeEntry(oid + '.json'); } catch (_) {} }
|
| 8047 |
async function bgMergeDone(oid, m){
|
| 8048 |
+
// Thibitisha kwamba faili lipo kweli na ukubwa unalingana kabla ya kusema "imekamilika"
|
| 8049 |
+
try {
|
| 8050 |
+
const d0 = await offlineDirHandle();
|
| 8051 |
+
const f0 = await (await d0.getFileHandle(oid + '.bin')).getFile();
|
| 8052 |
+
if (!f0.size || (m.size && f0.size !== m.size)) throw new Error('size_mismatch');
|
| 8053 |
+
} catch (e) {
|
| 8054 |
+
try { const d1 = await offlineDirHandle(); await d1.removeEntry(oid + '.bin'); } catch (_) {}
|
| 8055 |
+
await bgMetaDelete(oid);
|
| 8056 |
+
throw new Error(t('dq_err_net') || 'Faili halikukamilika β jaribu tena.');
|
| 8057 |
+
}
|
| 8058 |
const list = getOfflineMeta();
|
| 8059 |
if (!list.some(x => x.id === oid)) {
|
| 8060 |
list.push({ id: oid, title: m.title, sub: m.sub || '', poster: m.poster || '', thumbLabel: m.thumbLabel || '',
|
|
|
|
| 8101 |
const check = async () => {
|
| 8102 |
if (finished) return;
|
| 8103 |
const m = await bgMetaRead(oid);
|
| 8104 |
+
if (m && m.state === 'done') { try { finish(resolve, await bgMergeDone(oid, m)); } catch (e) { finish(reject, e); } }
|
| 8105 |
else if (m && m.state === 'failed') {
|
| 8106 |
await bgMetaDelete(oid);
|
| 8107 |
const err = new Error(m.reason === 'aborted' ? t('dq_status_error') : (m.reason || 'bg_failed'));
|
|
|
|
| 8173 |
const m = await bgMetaRead(oid); if (!m) continue;
|
| 8174 |
const item = dlQueueItems.find(x => x.bgId === 'jm-' + oid);
|
| 8175 |
if (m.state === 'done') {
|
| 8176 |
+
try {
|
| 8177 |
+
const saved = await bgMergeDone(oid, m);
|
| 8178 |
+
if (item && item.status !== 'done') bgFinishItem(item, saved);
|
| 8179 |
+
} catch (e) {
|
| 8180 |
+
if (item) { item.bgId = null; item.phase = null; item.status = 'error'; item.autoRetries = 0; item.progressText = (e && e.message) || t('dq_status_error'); }
|
| 8181 |
+
}
|
| 8182 |
changed = true;
|
| 8183 |
} else if (m.state === 'failed') {
|
| 8184 |
await bgMetaDelete(oid);
|
|
|
|
| 8196 |
for (const item of pend) {
|
| 8197 |
const oid = item.bgId.replace(/^jm-/, '');
|
| 8198 |
const m = await bgMetaRead(oid);
|
| 8199 |
+
if (m && m.state === 'done') { try { bgFinishItem(item, await bgMergeDone(oid, m)); } catch (e) { item.bgId = null; item.phase = null; item.status = 'error'; item.autoRetries = 0; item.progressText = (e && e.message) || t('dq_status_error'); } continue; }
|
| 8200 |
let bgf = null;
|
| 8201 |
try { bgf = reg && reg.backgroundFetch ? await reg.backgroundFetch.get(item.bgId) : null; } catch (_) {}
|
| 8202 |
if (bgf && !(m && m.state === 'failed')) {
|
|
|
|
| 9073 |
// Upakuaji uliokuwa 'downloading' ulipofungwa ukurasa haukuendelea kweli β
|
| 9074 |
// urudishe kwenye 'waiting' ili foleni iuanze upya.
|
| 9075 |
// Usianze upakuaji kiotomatiki app ikifunguliwa: vilivyokatizwa/vilivyokuwa vinasubiri vinasimamishwa hadi mtumiaji aguse β».
|
| 9076 |
+
dlQueueItems = saved.map(it => {
|
| 9077 |
+
if ((it.status === 'downloading' && !it.bgId) || it.status === 'waiting') {
|
| 9078 |
+
if (it.partId && Array.isArray(it.segDone) && it.segDone.length) {
|
| 9079 |
+
const sb = Math.min(it.partTotal || 0, it.segDone.length * (it.segSize || DL_SEG));
|
| 9080 |
+
return { ...it, status: 'paused', phase: 'download', dlBytes: sb, dlTotal: it.partTotal || 0, dlSpeed: 0, autoRetries: 0, progressText: T('dq_paused') };
|
| 9081 |
+
}
|
| 9082 |
+
return { ...it, status: 'error', autoRetries: 0, progressText: T('dq_paused_resume') };
|
| 9083 |
+
}
|
| 9084 |
+
return it;
|
| 9085 |
+
});
|
| 9086 |
dlIdSeq = dlQueueItems.reduce((max, it) => Math.max(max, it.id || 0), 0) + 1;
|
| 9087 |
} catch(_) {}
|
| 9088 |
}
|
|
|
|
| 9101 |
}
|
| 9102 |
|
| 9103 |
function updateDownloadsBadge(){
|
| 9104 |
+
const n = dlQueueItems.filter(it => it.status === 'waiting' || it.status === 'downloading' || it.status === 'paused').length;
|
| 9105 |
const badge = document.getElementById('dqNavBadge');
|
| 9106 |
if (!badge) return;
|
| 9107 |
if (n > 0) { badge.style.display = 'flex'; badge.textContent = n > 99 ? '99+' : String(n); }
|
|
|
|
| 9247 |
if (!videoUrl) throw new Error(t('dq_err_no_video'));
|
| 9248 |
dlSubCache.set(item.id, await fetchSubSidecars(subtitles)); // nakala za .vtt kwa ajili ya CC ya kicheza
|
| 9249 |
// HATUA 1: muunganisho (mux) wa video + manukuu β hii SIO download yenyewe
|
| 9250 |
+
const _resumeOk = !!(item.finalUrl && item.partId && Array.isArray(item.segDone) && item.segDone.length);
|
| 9251 |
+
const finalUrl = _resumeOk ? item.finalUrl : await muxAndDownload(videoUrl, subtitles, filename, (msg) => {
|
| 9252 |
item.phase = 'mux';
|
| 9253 |
item.progressText = msg;
|
| 9254 |
updateDqProgressDom(item);
|
| 9255 |
if (!document.querySelector('[data-dq-id="' + item.id + '"]')) renderDownloadsScreenIfActive();
|
| 9256 |
});
|
| 9257 |
+
item.finalUrl = finalUrl; saveDlQueue();
|
| 9258 |
// HATUA 2: download halisi β inaonyesha speed (MB/s) na ukubwa (kilichopakuliwa / jumla)
|
| 9259 |
let saved = null;
|
| 9260 |
if (offlineAutoOn() && offlineCanSave()) {
|
| 9261 |
// Njia bora: Background Fetch β inaendelea hata ukifunga app, na Android inaonyesha asilimia kwenye notification
|
| 9262 |
+
if (JM_DL_USE_BG && bgFetchSupported() && await dlSwReady()) {
|
| 9263 |
try { saved = await saveItemViaBackgroundFetch(item, finalUrl, filename); }
|
| 9264 |
catch (e) {
|
| 9265 |
if (item.cancelled) return;
|
|
|
|
| 9269 |
}
|
| 9270 |
if (!saved) {
|
| 9271 |
try { saved = await saveItemOffline(item, finalUrl, filename); }
|
| 9272 |
+
catch (e) {
|
| 9273 |
+
console.warn('offline save failed', e);
|
| 9274 |
+
// Kosa la mtandao/ukubwa/nafasi: USIONYESHE "imekamilika" β lisababishe retry / "Imeshindwa".
|
| 9275 |
+
// Kutuma kwa kivinjari ni kwa video isiyo ya moja kwa moja tu.
|
| 9276 |
+
if (!(e && (e.message === 'not_direct_video' || e.message === 'no_stream'))) {
|
| 9277 |
+
if (e && /incomplete|size_mismatch|Failed to fetch|NetworkError|network/i.test(e.message || '')) e.message = t('dq_err_net') || 'Mtandao ulikatika β jaribu tena.';
|
| 9278 |
+
throw e;
|
| 9279 |
+
}
|
| 9280 |
+
}
|
| 9281 |
}
|
| 9282 |
}
|
| 9283 |
item.bgId = null;
|
| 9284 |
+
item.partId = null; item.segDone = null; item.finalUrl = null; item.partTotal = 0; item.noPause = false;
|
| 9285 |
item.phase = 'done'; item.dlSpeed = 0;
|
| 9286 |
if (saved) {
|
| 9287 |
item.offlineId = saved.id; item.dlBytes = saved.size; item.dlTotal = saved.size;
|
|
|
|
| 9295 |
dlSubCache.delete(item.id);
|
| 9296 |
if (!(saved && saved.viaBg)) notifyDownloadDone(item, true);
|
| 9297 |
} catch (e) {
|
| 9298 |
+
if (e && e.paused) { // mtumiaji alibonyeza Simamisha
|
| 9299 |
+
item.status = 'paused'; item.phase = 'download'; item.dlSpeed = 0; item.progressText = t('dq_paused');
|
| 9300 |
+
saveDlQueue(); renderDownloadsScreenIfActive(); updateDownloadsBadge(); return;
|
| 9301 |
+
}
|
| 9302 |
+
if (e && e.cancelled) return; // mtumiaji aliondoa download
|
| 9303 |
const isPermanent = !!(e && e.permanent) || /DRM/i.test(e.message || '');
|
| 9304 |
item.phase = null; item.dlSpeed = 0;
|
| 9305 |
item.autoRetries = item.autoRetries || 0;
|
|
|
|
| 9361 |
if (it.phase === 'mux') return t('dq_phase_mux');
|
| 9362 |
return t('dq_status_downloading');
|
| 9363 |
}
|
| 9364 |
+
function fmtEta(sec){
|
| 9365 |
+
if (!isFinite(sec) || sec <= 0) return '';
|
| 9366 |
+
sec = Math.round(sec);
|
| 9367 |
+
if (sec < 60) return sec + ' ' + t('dq_sec');
|
| 9368 |
+
const m = Math.floor(sec / 60);
|
| 9369 |
+
if (m < 60) return m + ' ' + t('dq_min') + (sec % 60 ? ' ' + (sec % 60) + ' ' + t('dq_sec') : '');
|
| 9370 |
+
return Math.floor(m / 60) + ' ' + t('dq_hr') + ' ' + (m % 60) + ' ' + t('dq_min');
|
| 9371 |
+
}
|
| 9372 |
function dqProgressInner(it){
|
| 9373 |
if (it.phase === 'download') {
|
| 9374 |
const p = dqPercent(it);
|
| 9375 |
+
const paused = it.status === 'paused';
|
| 9376 |
const bar = p != null ? `<div class="dq-bar-fill det" style="width:${p}%"></div>` : `<div class="dq-bar-fill"></div>`;
|
| 9377 |
const sizeTxt = fmtSize(it.dlBytes) + ' ' + t('dq_of') + ' ' + (it.dlTotal ? fmtSize(it.dlTotal) : '?');
|
| 9378 |
+
const eta = (!paused && it.dlSpeed > 0 && it.dlTotal > it.dlBytes) ? fmtEta((it.dlTotal - it.dlBytes) / it.dlSpeed) : '';
|
| 9379 |
+
return `<div class="dq-bar-track">${paused ? bar.replace('dq-bar-fill det', 'dq-bar-fill det') : bar}</div>
|
| 9380 |
+
<div class="dq-meta"><span>${esc(sizeTxt)}</span><span>${esc(paused ? t('dq_paused') : fmtSpeed(it.dlSpeed))}</span></div>
|
| 9381 |
+
${eta ? `<div class="dq-meta"><span>${esc(t('dq_eta_left', { x: eta }))}</span><span>${p != null ? p + '%' : ''}</span></div>` : ''}`;
|
| 9382 |
}
|
| 9383 |
return `<div class="dq-bar-track"><div class="dq-bar-fill"></div></div>
|
| 9384 |
<div class="dq-meta"><span>${esc(it.progressText || t('dq_status_downloading'))}</span></div>`;
|
|
|
|
| 9400 |
if (dqSelectMode) div.style.cursor = 'pointer';
|
| 9401 |
const statusClass = it.status === 'error' ? 'err' : it.status === 'done' ? 'ok' : '';
|
| 9402 |
const statusLabel = it.status === 'waiting' ? (it.heldByWifi ? t('dq_waiting_wifi') : t('dq_status_waiting'))
|
| 9403 |
+
: it.status === 'paused' ? t('dq_paused')
|
| 9404 |
: it.status === 'downloading' ? dqStatusShort(it)
|
| 9405 |
: it.status === 'done' ? t('dq_status_done')
|
| 9406 |
: (it.progressText || t('dq_status_error'));
|
|
|
|
| 9409 |
<div class="dq-text">
|
| 9410 |
<div class="dq-title">${esc(it.title)}</div>
|
| 9411 |
<div class="dq-sub">${esc(it.sub)}${it.sizeBytes ? ' Β· ' + esc(formatBytes(it.sizeBytes)) : ''}</div>
|
| 9412 |
+
${(it.status === 'downloading' || it.status === 'paused') ? `<div class="dq-prog">${dqProgressInner(it)}</div>` : ''}
|
| 9413 |
${it.status === 'done' ? `<div class="dq-meta"><span>${esc(it.progressText || '')}</span>${it.dlBytes ? `<span>${esc(fmtSize(it.dlBytes))}</span>` : ''}</div>` : ''}
|
| 9414 |
</div>
|
| 9415 |
<div class="dq-status">${dqSelectMode
|
|
|
|
| 9428 |
btn.className = 'dq-action-btn'; btn.textContent = 'β»'; btn.title = t('dq_retry');
|
| 9429 |
btn.onclick = () => { it.status = 'waiting'; it.autoRetries = 0; it.progressText = ''; saveDlQueue(); renderDownloadsScreen(); pumpDownloadQueue(); };
|
| 9430 |
statusWrap.appendChild(btn);
|
| 9431 |
+
} else if (it.status === 'paused' || (it.status === 'downloading' && !it.bgId && it.phase === 'download' && dlRuns.has(it.id))) {
|
| 9432 |
+
const paused = it.status === 'paused';
|
| 9433 |
+
if (paused || !it.noPause) {
|
| 9434 |
+
const pb = document.createElement('button');
|
| 9435 |
+
pb.className = 'dq-action-btn cc'; pb.style.cssText = 'height:26px;font-size:11px;white-space:nowrap';
|
| 9436 |
+
pb.textContent = paused ? 'βΆ ' + t('dq_continue') : 'βΈ ' + t('dq_pause');
|
| 9437 |
+
pb.onclick = (ev) => { ev.stopPropagation(); if (paused) dlContinue(it); else dlPause(it); };
|
| 9438 |
+
statusWrap.appendChild(pb);
|
| 9439 |
+
}
|
| 9440 |
+
const xb = document.createElement('button');
|
| 9441 |
+
xb.className = 'dq-action-btn'; xb.textContent = 'β'; xb.title = t('dq_remove');
|
| 9442 |
+
xb.onclick = (ev) => { ev.stopPropagation(); dlDiscardPartial(it); dlQueueItems = dlQueueItems.filter(x => x.id !== it.id); saveDlQueue(); renderDownloadsScreen(); updateDownloadsBadge(); };
|
| 9443 |
+
statusWrap.appendChild(xb);
|
| 9444 |
} else if (it.status === 'downloading' && it.bgId) {
|
| 9445 |
const cb = document.createElement('button');
|
| 9446 |
cb.className = 'dq-action-btn'; cb.textContent = 'β'; cb.title = t('dq_remove');
|
|
|
|
| 9458 |
}
|
| 9459 |
const btn = document.createElement('button');
|
| 9460 |
btn.className = 'dq-action-btn'; btn.textContent = 'β'; btn.title = t('dq_remove');
|
| 9461 |
+
btn.onclick = (ev) => { ev.stopPropagation(); dlDiscardPartial(it); dlQueueItems = dlQueueItems.filter(x => x.id !== it.id); saveDlQueue(); renderDownloadsScreen(); updateDownloadsBadge(); };
|
| 9462 |
statusWrap.appendChild(btn);
|
| 9463 |
}
|
| 9464 |
return div;
|
|
|
|
| 9466 |
|
| 9467 |
function renderDownloadsScreen(){
|
| 9468 |
const waiting = dlQueueItems.filter(it => it.status === 'waiting');
|
| 9469 |
+
const downloading = dlQueueItems.filter(it => it.status === 'downloading' || it.status === 'paused');
|
| 9470 |
const done = dlQueueItems.filter(it => it.status === 'done' || it.status === 'error');
|
| 9471 |
if (dqActiveTab === 'offline') { renderOfflineTab(waiting.length, downloading.length, done.length); updateDownloadsBadge(); return; }
|
| 9472 |
const list = dqActiveTab === 'waiting' ? waiting : dqActiveTab === 'downloading' ? downloading : done;
|
|
|
|
| 9512 |
});
|
| 9513 |
const clearDoneBtn = document.getElementById('dqClearDoneBtn');
|
| 9514 |
if (clearDoneBtn) clearDoneBtn.onclick = () => {
|
| 9515 |
+
dlQueueItems.filter(x => x.status === 'error').forEach(dlDiscardPartial);
|
| 9516 |
dlQueueItems = dlQueueItems.filter(x => x.status !== 'done' && x.status !== 'error');
|
| 9517 |
saveDlQueue();
|
| 9518 |
renderDownloadsScreen();
|
|
|
|
| 9525 |
const deleteSelectedBtn = document.getElementById('dqDeleteSelectedBtn');
|
| 9526 |
if (deleteSelectedBtn) deleteSelectedBtn.onclick = () => {
|
| 9527 |
dlQueueItems.filter(x => dqSelection.has(x.id)).forEach(bgAbortItem);
|
| 9528 |
+
dlQueueItems.filter(x => dqSelection.has(x.id)).forEach(dlDiscardPartial);
|
| 9529 |
dlQueueItems = dlQueueItems.filter(x => !dqSelection.has(x.id));
|
| 9530 |
dqSelectMode = false; dqSelection.clear();
|
| 9531 |
saveDlQueue(); renderDownloadsScreen(); updateDownloadsBadge();
|