diff --git a/lib/hosters.js b/lib/hosters.js index 434fbe5..97dbf0f 100644 --- a/lib/hosters.js +++ b/lib/hosters.js @@ -446,6 +446,54 @@ async function _resolveByseUploadByName(apiKey, fileName, baselineCodes, signal) return null; } +async function _fetchDoodstreamFileList(apiKey, signal) { + // doodapi.co file list: { msg, status:200, result: { files: [{ file_code, title, uploaded, ... }] } } + const url = `https://doodapi.co/api/file/list?key=${encodeURIComponent(apiKey)}&per_page=200`; + 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 files = data && data.result && Array.isArray(data.result.files) ? data.result.files : []; + return files.map(f => ({ + file_code: String(f.file_code || f.filecode || '').trim(), + file_name: String(f.title || f.file_name || f.name || '').trim() + })).filter(f => f.file_code); + } catch { + return []; + } +} + +async function _resolveDoodstreamUploadByName(apiKey, fileName, baselineCodes, signal) { + // Same recovery byse uses: the upload POST returned no filecode, but the file + // may register in the account a little later. Poll the list for a NEW file + // whose normalized title matches what we uploaded. Exact-name match only + // (never "take the only new one") so parallel doodstream uploads can't claim + // each other's files. + const expected = _normalizeFileTitle(fileName); + const POLL_ATTEMPTS = 12; + const POLL_DELAY_MS = 2500; + for (let i = 0; i < POLL_ATTEMPTS; i++) { + if (signal && signal.aborted) return null; + const list = await _fetchDoodstreamFileList(apiKey, signal); + const fresh = list.filter(f => !baselineCodes.has(f.file_code)); + const match = fresh.find(f => _normalizeFileTitle(f.file_name) === expected); + if (match) { + return { + download_url: `https://doodstream.com/d/${match.file_code}`, + embed_url: `https://doodstream.com/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}`); @@ -458,6 +506,13 @@ async function uploadFile(hosterName, filePath, apiKey, onProgress, signal, thro const baseline = await _fetchByseFileList(apiKey, signal); byseBaseline = new Set(baseline.map(f => f.file_code)); } + // Doodstream: same snapshot so a codeless upload response can be recovered by + // matching a newly-appeared file in the account by name (see below). + let doodBaseline = null; + if (hosterName === 'doodstream.com') { + const baseline = await _fetchDoodstreamFileList(apiKey, signal); + doodBaseline = new Set(baseline.map(f => f.file_code)); + } // Step 1: Get upload server const uploadUrl = await getUploadServer(hosterName, config, apiKey, signal); @@ -537,6 +592,15 @@ async function uploadFile(hosterName, filePath, apiKey, onProgress, signal, thro if (polled) return polled; } + // Doodstream: the doodapi upload POST returned no filecode (the same backend + // hiccup that empties the web form). Poll the account file list by name — if + // the file did register, claim its code instead of failing the upload. + if (hosterName === 'doodstream.com' && doodBaseline) { + const fileName = path.basename(filePath); + const polled = await _resolveDoodstreamUploadByName(apiKey, fileName, doodBaseline, signal); + if (polled) return polled; + } + if (parseErr) throw parseErr; if (payload.success === false) {