diff --git a/docs/src/pages/troubleshooting.mdx b/docs/src/pages/troubleshooting.mdx index c2a08d3..313f9ff 100644 --- a/docs/src/pages/troubleshooting.mdx +++ b/docs/src/pages/troubleshooting.mdx @@ -54,3 +54,11 @@ Release must include all of: - `Twitch-VOD-Manager-Setup-.exe.blockmap` Tag version must match app version (example: `v3.7.6`). + +## Debug log for failed downloads + +From `v3.7.8`, detailed downloader logs are written to: + +`C:\ProgramData\Twitch_VOD_Manager\debug.log` + +If a download instantly switches from `Stoppen` back to `Start`, check the latest lines in that file for streamlink exit reasons. diff --git a/typescript-version/package-lock.json b/typescript-version/package-lock.json index 01086d4..f628402 100644 --- a/typescript-version/package-lock.json +++ b/typescript-version/package-lock.json @@ -1,12 +1,12 @@ { "name": "twitch-vod-manager", - "version": "3.7.7", + "version": "3.7.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "twitch-vod-manager", - "version": "3.7.7", + "version": "3.7.8", "license": "MIT", "dependencies": { "axios": "^1.6.0", diff --git a/typescript-version/package.json b/typescript-version/package.json index f9cfebf..b5c4498 100644 --- a/typescript-version/package.json +++ b/typescript-version/package.json @@ -1,6 +1,6 @@ { "name": "twitch-vod-manager", - "version": "3.7.7", + "version": "3.7.8", "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 66a248f..3d96f63 100644 --- a/typescript-version/src/index.html +++ b/typescript-version/src/index.html @@ -335,7 +335,7 @@

Updates

-

Version: v3.7.7

+

Version: v3.7.8

