diff --git a/package-lock.json b/package-lock.json index c080585..bbf543a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "twitch-vod-manager", - "version": "5.0.11", + "version": "5.0.12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "twitch-vod-manager", - "version": "5.0.11", + "version": "5.0.12", "license": "MIT", "dependencies": { "axios": "^1.16.1", diff --git a/package.json b/package.json index 9990baa..4494ddd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "twitch-vod-manager", - "version": "5.0.11", + "version": "5.0.12", "description": "Twitch VOD Manager - Download Twitch VODs easily", "main": "dist/main.js", "author": "xRangerDE", diff --git a/src/main.ts b/src/main.ts index c050cd1..37bb626 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3043,7 +3043,12 @@ function downloadVODPart( onProgress: (progress: DownloadProgress) => void, itemId: string, partNum: number, - totalParts: number + totalParts: number, + /** Erwartete Dauer in Sekunden fuer den Progress-Estimate. Wenn endTime + gesetzt ist, ueberschrieben aus dort. Wenn startTime und endTime null + sind (Full-VOD), kann Caller hier die VOD-Gesamtdauer reingeben, + damit der Bar nicht in indeterminate haengt. 0 = unknown. */ + expectedTotalSec: number = 0 ): Promise { return new Promise((resolve) => { const streamlinkCmd = getStreamlinkCommand(); @@ -3127,9 +3132,31 @@ function downloadVODPart( } } + // Bytes-basierte Schaetzung statt progress=-1, damit die Bar + // determinate bleibt + kontinuierlich waechst. Wenn streamlink + // spaeter eine echte % rausgibt (Path B), wird die ueber den + // bytes-Estimate gelegt (siehe lastStreamlinkPercent-Logik + // im stdout-handler). + // Quelle: endTime (--hls-duration arg) ODER expectedTotalSec + // Param (fuer Full-VOD wo Caller die Dauer kennt). + const expectedDurationSecForEstimate = parseClockDurationSeconds(endTime) || expectedTotalSec; + const expectedBytes = expectedDurationSecForEstimate > 0 ? expectedDurationSecForEstimate * 625_000 : 0; + let progressEstimate: number; + if (lastStreamlinkPercent > 0) { + // Streamlink hat % rausgegeben — vertrau dem (genauer als bytes). + progressEstimate = lastStreamlinkPercent; + } else if (expectedBytes > 0 && downloadedBytes > 0) { + // Bytes-Fallback: cap bei 95% damit der Bar nicht 100% + // vor dem tatsaechlichen Abschluss hinrennt. + progressEstimate = Math.min(95, (downloadedBytes / expectedBytes) * 100); + } else { + // Keine Info -> echtes Unknown, Bar geht in indeterminate. + progressEstimate = -1; + } + onProgress({ id: itemId, - progress: -1, // Unknown total + progress: progressEstimate, speed: formatSpeed(speed), eta: etaStr, status: tBackend('statusBytesDownloaded', { bytes: formatBytes(downloadedBytes) }), @@ -5323,7 +5350,9 @@ async function downloadVOD( // Check download mode if (config.download_mode === 'full' || totalDuration <= config.part_minutes * 60) { - // Full download + // Full download — totalDuration als expectedTotalSec damit der Bar + // determinate-progress aus bytes/duration schaetzen kann (statt in + // indeterminate-Animation zu haengen). const filename = ensureUniqueFilename(makeTemplateFilename( config.filename_template_vod, DEFAULT_FILENAME_TEMPLATE_VOD, @@ -5331,13 +5360,18 @@ async function downloadVOD( 0, totalDuration ), item.id); - const result = await downloadVODPart(item.url, filename, null, null, onProgress, item.id, 1, 1); + const result = await downloadVODPart(item.url, filename, null, null, onProgress, item.id, 1, 1, totalDuration); return result.success ? { ...result, outputFiles: [filename] } : result; } else { - // Part-based download + // Part-based download — wrappt onProgress mit einem Aggregator, der + // pro Part den letzten bekannten %-Wert haelt und einen weighted + // overallProgress (0-100%) zurueck an die UI emittiert. Ohne den + // Wrapper sah die UI nur "Part X bei Y%" und der Bar sprang bei + // Part-Wechsel von 100% zurueck auf 0%. const partDuration = config.part_minutes * 60; const numParts = Math.ceil(totalDuration / partDuration); const downloadedFiles: string[] = []; + const partProgresses: number[] = Array(numParts).fill(0); for (let i = 0; i < numParts; i++) { if (cancelledItemIds.has(item.id)) break; @@ -5359,16 +5393,32 @@ async function downloadVOD( partFilename, formatDuration(startSec), formatDuration(duration), - onProgress, + (progress) => { + // Per-part %-Update — clampen, NaN/negativ filtern + if (Number.isFinite(progress.progress) && progress.progress > 0 && progress.progress <= 100) { + partProgresses[i] = Math.max(partProgresses[i], progress.progress); + } + // Overall: avg ueber alle Parts (parts haben gleiche + // Dauer per Definition, also avg = weighted avg) + const overall = partProgresses.reduce((s, p) => s + p, 0) / numParts; + onProgress({ + ...progress, + progress: overall, + currentPart: i + 1, + totalParts: numParts + }); + }, item.id, i + 1, - numParts + numParts, + duration ); if (!result.success) { return result; } + partProgresses[i] = 100; downloadedFiles.push(partFilename); } @@ -5445,12 +5495,19 @@ async function processDownloadMergeGroup( const vodWeight = vodDuration / totalDurationSec; const priorWeight = mg.items.slice(0, i).reduce((s, v) => s + parseDuration(v.duration_str), 0) / totalDurationSec; - // Persistente per-part vodProgress, damit Path-A-Ticks (progress=-1, - // 1s-rhythmus mit unknown-total) den determinate Bar nicht auf - // priorWeight zuruecksetzen. Wir merken uns die letzte positive - // Streamlink-Prozentangabe (Path B) und nutzen sie, bis ein neuer - // Wert kommt. Ohne das oszilliert die Bar zwischen indeterminate - // und priorWeight, optisch eingefroren. + // Geschaetzte Bytes pro Part fuer den Fallback-Progress: Twitch- + // VOD Bitrate ~5 Mbit/s = ~625 KB/s. Wenn streamlink-stdout keine + // %-Lines emittiert (HLS ohne known total), nutzen wir + // downloadedBytes / estimatedTotalBytes als rough progress. Cap + // bei 95% damit der Bar nie 100% vorm tatsaechlichen Done erreicht. + const estimatedTotalBytes = Math.max(1, vodDuration * 625_000); + + // Persistente per-part vodProgress. Quelle 1: streamlink stdout % + // (genau). Quelle 2: downloadedBytes / estimated (Fallback wenn + // % nicht reportet wird). Ohne den Fallback haengte der Bar auf + // dem indeterminate-Pattern (animierte 35%-Box) waehrend tatsaechlich + // schon ein paar 100 MB unten waren — User sieht das als "fest mittig + // links" weil die Animation schnell ist und nur Snapshots zeigen. let lastVodProgress = 0; const result = await downloadVODPart( vodItem.url, @@ -5460,6 +5517,14 @@ async function processDownloadMergeGroup( (progress) => { if (progress.progress > 0 && progress.progress <= 100) { lastVodProgress = progress.progress; + } else if (progress.downloadedBytes && progress.downloadedBytes > 0) { + // Fallback: bytes-basierte Schaetzung. Streamlink-stdout-% + // bleibt bevorzugt; bytes-Fallback wird nur genutzt wenn + // noch nie ein echter % rein kam (lastVodProgress noch 0). + if (lastVodProgress === 0) { + const bytePct = Math.min(95, (progress.downloadedBytes / estimatedTotalBytes) * 100); + lastVodProgress = bytePct; + } } // Weighted progress: download phase = 0-70% const overallProgress = (priorWeight + vodWeight * (lastVodProgress / 100)) * 70;