diff --git a/lib/hosters.js b/lib/hosters.js index c941144..84517e5 100644 --- a/lib/hosters.js +++ b/lib/hosters.js @@ -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.`); }