@@ -346,7 +346,7 @@
Nicht verbunden - v3.7.7 + v3.7.8 diff --git a/typescript-version/src/main.ts b/typescript-version/src/main.ts index 3d8d3c5..433ada3 100644 --- a/typescript-version/src/main.ts +++ b/typescript-version/src/main.ts @@ -8,13 +8,14 @@ import { autoUpdater } from 'electron-updater'; // ========================================== // CONFIG & CONSTANTS // ========================================== -const APP_VERSION = '3.7.7'; +const APP_VERSION = '3.7.8'; const UPDATE_CHECK_URL = 'http://24-music.de/version.json'; // Paths const APPDATA_DIR = path.join(process.env.PROGRAMDATA || 'C:\\ProgramData', 'Twitch_VOD_Manager'); const CONFIG_FILE = path.join(APPDATA_DIR, 'config.json'); const QUEUE_FILE = path.join(APPDATA_DIR, 'download_queue.json'); +const DEBUG_LOG_FILE = path.join(APPDATA_DIR, 'debug.log'); const DEFAULT_DOWNLOAD_PATH = path.join(app.getPath('desktop'), 'Twitch_VODs'); // Timeouts @@ -74,9 +75,15 @@ interface QueueItem { eta?: string; downloadedBytes?: number; totalBytes?: number; + last_error?: string; customClip?: CustomClip; } +interface DownloadResult { + success: boolean; + error?: string; +} + interface DownloadProgress { id: string; progress: number; @@ -225,6 +232,18 @@ function getFFprobePath(): string { return path.join(path.dirname(ffmpegPath), ffprobeExe); } +function appendDebugLog(message: string, details?: unknown): void { + try { + const ts = new Date().toISOString(); + const payload = details === undefined + ? '' + : ` | ${typeof details === 'string' ? details : JSON.stringify(details)}`; + fs.appendFileSync(DEBUG_LOG_FILE, `[${ts}] ${message}${payload}\n`); + } catch { + // ignore debug log errors + } +} + // ========================================== // DURATION HELPERS // ========================================== @@ -756,10 +775,11 @@ function downloadVODPart( itemId: string, partNum: number, totalParts: number -): Promise { +): Promise { return new Promise((resolve) => { const streamlinkPath = getStreamlinkPath(); const args = [url, 'best', '-o', filename, '--force']; + let lastErrorLine = ''; if (startTime) { args.push('--hls-start-offset', startTime); @@ -769,6 +789,7 @@ function downloadVODPart( } console.log('Starting download:', streamlinkPath, args); + appendDebugLog('download-part-start', { itemId, streamlinkPath, filename, args }); const proc = spawn(streamlinkPath, args, { windowsHide: true }); currentProcess = proc; @@ -828,7 +849,12 @@ function downloadVODPart( }); proc.stderr?.on('data', (data: Buffer) => { - console.error('Streamlink error:', data.toString()); + const message = data.toString().trim(); + if (message) { + lastErrorLine = message.split('\n').pop() || message; + appendDebugLog('download-part-stderr', { itemId, message: lastErrorLine }); + console.error('Streamlink error:', message); + } }); proc.on('close', (code) => { @@ -836,26 +862,36 @@ function downloadVODPart( currentProcess = null; if (currentDownloadCancelled) { - resolve(false); + appendDebugLog('download-part-cancelled', { itemId, filename }); + resolve({ success: false, error: 'Download wurde abgebrochen.' }); return; } if (code === 0 && fs.existsSync(filename)) { const stats = fs.statSync(filename); if (stats.size > 1024 * 1024) { - resolve(true); + appendDebugLog('download-part-success', { itemId, filename, bytes: stats.size }); + resolve({ success: true }); return; } + + const tooSmall = `Datei zu klein (${stats.size} Bytes)`; + appendDebugLog('download-part-failed-small-file', { itemId, filename, bytes: stats.size }); + resolve({ success: false, error: tooSmall }); + return; } - resolve(false); + const genericError = lastErrorLine || `Streamlink Exit-Code ${code ?? -1}`; + appendDebugLog('download-part-failed', { itemId, filename, code, error: genericError }); + resolve({ success: false, error: genericError }); }); proc.on('error', (err) => { clearInterval(progressInterval); console.error('Process error:', err); currentProcess = null; - resolve(false); + appendDebugLog('download-part-process-error', { itemId, error: String(err) }); + resolve({ success: false, error: String(err) }); }); }); } @@ -863,7 +899,7 @@ function downloadVODPart( async function downloadVOD( item: QueueItem, onProgress: (progress: DownloadProgress) => void -): Promise { +): Promise { const streamer = item.streamer.replace(/[^a-zA-Z0-9_-]/g, ''); const date = new Date(item.date); const dateStr = `${date.getDate().toString().padStart(2, '0')}.${(date.getMonth() + 1).toString().padStart(2, '0')}.${date.getFullYear()}`; @@ -907,7 +943,7 @@ async function downloadVOD( const partFilename = makeClipFilename(partNum, startOffset); - const success = await downloadVODPart( + const result = await downloadVODPart( item.url, partFilename, formatDuration(startOffset), @@ -918,11 +954,14 @@ async function downloadVOD( numParts ); - if (!success) return false; + if (!result.success) return result; downloadedFiles.push(partFilename); } - return downloadedFiles.length === numParts; + return { + success: downloadedFiles.length === numParts, + error: downloadedFiles.length === numParts ? undefined : 'Nicht alle Clip-Teile konnten heruntergeladen werden.' + }; } else { // Single clip file const filename = makeClipFilename(clip.startPart, clip.startSec); @@ -959,7 +998,7 @@ async function downloadVOD( const partFilename = path.join(folder, `${dateStr}_Part${(i + 1).toString().padStart(2, '0')}.mp4`); - const success = await downloadVODPart( + const result = await downloadVODPart( item.url, partFilename, formatDuration(startSec), @@ -970,20 +1009,24 @@ async function downloadVOD( numParts ); - if (!success) { - return false; + if (!result.success) { + return result; } downloadedFiles.push(partFilename); } - return downloadedFiles.length === numParts; + return { + success: downloadedFiles.length === numParts, + error: downloadedFiles.length === numParts ? undefined : 'Nicht alle Teile konnten heruntergeladen werden.' + }; } } async function processQueue(): Promise { if (isDownloading || downloadQueue.length === 0) return; + appendDebugLog('queue-start', { items: downloadQueue.length }); isDownloading = true; mainWindow?.webContents.send('download-started'); mainWindow?.webContents.send('queue-updated', downloadQueue); @@ -992,17 +1035,27 @@ async function processQueue(): Promise { if (!isDownloading) break; if (item.status === 'completed') continue; + appendDebugLog('queue-item-start', { itemId: item.id, title: item.title, url: item.url }); + currentDownloadCancelled = false; item.status = 'downloading'; saveQueue(downloadQueue); mainWindow?.webContents.send('queue-updated', downloadQueue); - const success = await downloadVOD(item, (progress) => { + item.last_error = ''; + + const result = await downloadVOD(item, (progress) => { mainWindow?.webContents.send('download-progress', progress); }); - item.status = success ? 'completed' : 'error'; - item.progress = success ? 100 : 0; + item.status = result.success ? 'completed' : 'error'; + item.progress = result.success ? 100 : 0; + item.last_error = result.success ? '' : (result.error || 'Unbekannter Fehler beim Download'); + appendDebugLog('queue-item-finished', { + itemId: item.id, + status: item.status, + error: item.last_error + }); saveQueue(downloadQueue); mainWindow?.webContents.send('queue-updated', downloadQueue); } @@ -1011,6 +1064,7 @@ async function processQueue(): Promise { saveQueue(downloadQueue); mainWindow?.webContents.send('queue-updated', downloadQueue); mainWindow?.webContents.send('download-finished'); + appendDebugLog('queue-finished', { items: downloadQueue.length }); } // ========================================== diff --git a/typescript-version/src/renderer-globals.d.ts b/typescript-version/src/renderer-globals.d.ts index bafacb0..72e0c69 100644 --- a/typescript-version/src/renderer-globals.d.ts +++ b/typescript-version/src/renderer-globals.d.ts @@ -42,6 +42,8 @@ interface QueueItem { eta?: string; downloadedBytes?: number; totalBytes?: number; + progressStatus?: string; + last_error?: string; customClip?: CustomClip; } diff --git a/typescript-version/src/renderer-queue.ts b/typescript-version/src/renderer-queue.ts index c706c2a..1fca9ca 100644 --- a/typescript-version/src/renderer-queue.ts +++ b/typescript-version/src/renderer-queue.ts @@ -19,6 +19,59 @@ async function clearCompleted(): Promise { renderQueue(); } +function getQueueStatusLabel(item: QueueItem): string { + if (item.status === 'completed') return 'Abgeschlossen'; + if (item.status === 'error') return 'Fehlgeschlagen'; + if (item.status === 'downloading') return 'Lauft'; + return 'Wartet'; +} + +function getQueueProgressText(item: QueueItem): string { + if (item.status === 'completed') return '100%'; + if (item.status === 'error') return 'Fehler'; + if (item.status === 'pending') return 'Bereit'; + if (item.progress > 0) return `${Math.max(0, Math.min(100, item.progress)).toFixed(1)}%`; + return item.progressStatus || 'Lade...'; +} + +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(`Teil ${item.currentPart}/${item.totalParts}`); + } + + if (item.speed) { + parts.push(item.speed); + } + + if (item.eta) { + parts.push(`ETA ${item.eta}`); + } + + if (!parts.length && item.status === 'pending') { + parts.push('Bereit zum Download'); + } + + if (!parts.length && item.status === 'downloading') { + parts.push(item.progressStatus || 'Download gestartet'); + } + + if (!parts.length && item.status === 'completed') { + parts.push('Fertig'); + } + + if (!parts.length && item.status === 'error') { + parts.push('Download fehlgeschlagen'); + } + + return parts.join(' | '); +} + function renderQueue(): void { if (!Array.isArray(queue)) { queue = []; @@ -34,11 +87,30 @@ function renderQueue(): void { list.innerHTML = queue.map((item: QueueItem) => { const safeTitle = escapeHtml(item.title || '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}
+
+
+
${isClip}${safeTitle}
+
${safeStatusLabel}
+
+
${safeMeta}
+
+
+
+
${safeProgressText}
+
x
`; diff --git a/typescript-version/src/renderer.ts b/typescript-version/src/renderer.ts index 81403c9..e33e9fe 100644 --- a/typescript-version/src/renderer.ts +++ b/typescript-version/src/renderer.ts @@ -31,7 +31,15 @@ async function init(): Promise { return; } + item.status = 'downloading'; item.progress = progress.progress; + item.speed = progress.speed; + item.eta = progress.eta; + item.currentPart = progress.currentPart; + item.totalParts = progress.totalParts; + item.downloadedBytes = progress.downloadedBytes; + item.totalBytes = progress.totalBytes; + item.progressStatus = progress.status; renderQueue(); }); diff --git a/typescript-version/src/styles.css b/typescript-version/src/styles.css index a79fd9a..7da2527 100644 --- a/typescript-version/src/styles.css +++ b/typescript-version/src/styles.css @@ -195,13 +195,13 @@ body { } .queue-list { - max-height: 150px; + max-height: 210px; overflow-y: auto; } .queue-item { display: flex; - align-items: center; + align-items: flex-start; gap: 10px; padding: 8px; background: var(--bg-card); @@ -234,6 +234,63 @@ body { white-space: nowrap; } +.queue-main { + flex: 1; + min-width: 0; +} + +.queue-title-row { + display: flex; + align-items: center; + gap: 8px; +} + +.queue-status-label { + flex-shrink: 0; + font-size: 10px; + color: var(--text-secondary); +} + +.queue-meta { + font-size: 10px; + color: var(--text-secondary); + margin-top: 2px; + margin-bottom: 4px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.queue-progress-wrap { + height: 4px; + border-radius: 999px; + overflow: hidden; + background: rgba(255,255,255,0.12); +} + +.queue-progress-bar { + height: 100%; + width: 0; + background: var(--accent); + transition: width 0.2s ease; +} + +.queue-progress-bar.indeterminate { + width: 35% !important; + animation: queue-indeterminate 1.3s ease-in-out infinite; +} + +.queue-progress-text { + margin-top: 3px; + font-size: 10px; + color: var(--text-secondary); +} + +@keyframes queue-indeterminate { + 0% { transform: translateX(-100%); } + 100% { transform: translateX(280%); } +} + .queue-item .remove { cursor: pointer; color: var(--error);