release: 5.0.5 — streamlink Resilience + bessere Fehler-Diagnose
Problem: User berichtet 'streamlink exit code 1' bei VOD-Downloads ohne sinnvolle Fehlermeldung — UI zeigt 'Retrying in 8s (unknown)...'. Root Cause: classifyDownloadError matched die echten Twitch-Errors nicht und nur die letzte stderr-Zeile wurde im Debug-Log gespeichert. Fixes: - Volle stderr-History wird gepuffert + im download-part-failed Debug-Log als stderrTail (letzte 2000 chars) gespeichert - UI bekommt jetzt die echte streamlink Error-Zeile statt 'Streamlink Fehlercode N' (prefer 'error:'-prefixed Zeilen, dann last non-bracket non-INFO line) - classifyDownloadError matcht jetzt zusaetzlich: 'no playable streams', 'could not find any kind of stream', 'access token', 'session token', 'signature', 'integrity token', 'subscriber only', 'sub-only', 'not subscribed', 'http error', 'connectionerror', 'readerror' Streamlink-Args: - --stream-segment-attempts 5 (default 3 — mehr Retries bei flaky CDN) - --stream-segment-timeout 20 - --stream-timeout 120 - --retry-streams 3 (retry initial stream listing) - --retry-max 2 Damit ueberlebt der Download transiente Twitch-CDN-Hicks und der User sieht im naechsten Fail die echte Fehlerursache in der UI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
261aaa362e
commit
e270e1ec12
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "twitch-vod-manager",
|
||||
"version": "5.0.4",
|
||||
"version": "5.0.5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "twitch-vod-manager",
|
||||
"version": "5.0.4",
|
||||
"version": "5.0.5",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^1.6.0",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "twitch-vod-manager",
|
||||
"version": "5.0.4",
|
||||
"version": "5.0.5",
|
||||
"description": "Twitch VOD Manager - Download Twitch VODs easily",
|
||||
"main": "dist/main.js",
|
||||
"author": "xRangerDE",
|
||||
|
||||
63
src/main.ts
63
src/main.ts
@ -1338,11 +1338,18 @@ function classifyDownloadError(errorMessage: string): RetryErrorClass {
|
||||
|
||||
if (text.includes('ungueltige vod-url') || text.includes('invalid vod url')) return 'validation';
|
||||
if (text.includes('429') || text.includes('rate limit') || text.includes('too many requests')) return 'rate_limit';
|
||||
if (text.includes('401') || text.includes('403') || text.includes('unauthorized') || text.includes('forbidden')) return 'auth';
|
||||
if (text.includes('timed out') || text.includes('timeout') || text.includes('network') || text.includes('connection') || text.includes('dns')) return 'network';
|
||||
if (text.includes('401') || text.includes('403') || text.includes('unauthorized') || text.includes('forbidden') || text.includes('subscriber only') || text.includes('sub-only') || text.includes('not subscribed')) return 'auth';
|
||||
if (text.includes('timed out') || text.includes('timeout') || text.includes('network') || text.includes('connection') || text.includes('dns') || text.includes('http error') || text.includes('connectionerror') || text.includes('readerror')) return 'network';
|
||||
if (text.includes('streamlink nicht gefunden') || text.includes('streamlink not found') || text.includes('streamlink is missing') || text.includes('ffmpeg') || text.includes('ffprobe') || text.includes('enoent')) return 'tooling';
|
||||
if (text.includes('integritaet') || text.includes('integrity') || text.includes('kein videostream') || text.includes('no video stream')) return 'integrity';
|
||||
if (text.includes('access denied') || text.includes('permission') || text.includes('disk') || text.includes('file') || text.includes('ordner') || text.includes('folder')) return 'io';
|
||||
// Twitch-spezifische streamlink errors:
|
||||
// "error: No playable streams found on this URL" — VOD weg / private / sub-only
|
||||
// "error: Could not find any kind of stream" — gleich
|
||||
// "error: Unable to validate session token" — Twitch-API rejected
|
||||
// "error: Unable to fetch access token" — Auth pre-flight failed
|
||||
if (text.includes('no playable streams') || text.includes('could not find any kind of stream')) return 'validation';
|
||||
if (text.includes('access token') || text.includes('session token') || text.includes('signature') || text.includes('integrity token')) return 'auth';
|
||||
|
||||
return 'unknown';
|
||||
}
|
||||
@ -3043,7 +3050,19 @@ function downloadVODPart(
|
||||
// in the VOD output. Off only if the user explicitly disabled it.
|
||||
args.push('--twitch-disable-ads');
|
||||
}
|
||||
// HLS-Segment-Resilience: bei vereinzelten CDN-Fehlern weiter retrien,
|
||||
// statt komplett zu sterben. Twitch hat 2025/26 oefter transiente 403/
|
||||
// timeout-Errors auf einzelne HLS-Segments. Default ist 3 — 5 ist ein
|
||||
// pragmatischer Kompromiss zwischen Resilience und Failing-Fast.
|
||||
args.push('--stream-segment-attempts', '5');
|
||||
args.push('--stream-segment-timeout', '20');
|
||||
args.push('--stream-timeout', '120');
|
||||
// Streamlink-Plugin retry: bei "stream not found on URL"-Erstabfrage
|
||||
// einmal nachhaken, bevor wir den ganzen Run failen.
|
||||
args.push('--retry-streams', '3');
|
||||
args.push('--retry-max', '2');
|
||||
let lastErrorLine = '';
|
||||
const stderrBuffer: string[] = [];
|
||||
const expectedDurationSeconds = parseClockDurationSeconds(endTime);
|
||||
let lastStreamlinkPercent = 0;
|
||||
|
||||
@ -3146,10 +3165,26 @@ function downloadVODPart(
|
||||
});
|
||||
|
||||
proc.stderr?.on('data', (data: Buffer) => {
|
||||
const message = data.toString().trim();
|
||||
if (message) {
|
||||
lastErrorLine = message.split('\n').pop() || message;
|
||||
appendDebugLog('download-part-stderr', { itemId, message: lastErrorLine });
|
||||
const message = data.toString();
|
||||
if (message.trim()) {
|
||||
stderrBuffer.push(message);
|
||||
// Bounded buffer — wir wollen nicht 100MB stderr in RAM bei einem
|
||||
// streamlink-loop. 200 chunks reichen fuer normale Diagnose.
|
||||
if (stderrBuffer.length > 200) stderrBuffer.shift();
|
||||
// Letzte echte Errorzeile fuer User-Surface. "[ ... ] log lines"
|
||||
// ueberspringen, "error: ..." bevorzugen damit nicht ein triviales
|
||||
// INFO-Statement als User-facing-Fehler landet.
|
||||
const lines = message.split('\n').map(l => l.trim()).filter(Boolean);
|
||||
for (const line of lines) {
|
||||
const lower = line.toLowerCase();
|
||||
if (lower.startsWith('error:') || lower.includes('error:')) {
|
||||
lastErrorLine = line;
|
||||
} else if (!lastErrorLine && line.length > 0 && !lower.startsWith('[')) {
|
||||
// Fallback: jede non-bracket non-INFO Zeile
|
||||
lastErrorLine = line;
|
||||
}
|
||||
}
|
||||
appendDebugLog('download-part-stderr', { itemId, message: message.trim() });
|
||||
console.error('Streamlink error:', message);
|
||||
}
|
||||
});
|
||||
@ -3192,9 +3227,19 @@ function downloadVODPart(
|
||||
return;
|
||||
}
|
||||
|
||||
const genericError = lastErrorLine || tBackend('streamlinkExitCode', { code: String(code ?? -1) });
|
||||
appendDebugLog('download-part-failed', { itemId, filename, code, error: genericError });
|
||||
resolve({ success: false, error: genericError });
|
||||
// Volle stderr-History im Debug-Log (kollabiert) — fuer Forensik
|
||||
// wenn der User schreibt "es funktioniert nicht". User-facing
|
||||
// genericError ist die echte streamlink-Fehlerzeile, nicht nur
|
||||
// "Streamlink Fehlercode 1".
|
||||
const fullStderr = stderrBuffer.join('').trim();
|
||||
const userFacingError = lastErrorLine
|
||||
|| (fullStderr.split('\n').filter(l => l.trim()).pop() || '').trim()
|
||||
|| tBackend('streamlinkExitCode', { code: String(code ?? -1) });
|
||||
appendDebugLog('download-part-failed', {
|
||||
itemId, filename, code, error: userFacingError,
|
||||
stderrTail: fullStderr.slice(-2000),
|
||||
});
|
||||
resolve({ success: false, error: userFacingError });
|
||||
});
|
||||
|
||||
proc.on('error', (err) => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user