function buildQueueFingerprint(url: string, streamer: string, date: string, customClip?: CustomClip): string { const clipFingerprint = customClip ? [ 'clip', customClip.startSec, customClip.durationSec, customClip.startPart, customClip.filenameFormat, (customClip.filenameTemplate || '').trim().toLowerCase() ].join(':') : 'vod'; return [ (url || '').trim().toLowerCase().replace(/^https?:\/\/(www\.)?/, ''), (streamer || '').trim().toLowerCase(), (date || '').trim(), clipFingerprint ].join('|'); } let lastQueueRenderFingerprint = ''; function getQueueRenderFingerprint(items: QueueItem[]): string { const lang = typeof currentLanguage === 'string' ? currentLanguage : 'en'; const pieces = items.map((item) => [ item.id, item.status, Math.round((Number(item.progress) || 0) * 10), item.currentPart || 0, item.totalParts || 0, item.speed || '', item.eta || '', item.progressStatus || '', item.last_error || '' ].join(':')); return `${lang}|${pieces.join('|')}`; } function hasActiveQueueDuplicate(url: string, streamer: string, date: string, customClip?: CustomClip): boolean { const target = buildQueueFingerprint(url, streamer, date, customClip); return queue.some((item) => { if (item.status !== 'pending' && item.status !== 'downloading' && item.status !== 'paused') { return false; } return buildQueueFingerprint(item.url, item.streamer, item.date, item.customClip) === target; }); } async function addToQueue(url: string, title: string, date: string, streamer: string, duration: string): Promise { if ((config.prevent_duplicate_downloads as boolean) !== false && hasActiveQueueDuplicate(url, streamer, date)) { alert(UI_TEXT.queue.duplicateSkipped); return; } queue = await window.api.addToQueue({ url, title, date, streamer, duration_str: duration }); renderQueue(); } async function removeFromQueue(id: string): Promise { queue = await window.api.removeFromQueue(id); renderQueue(); } async function clearCompleted(): Promise { queue = await window.api.clearCompleted(); renderQueue(); } async function retryFailedDownloads(): Promise { queue = await window.api.retryFailedDownloads(); renderQueue(); } function getQueueStatusLabel(item: QueueItem): string { if (item.status === 'completed') return UI_TEXT.queue.statusDone; if (item.status === 'error') return UI_TEXT.queue.statusFailed; if (item.status === 'paused') return UI_TEXT.queue.statusPaused; if (item.status === 'downloading') return UI_TEXT.queue.statusRunning; return UI_TEXT.queue.statusWaiting; } function getQueueProgressText(item: QueueItem): string { if (item.status === 'completed') return '100%'; if (item.status === 'error') return UI_TEXT.queue.progressError; if (item.status === 'paused') return UI_TEXT.queue.progressReady; if (item.status === 'pending') return UI_TEXT.queue.progressReady; if (item.progress > 0) return `${Math.max(0, Math.min(100, item.progress)).toFixed(1)}%`; return item.progressStatus || UI_TEXT.queue.progressLoading; } function getQueueMetaText(item: QueueItem): string { if (item.status === 'error' && item.last_error) { return item.last_error; } const parts: string[] = []; if (item.currentPart && item.totalParts) { parts.push(`${UI_TEXT.queue.part} ${item.currentPart}/${item.totalParts}`); } if (item.speed) { parts.push(`${UI_TEXT.queue.speed}: ${item.speed}`); } if (item.eta) { parts.push(`${UI_TEXT.queue.eta}: ${item.eta}`); } if (!parts.length && item.status === 'pending') { parts.push(UI_TEXT.queue.readyToDownload); } if (!parts.length && item.status === 'paused') { parts.push(UI_TEXT.queue.statusPaused); } if (!parts.length && item.status === 'downloading') { parts.push(item.progressStatus || UI_TEXT.queue.started); } if (!parts.length && item.status === 'completed') { parts.push(UI_TEXT.queue.done); } if (!parts.length && item.status === 'error') { parts.push(UI_TEXT.queue.failed); } return parts.join(' | '); } function renderQueue(): void { if (!Array.isArray(queue)) { queue = []; } const list = byId('queueList'); byId('queueCount').textContent = String(queue.length); const retryBtn = byId('btnRetryFailed'); const hasFailed = queue.some((item) => item.status === 'error'); retryBtn.disabled = !hasFailed; const renderFingerprint = getQueueRenderFingerprint(queue); if (renderFingerprint === lastQueueRenderFingerprint) { return; } if (queue.length === 0) { lastQueueRenderFingerprint = renderFingerprint; list.innerHTML = `
${UI_TEXT.queue.empty}
`; return; } list.innerHTML = queue.map((item: QueueItem) => { const safeTitle = escapeHtml(item.title || UI_TEXT.vods.untitled); const safeStatusLabel = escapeHtml(getQueueStatusLabel(item)); const safeProgressText = escapeHtml(getQueueProgressText(item)); const safeMeta = escapeHtml(getQueueMetaText(item)); const isClip = item.customClip ? '* ' : ''; const hasDeterminateProgress = item.progress > 0 && item.progress <= 100; const progressValue = item.status === 'completed' ? 100 : (hasDeterminateProgress ? Math.max(0, Math.min(100, item.progress)) : 0); const progressClass = item.status === 'downloading' && !hasDeterminateProgress ? ' indeterminate' : ''; return `
${isClip}${safeTitle}
${safeStatusLabel}
${safeMeta}
${safeProgressText}
x
`; }).join(''); lastQueueRenderFingerprint = renderFingerprint; } async function toggleDownload(): Promise { if (downloading) { await window.api.pauseDownload(); return; } const started = await window.api.startDownload(); if (!started) { renderQueue(); alert(UI_TEXT.queue.emptyAlert); } }