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",
|
"name": "twitch-vod-manager",
|
||||||
"version": "5.0.11",
|
"version": "5.0.12",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "twitch-vod-manager",
|
"name": "twitch-vod-manager",
|
||||||
"version": "5.0.11",
|
"version": "5.0.12",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.16.1",
|
"axios": "^1.16.1",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "twitch-vod-manager",
|
"name": "twitch-vod-manager",
|
||||||
"version": "5.0.11",
|
"version": "5.0.12",
|
||||||
"description": "Twitch VOD Manager - Download Twitch VODs easily",
|
"description": "Twitch VOD Manager - Download Twitch VODs easily",
|
||||||
"main": "dist/main.js",
|
"main": "dist/main.js",
|
||||||
"author": "xRangerDE",
|
"author": "xRangerDE",
|
||||||
|
|||||||
91
src/main.ts
91
src/main.ts
@ -3043,7 +3043,12 @@ function downloadVODPart(
|
|||||||
onProgress: (progress: DownloadProgress) => void,
|
onProgress: (progress: DownloadProgress) => void,
|
||||||
itemId: string,
|
itemId: string,
|
||||||
partNum: number,
|
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> {
|
): Promise<DownloadResult> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const streamlinkCmd = getStreamlinkCommand();
|
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({
|
onProgress({
|
||||||
id: itemId,
|
id: itemId,
|
||||||
progress: -1, // Unknown total
|
progress: progressEstimate,
|
||||||
speed: formatSpeed(speed),
|
speed: formatSpeed(speed),
|
||||||
eta: etaStr,
|
eta: etaStr,
|
||||||
status: tBackend('statusBytesDownloaded', { bytes: formatBytes(downloadedBytes) }),
|
status: tBackend('statusBytesDownloaded', { bytes: formatBytes(downloadedBytes) }),
|
||||||
@ -5323,7 +5350,9 @@ async function downloadVOD(
|
|||||||
|
|
||||||
// Check download mode
|
// Check download mode
|
||||||
if (config.download_mode === 'full' || totalDuration <= config.part_minutes * 60) {
|
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(
|
const filename = ensureUniqueFilename(makeTemplateFilename(
|
||||||
config.filename_template_vod,
|
config.filename_template_vod,
|
||||||
DEFAULT_FILENAME_TEMPLATE_VOD,
|
DEFAULT_FILENAME_TEMPLATE_VOD,
|
||||||
@ -5331,13 +5360,18 @@ async function downloadVOD(
|
|||||||
0,
|
0,
|
||||||
totalDuration
|
totalDuration
|
||||||
), item.id);
|
), 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;
|
return result.success ? { ...result, outputFiles: [filename] } : result;
|
||||||
} else {
|
} 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 partDuration = config.part_minutes * 60;
|
||||||
const numParts = Math.ceil(totalDuration / partDuration);
|
const numParts = Math.ceil(totalDuration / partDuration);
|
||||||
const downloadedFiles: string[] = [];
|
const downloadedFiles: string[] = [];
|
||||||
|
const partProgresses: number[] = Array(numParts).fill(0);
|
||||||
|
|
||||||
for (let i = 0; i < numParts; i++) {
|
for (let i = 0; i < numParts; i++) {
|
||||||
if (cancelledItemIds.has(item.id)) break;
|
if (cancelledItemIds.has(item.id)) break;
|
||||||
@ -5359,16 +5393,32 @@ async function downloadVOD(
|
|||||||
partFilename,
|
partFilename,
|
||||||
formatDuration(startSec),
|
formatDuration(startSec),
|
||||||
formatDuration(duration),
|
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,
|
item.id,
|
||||||
i + 1,
|
i + 1,
|
||||||
numParts
|
numParts,
|
||||||
|
duration
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
partProgresses[i] = 100;
|
||||||
downloadedFiles.push(partFilename);
|
downloadedFiles.push(partFilename);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -5445,12 +5495,19 @@ async function processDownloadMergeGroup(
|
|||||||
const vodWeight = vodDuration / totalDurationSec;
|
const vodWeight = vodDuration / totalDurationSec;
|
||||||
const priorWeight = mg.items.slice(0, i).reduce((s, v) => s + parseDuration(v.duration_str), 0) / 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,
|
// Geschaetzte Bytes pro Part fuer den Fallback-Progress: Twitch-
|
||||||
// 1s-rhythmus mit unknown-total) den determinate Bar nicht auf
|
// VOD Bitrate ~5 Mbit/s = ~625 KB/s. Wenn streamlink-stdout keine
|
||||||
// priorWeight zuruecksetzen. Wir merken uns die letzte positive
|
// %-Lines emittiert (HLS ohne known total), nutzen wir
|
||||||
// Streamlink-Prozentangabe (Path B) und nutzen sie, bis ein neuer
|
// downloadedBytes / estimatedTotalBytes als rough progress. Cap
|
||||||
// Wert kommt. Ohne das oszilliert die Bar zwischen indeterminate
|
// bei 95% damit der Bar nie 100% vorm tatsaechlichen Done erreicht.
|
||||||
// und priorWeight, optisch eingefroren.
|
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;
|
let lastVodProgress = 0;
|
||||||
const result = await downloadVODPart(
|
const result = await downloadVODPart(
|
||||||
vodItem.url,
|
vodItem.url,
|
||||||
@ -5460,6 +5517,14 @@ async function processDownloadMergeGroup(
|
|||||||
(progress) => {
|
(progress) => {
|
||||||
if (progress.progress > 0 && progress.progress <= 100) {
|
if (progress.progress > 0 && progress.progress <= 100) {
|
||||||
lastVodProgress = progress.progress;
|
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%
|
// Weighted progress: download phase = 0-70%
|
||||||
const overallProgress = (priorWeight + vodWeight * (lastVodProgress / 100)) * 70;
|
const overallProgress = (priorWeight + vodWeight * (lastVodProgress / 100)) * 70;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user