fix(doodstream): recover codeless API uploads by polling the file list by name
Backstop for the API path: if the doodapi upload POST returns no filecode (the
same backend registration hiccup that empties the web form), poll
doodapi.co/api/file/list for a newly-appeared file whose normalized title
matches what we uploaded, and claim its code — instead of failing the upload.
This is the exact recovery byse already uses in this file for the identical
symptom (large MKV, server-side "OK" but empty immediate response, file shows up
in the account shortly after). Doodstream is the same XFileSharing family with
the same doodapi-style API, and it directly addresses the user's observation
that the same file often succeeds on a second run.
- _fetchDoodstreamFileList / _resolveDoodstreamUploadByName: list via
/api/file/list?key=&per_page=200, baseline-diff + exact normalized-title match
(never "take the only new one", so parallel uploads can't claim each other's
files), 12 polls × 2.5s.
- uploadFile snapshots a doodstream baseline before upload and polls after a
codeless result, before the hosterTransient throw.
Verified solo: doodapi.co is reachable and returns {"status":400,"msg":"Invalid
key"} for a bad key, so the validation/list path keys off status correctly.
178/178. The real large-file run on the server is the final confirmation.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
6286bca7c6
commit
fc48f20db5
@ -446,6 +446,54 @@ async function _resolveByseUploadByName(apiKey, fileName, baselineCodes, signal)
|
|||||||
return null;
|
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) {
|
async function uploadFile(hosterName, filePath, apiKey, onProgress, signal, throttle) {
|
||||||
const config = HOSTER_CONFIGS[hosterName];
|
const config = HOSTER_CONFIGS[hosterName];
|
||||||
if (!config) throw new Error(`Unbekannter Hoster: ${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);
|
const baseline = await _fetchByseFileList(apiKey, signal);
|
||||||
byseBaseline = new Set(baseline.map(f => f.file_code));
|
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
|
// Step 1: Get upload server
|
||||||
const uploadUrl = await getUploadServer(hosterName, config, apiKey, signal);
|
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;
|
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 (parseErr) throw parseErr;
|
||||||
|
|
||||||
if (payload.success === false) {
|
if (payload.success === false) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user