release: 5.0.12 — Progress-Logik komplett neu geschrieben
Root Cause (User-Report): bei Part 1/6 Multi-Part-Download mit 869 MB schon geladen, blieb der Bar auf einer fixen Position weit links — sah aus als ob ein paar % erreicht waeren obwohl viel mehr lief. Drei Probleme die das aus 5.0.1-5.0.11 noch ueberlebt haben: 1. downloadVODPart Path A (1-Sek-Heartbeat) emittierte progress=-1 wenn HLS kein known total hat. UI flippte zwischen 'determinate bei letztem streamlink-%' und 'indeterminate-Animation' — User sah einen 35%-Bar der zwischen Streamlink-%-Updates kurz aufpoppt. 2. Part-based-Split (download_mode='parts', langes VOD in N Stunden- Parts) hat downloadVODPart's onProgress UNGEWRAPPT an die UI gegeben. Bar zeigte 'X% von Part i' statt 'gewichtete overall %'. Bei Part-Wechsel sprang er von 100% zurueck auf 0%. Bei Part 1 mit 50% Stream-Progress zeigte der Bar 50% obwohl overall erst bei 8.3% (1 von 6 Parts halb fertig). 3. Full-VOD-Download (kein --hls-duration) hatte kein expected total fuer den bytes-Estimate -> blieb in indeterminate-Mode. Fixes: - downloadVODPart bekommt optionalen Parameter. Path A schaetzt jetzt progress aus downloadedBytes / (duration * ~625 KB/s, Twitch ~5 Mbit/s Schaetzung), gecappt bei 95%. Wenn streamlink-stdout-% rauskommt, override mit dem (genauer). Nur noch progress=-1 wenn weder bytes noch streamlink-% verfuegbar. - Part-based-Split (downloadVodWithStrategy) wrappt onProgress jetzt mit einem Aggregator: pro Part den max-bekannten %-Wert merken, overallProgress = avg(allParts). Bei Part-Done aus dem Loop partProgresses[i] = 100 setzen. Bar steigt monoton von 0% auf 100% ueber alle Parts. - Full-VOD-Download passt totalDuration als expectedTotalSec an downloadVODPart, sodass auch hier der bytes-Estimate greift. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a2d9215b22
commit
7bc7ef84a2
4
package-lock.json
generated
4
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
91
src/main.ts
91
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<DownloadResult> {
|
||||
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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user