diff --git a/lib/hosters.js b/lib/hosters.js index 73b8a8d..c941144 100644 --- a/lib/hosters.js +++ b/lib/hosters.js @@ -196,10 +196,19 @@ function parseVoeResult(payload) { // Byse: { files: [{ filecode, filename, status }] } function parseByseResult(payload) { let file_code = null; + let perFileError = null; // Primary: files array (per official Byse API docs) if (Array.isArray(payload.files) && payload.files.length > 0) { - file_code = payload.files[0].filecode || payload.files[0].file_code; + const f = payload.files[0]; + file_code = f.filecode || f.file_code || null; + // Byse returns HTTP 200 + msg=OK even when a specific file was rejected + // ("Not video file format", "Duplicate", "File too small", ...). When + // filecode is empty and status carries a non-OK message, that IS the + // actual per-file error, not a server problem. + if (!file_code && f.status && !/^(ok|success|done)$/i.test(String(f.status))) { + perFileError = String(f.status).trim(); + } } // Fallback: result object if (!file_code && payload.result) { @@ -211,6 +220,12 @@ function parseByseResult(payload) { } } + if (!file_code && perFileError) { + const err = new Error(`Byse lehnte Datei ab: ${perFileError}`); + err.fileRejected = true; + throw err; + } + return { download_url: file_code ? `https://byse.sx/d/${file_code}` : null, embed_url: file_code ? `https://byse.sx/e/${file_code}` : null, diff --git a/lib/upload-manager.js b/lib/upload-manager.js index 9abbaa3..bd0e00c 100644 --- a/lib/upload-manager.js +++ b/lib/upload-manager.js @@ -56,6 +56,17 @@ class UploadManager extends EventEmitter { this.emit('rot-log', { ts: Date.now(), event, ...data }); } + // File-specific rejections from the hoster: the same file will get rejected + // on any account, so rotation is pointless. Matches the `err.fileRejected` + // flag set by parsers plus known rejection phrases. + _isFileRejectedError(err) { + if (!err) return false; + if (err.fileRejected === true) return true; + if (!err.message) return false; + const m = String(err.message); + return /(Not video file format|Duplicate|Datei zu (klein|gross|groß)|File too (small|large)|Invalid file|Unsupported format|lehnte Datei ab)/i.test(m); + } + // Transient network errors — the account is fine, the network or the // hoster's own backend hiccuped. Retrying on the SAME account is the right // move; marking it failed would wrongly poison the fallback chain. If all @@ -530,6 +541,10 @@ class UploadManager extends EventEmitter { } lastError = err; + // File-specific rejection — re-uploading won't change the server's + // mind. Break out immediately; the outer file-rejected branch then + // records the final error without burning through 5 × 3s retries. + if (this._isFileRejectedError(err)) break; // Account-specific errors — don't waste retries on the same account, // jump straight to rotation. if (this._shouldSkipRetryOnAccountError(err)) { @@ -569,6 +584,19 @@ class UploadManager extends EventEmitter { hoster: task.hoster, fileName, accountId: task.accountId, lastError: lastError ? lastError.message : null }); + // File-specific rejection → same file will get the same verdict on + // every other account, rotation is pointless. Don't blacklist, don't + // retry siblings, just fail this file cleanly. + if (this._isFileRejectedError(lastError)) { + this._rotLog('skip-rotation-file-rejected', { + hoster: task.hoster, fileName, accountId: task.accountId, + lastError: lastError ? lastError.message : null + }); + const error = lastError.message || 'Datei abgelehnt'; + emitFinalStatus('error', { error }); + recordFinalResult('error', { error }); + return; + } // If the reason for failure was a transient network error we do NOT // blacklist the account. Other jobs on the same account in this batch // can still try fresh. This file just errors out for now.