From 8f44211115725f390f7ae8268a0787b1efceeca8 Mon Sep 17 00:00:00 2001 From: xRangerDE Date: Sat, 14 Feb 2026 06:07:36 +0100 Subject: [PATCH] Launch v4.0.0 with reliability center and advanced queue controls Ship a major update that adds in-app preflight diagnostics with health badge, live debug log tooling, retry management, pause/resume downloads, queue reordering support, locale polish (flags + clearer retry wording), and extensive backend hardening for day-to-day stability. --- typescript-version/package-lock.json | 4 +- typescript-version/package.json | 2 +- typescript-version/src/index.html | 11 ++-- typescript-version/src/main.ts | 55 ++++++++++++++++---- typescript-version/src/preload.ts | 4 +- typescript-version/src/renderer-globals.d.ts | 4 +- typescript-version/src/renderer-locale-de.ts | 23 ++++++-- typescript-version/src/renderer-locale-en.ts | 23 ++++++-- typescript-version/src/renderer-queue.ts | 11 +++- typescript-version/src/renderer-settings.ts | 32 +++++++++--- typescript-version/src/renderer-shared.ts | 1 + typescript-version/src/renderer-texts.ts | 7 +++ typescript-version/src/renderer.ts | 3 +- typescript-version/src/styles.css | 28 ++++++++++ 14 files changed, 171 insertions(+), 37 deletions(-) diff --git a/typescript-version/package-lock.json b/typescript-version/package-lock.json index fd0ad1a..d4747fb 100644 --- a/typescript-version/package-lock.json +++ b/typescript-version/package-lock.json @@ -1,12 +1,12 @@ { "name": "twitch-vod-manager", - "version": "3.9.0", + "version": "4.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "twitch-vod-manager", - "version": "3.9.0", + "version": "4.0.0", "license": "MIT", "dependencies": { "axios": "^1.6.0", diff --git a/typescript-version/package.json b/typescript-version/package.json index 2c794a2..89c5ac6 100644 --- a/typescript-version/package.json +++ b/typescript-version/package.json @@ -1,6 +1,6 @@ { "name": "twitch-vod-manager", - "version": "3.9.0", + "version": "4.0.0", "description": "Twitch VOD Manager - Download Twitch VODs easily", "main": "dist/main.js", "author": "xRangerDE", diff --git a/typescript-version/src/index.html b/typescript-version/src/index.html index ba2109b..6d0d9f8 100644 --- a/typescript-version/src/index.html +++ b/typescript-version/src/index.html @@ -125,12 +125,13 @@
Warteschlange + System: Unbekannt 0
- +
@@ -300,8 +301,8 @@
@@ -344,7 +345,7 @@

Updates

-

Version: v3.9.0

+

Version: v4.0.0

@@ -376,7 +377,7 @@
Nicht verbunden - v3.9.0 + v4.0.0 diff --git a/typescript-version/src/main.ts b/typescript-version/src/main.ts index af33d90..fad7fda 100644 --- a/typescript-version/src/main.ts +++ b/typescript-version/src/main.ts @@ -8,7 +8,7 @@ import { autoUpdater } from 'electron-updater'; // ========================================== // CONFIG & CONSTANTS // ========================================== -const APP_VERSION = '3.9.0'; +const APP_VERSION = '4.0.0'; const UPDATE_CHECK_URL = 'http://24-music.de/version.json'; // Paths @@ -71,7 +71,7 @@ interface QueueItem { date: string; streamer: string; duration_str: string; - status: 'pending' | 'downloading' | 'completed' | 'error'; + status: 'pending' | 'downloading' | 'paused' | 'completed' | 'error'; progress: number; currentPart?: number; totalParts?: number; @@ -190,6 +190,7 @@ let downloadQueue: QueueItem[] = loadQueue(); let isDownloading = false; let currentProcess: ChildProcess | null = null; let currentDownloadCancelled = false; +let pauseRequested = false; let downloadStartTime = 0; let downloadedBytes = 0; const userIdLoginCache = new Map(); @@ -1440,12 +1441,13 @@ async function processQueue(): Promise { appendDebugLog('queue-start', { items: downloadQueue.length }); isDownloading = true; + pauseRequested = false; mainWindow?.webContents.send('download-started'); mainWindow?.webContents.send('queue-updated', downloadQueue); for (const item of downloadQueue) { - if (!isDownloading) break; - if (item.status === 'completed') continue; + if (!isDownloading || pauseRequested) break; + if (item.status === 'completed' || item.status === 'error' || item.status === 'paused') continue; appendDebugLog('queue-item-start', { itemId: item.id, title: item.title, url: item.url }); @@ -1472,8 +1474,8 @@ async function processQueue(): Promise { finalResult = result; - if (!isDownloading || currentDownloadCancelled) { - finalResult = { success: false, error: 'Download wurde abgebrochen.' }; + if (!isDownloading || currentDownloadCancelled || pauseRequested) { + finalResult = { success: false, error: pauseRequested ? 'Download wurde pausiert.' : 'Download wurde abgebrochen.' }; break; } @@ -1494,9 +1496,10 @@ async function processQueue(): Promise { } } - item.status = finalResult.success ? 'completed' : 'error'; - item.progress = finalResult.success ? 100 : 0; - item.last_error = finalResult.success ? '' : (finalResult.error || 'Unbekannter Fehler beim Download'); + const wasPaused = pauseRequested || (finalResult.error || '').includes('pausiert'); + item.status = finalResult.success ? 'completed' : (wasPaused ? 'paused' : 'error'); + item.progress = finalResult.success ? 100 : item.progress; + item.last_error = finalResult.success || wasPaused ? '' : (finalResult.error || 'Unbekannter Fehler beim Download'); appendDebugLog('queue-item-finished', { itemId: item.id, status: item.status, @@ -1507,6 +1510,7 @@ async function processQueue(): Promise { } isDownloading = false; + pauseRequested = false; saveQueue(downloadQueue); mainWindow?.webContents.send('queue-updated', downloadQueue); mainWindow?.webContents.send('download-finished'); @@ -1659,6 +1663,20 @@ ipcMain.handle('clear-completed', () => { return downloadQueue; }); +ipcMain.handle('reorder-queue', (_, orderIds: string[]) => { + const order = new Map(orderIds.map((id, idx) => [id, idx])); + const withOrder = [...downloadQueue].sort((a, b) => { + const ai = order.has(a.id) ? (order.get(a.id) as number) : Number.MAX_SAFE_INTEGER; + const bi = order.has(b.id) ? (order.get(b.id) as number) : Number.MAX_SAFE_INTEGER; + return ai - bi; + }); + + downloadQueue = withOrder; + saveQueue(downloadQueue); + mainWindow?.webContents.send('queue-updated', downloadQueue); + return downloadQueue; +}); + ipcMain.handle('retry-failed-downloads', () => { downloadQueue = downloadQueue.map((item) => { if (item.status !== 'error') return item; @@ -1682,18 +1700,35 @@ ipcMain.handle('retry-failed-downloads', () => { }); ipcMain.handle('start-download', async () => { - const hasPendingItems = downloadQueue.some(item => item.status !== 'completed'); + downloadQueue = downloadQueue.map((item) => item.status === 'paused' ? { ...item, status: 'pending' } : item); + + const hasPendingItems = downloadQueue.some(item => item.status === 'pending'); if (!hasPendingItems) { mainWindow?.webContents.send('queue-updated', downloadQueue); return false; } + saveQueue(downloadQueue); + mainWindow?.webContents.send('queue-updated', downloadQueue); + processQueue(); return true; }); +ipcMain.handle('pause-download', () => { + if (!isDownloading) return false; + + pauseRequested = true; + currentDownloadCancelled = true; + if (currentProcess) { + currentProcess.kill(); + } + return true; +}); + ipcMain.handle('cancel-download', () => { isDownloading = false; + pauseRequested = false; currentDownloadCancelled = true; if (currentProcess) { currentProcess.kill(); diff --git a/typescript-version/src/preload.ts b/typescript-version/src/preload.ts index 0d803c6..a9c0c4b 100644 --- a/typescript-version/src/preload.ts +++ b/typescript-version/src/preload.ts @@ -15,7 +15,7 @@ interface QueueItem { date: string; streamer: string; duration_str: string; - status: 'pending' | 'downloading' | 'completed' | 'error'; + status: 'pending' | 'downloading' | 'paused' | 'completed' | 'error'; progress: number; currentPart?: number; totalParts?: number; @@ -60,11 +60,13 @@ contextBridge.exposeInMainWorld('api', { getQueue: () => ipcRenderer.invoke('get-queue'), addToQueue: (item: Omit) => ipcRenderer.invoke('add-to-queue', item), removeFromQueue: (id: string) => ipcRenderer.invoke('remove-from-queue', id), + reorderQueue: (orderIds: string[]) => ipcRenderer.invoke('reorder-queue', orderIds), clearCompleted: () => ipcRenderer.invoke('clear-completed'), retryFailedDownloads: () => ipcRenderer.invoke('retry-failed-downloads'), // Download startDownload: () => ipcRenderer.invoke('start-download'), + pauseDownload: () => ipcRenderer.invoke('pause-download'), cancelDownload: () => ipcRenderer.invoke('cancel-download'), isDownloading: () => ipcRenderer.invoke('is-downloading'), downloadClip: (url: string) => ipcRenderer.invoke('download-clip', url), diff --git a/typescript-version/src/renderer-globals.d.ts b/typescript-version/src/renderer-globals.d.ts index 7de094b..1873655 100644 --- a/typescript-version/src/renderer-globals.d.ts +++ b/typescript-version/src/renderer-globals.d.ts @@ -35,7 +35,7 @@ interface QueueItem { date: string; streamer: string; duration_str: string; - status: 'pending' | 'downloading' | 'completed' | 'error'; + status: 'pending' | 'downloading' | 'paused' | 'completed' | 'error'; progress: number; currentPart?: number; totalParts?: number; @@ -112,9 +112,11 @@ interface ApiBridge { getQueue(): Promise; addToQueue(item: Omit): Promise; removeFromQueue(id: string): Promise; + reorderQueue(orderIds: string[]): Promise; clearCompleted(): Promise; retryFailedDownloads(): Promise; startDownload(): Promise; + pauseDownload(): Promise; cancelDownload(): Promise; isDownloading(): Promise; downloadClip(url: string): Promise<{ success: boolean; error?: string }>; diff --git a/typescript-version/src/renderer-locale-de.ts b/typescript-version/src/renderer-locale-de.ts index 114619b..5bbe3d3 100644 --- a/typescript-version/src/renderer-locale-de.ts +++ b/typescript-version/src/renderer-locale-de.ts @@ -7,7 +7,12 @@ const UI_TEXT_DE = { navMerge: 'Videos zusammenfugen', navSettings: 'Einstellungen', queueTitle: 'Warteschlange', - retryFailed: 'Fehler neu', + retryFailed: 'Wiederholen', + retryFailedHint: 'Nur fehlgeschlagene Downloads erneut starten', + healthUnknown: 'System: Unbekannt', + healthGood: 'System: Stabil', + healthWarn: 'System: Warnung', + healthBad: 'System: Problem', clearQueue: 'Leeren', refresh: 'Aktualisieren', streamerPlaceholder: 'Streamer hinzufugen...', @@ -22,8 +27,8 @@ const UI_TEXT_DE = { designTitle: 'Design', themeLabel: 'Theme', languageLabel: 'Sprache', - languageDe: 'DE - Deutsch', - languageEn: 'EN - Englisch', + languageDe: 'πŸ‡©πŸ‡ͺ Deutsch', + languageEn: 'πŸ‡ΊπŸ‡Έ Englisch', apiTitle: 'Twitch API', clientIdLabel: 'Client ID', clientSecretLabel: 'Client Secret', @@ -41,6 +46,14 @@ const UI_TEXT_DE = { preflightRun: 'Check ausfuhren', preflightFix: 'Auto-Fix Tools', preflightEmpty: 'Noch kein Check ausgefuhrt.', + preflightChecking: 'Prufe...', + preflightFixing: 'Fixe...', + preflightReady: 'Alles bereit.', + preflightInternet: 'Internet', + preflightStreamlink: 'Streamlink', + preflightFfmpeg: 'FFmpeg', + preflightFfprobe: 'FFprobe', + preflightPath: 'Download-Pfad', debugLogTitle: 'Live Debug-Log', refreshLog: 'Aktualisieren', autoRefresh: 'Auto-Refresh', @@ -62,10 +75,12 @@ const UI_TEXT_DE = { queue: { empty: 'Keine Downloads in der Warteschlange', start: 'Start', - stop: 'Stoppen', + stop: 'Pausieren', + resume: 'Fortsetzen', statusDone: 'Abgeschlossen', statusFailed: 'Fehlgeschlagen', statusRunning: 'Laeuft', + statusPaused: 'Pausiert', statusWaiting: 'Wartet', progressError: 'Fehler', progressReady: 'Bereit', diff --git a/typescript-version/src/renderer-locale-en.ts b/typescript-version/src/renderer-locale-en.ts index 79ef216..bd40d7a 100644 --- a/typescript-version/src/renderer-locale-en.ts +++ b/typescript-version/src/renderer-locale-en.ts @@ -7,7 +7,12 @@ const UI_TEXT_EN = { navMerge: 'Merge Videos', navSettings: 'Settings', queueTitle: 'Queue', - retryFailed: 'Retry failed', + retryFailed: 'Retry', + retryFailedHint: 'Retry failed downloads only', + healthUnknown: 'System: Unknown', + healthGood: 'System: Stable', + healthWarn: 'System: Warning', + healthBad: 'System: Problem', clearQueue: 'Clear', refresh: 'Refresh', streamerPlaceholder: 'Add streamer...', @@ -22,8 +27,8 @@ const UI_TEXT_EN = { designTitle: 'Design', themeLabel: 'Theme', languageLabel: 'Language', - languageDe: 'DE - German', - languageEn: 'EN - English', + languageDe: 'πŸ‡©πŸ‡ͺ German', + languageEn: 'πŸ‡ΊπŸ‡Έ English', apiTitle: 'Twitch API', clientIdLabel: 'Client ID', clientSecretLabel: 'Client Secret', @@ -41,6 +46,14 @@ const UI_TEXT_EN = { preflightRun: 'Run check', preflightFix: 'Auto-fix tools', preflightEmpty: 'No checks run yet.', + preflightChecking: 'Checking...', + preflightFixing: 'Fixing...', + preflightReady: 'Everything is ready.', + preflightInternet: 'Internet', + preflightStreamlink: 'Streamlink', + preflightFfmpeg: 'FFmpeg', + preflightFfprobe: 'FFprobe', + preflightPath: 'Download path', debugLogTitle: 'Live Debug Log', refreshLog: 'Refresh', autoRefresh: 'Auto refresh', @@ -62,10 +75,12 @@ const UI_TEXT_EN = { queue: { empty: 'No downloads in queue', start: 'Start', - stop: 'Stop', + stop: 'Pause', + resume: 'Resume', statusDone: 'Completed', statusFailed: 'Failed', statusRunning: 'Running', + statusPaused: 'Paused', statusWaiting: 'Waiting', progressError: 'Error', progressReady: 'Ready', diff --git a/typescript-version/src/renderer-queue.ts b/typescript-version/src/renderer-queue.ts index d17f45b..3e4a57c 100644 --- a/typescript-version/src/renderer-queue.ts +++ b/typescript-version/src/renderer-queue.ts @@ -27,6 +27,7 @@ async function retryFailedDownloads(): Promise { 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; } @@ -34,6 +35,7 @@ function getQueueStatusLabel(item: QueueItem): string { 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; @@ -62,6 +64,10 @@ function getQueueMetaText(item: QueueItem): string { 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); } @@ -84,6 +90,9 @@ function renderQueue(): void { 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; if (queue.length === 0) { list.innerHTML = `
${UI_TEXT.queue.empty}
`; @@ -124,7 +133,7 @@ function renderQueue(): void { async function toggleDownload(): Promise { if (downloading) { - await window.api.cancelDownload(); + await window.api.pauseDownload(); return; } diff --git a/typescript-version/src/renderer-settings.ts b/typescript-version/src/renderer-settings.ts index cf5d2be..cb559ae 100644 --- a/typescript-version/src/renderer-settings.ts +++ b/typescript-version/src/renderer-settings.ts @@ -42,24 +42,42 @@ function changeLanguage(lang: string): void { function renderPreflightResult(result: PreflightResult): void { const entries = [ - ['Internet', result.checks.internet], - ['Streamlink', result.checks.streamlink], - ['FFmpeg', result.checks.ffmpeg], - ['FFprobe', result.checks.ffprobe], - ['Download-Pfad', result.checks.downloadPathWritable] + [UI_TEXT.static.preflightInternet, result.checks.internet], + [UI_TEXT.static.preflightStreamlink, result.checks.streamlink], + [UI_TEXT.static.preflightFfmpeg, result.checks.ffmpeg], + [UI_TEXT.static.preflightFfprobe, result.checks.ffprobe], + [UI_TEXT.static.preflightPath, result.checks.downloadPathWritable] ]; const lines = entries.map(([name, ok]) => `${ok ? 'OK' : 'FAIL'} ${name}`).join('\n'); - const extra = result.messages.length ? `\n\n${result.messages.join('\n')}` : '\n\nAlles bereit.'; + const extra = result.messages.length ? `\n\n${result.messages.join('\n')}` : `\n\n${UI_TEXT.static.preflightReady}`; byId('preflightResult').textContent = `${lines}${extra}`; + + const badge = byId('healthBadge'); + badge.classList.remove('good', 'warn', 'bad', 'unknown'); + + if (result.ok) { + badge.classList.add('good'); + badge.textContent = UI_TEXT.static.healthGood; + return; + } + + const failCount = Object.values(result.checks).filter((ok) => !ok).length; + if (failCount <= 2) { + badge.classList.add('warn'); + badge.textContent = UI_TEXT.static.healthWarn; + } else { + badge.classList.add('bad'); + badge.textContent = UI_TEXT.static.healthBad; + } } async function runPreflight(autoFix = false): Promise { const btn = byId(autoFix ? 'btnPreflightFix' : 'btnPreflightRun'); const old = btn.textContent || ''; btn.disabled = true; - btn.textContent = autoFix ? 'Fixe...' : 'Prufe...'; + btn.textContent = autoFix ? UI_TEXT.static.preflightFixing : UI_TEXT.static.preflightChecking; try { const result = await window.api.runPreflight(autoFix); diff --git a/typescript-version/src/renderer-shared.ts b/typescript-version/src/renderer-shared.ts index 9493b5b..05535c0 100644 --- a/typescript-version/src/renderer-shared.ts +++ b/typescript-version/src/renderer-shared.ts @@ -39,3 +39,4 @@ let clipTotalSeconds = 0; let updateReady = false; let debugLogAutoRefreshTimer: number | null = null; +let draggedQueueItemId: string | null = null; diff --git a/typescript-version/src/renderer-texts.ts b/typescript-version/src/renderer-texts.ts index e861b08..04348a0 100644 --- a/typescript-version/src/renderer-texts.ts +++ b/typescript-version/src/renderer-texts.ts @@ -31,6 +31,11 @@ function setPlaceholder(id: string, value: string): void { if (node) node.placeholder = value; } +function setTitle(id: string, value: string): void { + const node = document.getElementById(id); + if (node) node.setAttribute('title', value); +} + function setLanguage(lang: string): LanguageCode { currentLanguage = lang === 'en' ? 'en' : 'de'; UI_TEXT = UI_TEXTS[currentLanguage]; @@ -46,7 +51,9 @@ function applyLanguageToStaticUI(): void { setText('navMergeText', UI_TEXT.static.navMerge); setText('navSettingsText', UI_TEXT.static.navSettings); setText('queueTitleText', UI_TEXT.static.queueTitle); + setText('healthBadge', UI_TEXT.static.healthUnknown); setText('btnRetryFailed', UI_TEXT.static.retryFailed); + setTitle('btnRetryFailed', UI_TEXT.static.retryFailedHint); setText('btnClear', UI_TEXT.static.clearQueue); setText('refreshText', UI_TEXT.static.refresh); setText('clipsHeading', UI_TEXT.static.clipsHeading); diff --git a/typescript-version/src/renderer.ts b/typescript-version/src/renderer.ts index 24dc277..fe9733c 100644 --- a/typescript-version/src/renderer.ts +++ b/typescript-version/src/renderer.ts @@ -117,7 +117,8 @@ function mergeQueueState(nextQueue: QueueItem[]): QueueItem[] { function updateDownloadButtonState(): void { const btn = byId('btnStart'); - btn.textContent = downloading ? UI_TEXT.queue.stop : UI_TEXT.queue.start; + const hasPaused = queue.some((item) => item.status === 'paused'); + btn.textContent = downloading ? UI_TEXT.queue.stop : (hasPaused ? UI_TEXT.queue.resume : UI_TEXT.queue.start); btn.classList.toggle('downloading', downloading); } diff --git a/typescript-version/src/styles.css b/typescript-version/src/styles.css index e96c375..e4c4aa3 100644 --- a/typescript-version/src/styles.css +++ b/typescript-version/src/styles.css @@ -179,6 +179,7 @@ body { justify-content: space-between; align-items: center; margin-bottom: 10px; + gap: 8px; } .queue-title { @@ -199,6 +200,33 @@ body { overflow-y: auto; } +.health-badge { + font-size: 10px; + padding: 2px 8px; + border-radius: 999px; + border: 1px solid transparent; + white-space: nowrap; +} + +.health-badge.good { + background: rgba(0, 200, 83, 0.2); + border-color: rgba(0, 200, 83, 0.45); + color: #93efb9; +} + +.health-badge.warn { + background: rgba(255, 171, 0, 0.2); + border-color: rgba(255, 171, 0, 0.45); + color: #ffd98e; +} + +.health-badge.bad, +.health-badge.unknown { + background: rgba(255, 68, 68, 0.2); + border-color: rgba(255, 68, 68, 0.45); + color: #ffaaaa; +} + .queue-item { display: flex; align-items: flex-start;