fix(byse): poll file list when response has empty filecode

User reported uploads appearing on the byse dashboard (2+ GB MKV,
Server #262, status OK) even though the app marked them failed. The
byse API sometimes replies with msg=OK + files:[{filecode:"",
status:"Not video file format"}] — a misleading response where the
file is actually being accepted and gets its filecode assigned
asynchronously.

  - Before the upload POST, snapshot the current /api/file/list to
    know what was already there.
  - If parseByseResult returns an empty filecode (or throws a
    fileRejected error), poll /api/file/list up to 15 × 2s looking
    for a new file_code matching the uploaded filename (case/punct
    normalized, extension stripped).
  - If matched, return the real download/embed URLs and let the
    upload complete as successful. Only throw the parser's error
    if polling also finds nothing.
This commit is contained in:
Administrator 2026-04-20 16:09:28 +02:00
parent 927fbc5895
commit 1e449e3d67

View File

@ -369,10 +369,73 @@ async function getUploadServer(hosterName, hosterConfig, apiKey, signal) {
throw new Error('Kein Upload-Server erhalten. API-Key pruefen.');
}
async function _fetchByseFileList(apiKey, signal) {
// Byse's file-list endpoint. Returns up to 100 most-recent files — enough
// to match the upload we just did against what the server has. The API
// shape is typical XFS: { status, msg, result: { files: [...] } } or
// { status, msg, files: [...] }.
const url = `https://api.byse.sx/api/file/list?key=${encodeURIComponent(apiKey)}&per_page=100&sort=date&order=desc`;
try {
const { body, statusCode } = await request(url, {
method: 'GET', signal,
headers: { 'Accept': 'application/json', 'User-Agent': 'multi-hoster-uploader/1.1' },
headersTimeout: 30_000, bodyTimeout: 30_000
});
const text = await body.text();
if (statusCode < 200 || statusCode >= 300) return [];
const data = JSON.parse(text);
const src = Array.isArray(data.files) ? data.files
: (data.result && Array.isArray(data.result.files) ? data.result.files
: (Array.isArray(data.result) ? data.result : []));
return src.map(f => ({
file_code: String(f.file_code || f.filecode || '').trim(),
file_name: String(f.title || f.name || f.file_name || '').trim()
})).filter(f => f.file_code);
} catch {
return [];
}
}
function _normalizeFileTitle(s) {
return String(s || '').toLowerCase().replace(/\.[a-z0-9]+$/i, '').replace(/[^a-z0-9]+/g, '');
}
async function _resolveByseUploadByName(apiKey, fileName, baselineCodes, signal) {
const expected = _normalizeFileTitle(fileName);
const POLL_ATTEMPTS = 15;
const POLL_DELAY_MS = 2000;
for (let i = 0; i < POLL_ATTEMPTS; i++) {
if (signal && signal.aborted) return null;
const list = await _fetchByseFileList(apiKey, signal);
const newFiles = list.filter(f => !baselineCodes.has(f.file_code));
// Prefer exact filename match (ignoring case/punctuation/extension)
const match = newFiles.find(f => _normalizeFileTitle(f.file_name) === expected)
|| (newFiles.length === 1 ? newFiles[0] : null);
if (match) {
return {
download_url: `https://byse.sx/d/${match.file_code}`,
embed_url: `https://byse.sx/e/${match.file_code}`,
file_code: match.file_code
};
}
if (i < POLL_ATTEMPTS - 1) await sleep(POLL_DELAY_MS, signal);
}
return null;
}
async function uploadFile(hosterName, filePath, apiKey, onProgress, signal, throttle) {
const config = HOSTER_CONFIGS[hosterName];
if (!config) throw new Error(`Unbekannter Hoster: ${hosterName}`);
// For byse: snapshot the current file-code list so the post-upload poller
// can identify new arrivals even when the initial POST response has an
// empty filecode.
let byseBaseline = null;
if (hosterName === 'byse.sx') {
const baseline = await _fetchByseFileList(apiKey, signal);
byseBaseline = new Set(baseline.map(f => f.file_code));
}
// Step 1: Get upload server
const uploadUrl = await getUploadServer(hosterName, config, apiKey, signal);
@ -419,11 +482,30 @@ async function uploadFile(hosterName, filePath, apiKey, onProgress, signal, thro
throw new Error(payload.msg || payload.message || JSON.stringify(payload));
}
const result = config.parseResult(payload);
if (result?.file_code || result?.download_url || result?.embed_url) {
let result = null;
let parseErr = null;
try {
result = config.parseResult(payload);
} catch (err) {
parseErr = err;
}
if (result && (result.file_code || result.download_url || result.embed_url)) {
return result;
}
// Byse-specific async handling: server accepts the file but responds with
// filecode="" + misleading status ("Not video file format"). The file shows
// up in the account shortly after — poll the list to claim it. User observed
// this with 2+ GB MKV uploads that appeared as "OK" on the byse dashboard
// even after our uploader gave up.
if (hosterName === 'byse.sx' && byseBaseline) {
const fileName = path.basename(filePath);
const polled = await _resolveByseUploadByName(apiKey, fileName, byseBaseline, signal);
if (polled) return polled;
}
if (parseErr) throw parseErr;
if (payload.success === false) {
throw new Error(payload.msg || payload.message || `Upload zu ${hosterName} wurde vom Server abgelehnt.`);
}