Compare commits

..

No commits in common. "287ebde1f50adfce493b0d7941d14cd00188b9e0" and "d2f903b8ba9af97793bf942f8b3aff7907c44701" have entirely different histories.

5 changed files with 8 additions and 179 deletions

View File

@ -247,10 +247,7 @@ class DoodstreamUploader {
} else { } else {
_debugLog('upload_server: form action found but no sess_id on page; keeping existing sessId'); _debugLog('upload_server: form action found but no sess_id on page; keeping existing sessId');
} }
// Capture the form's real fields so upload() submits exactly what the _debugLog(`upload_server: using form action node=${url} sess=${this.sessId}`);
// browser would (file_title, submit_btn, …) instead of stale hardcoded ones.
this._uploadFormFields = this._parseUploadFormFields(html);
_debugLog(`upload_server: using form action node=${url} sess=${this.sessId} fields=${Object.keys(this._uploadFormFields).join(',')}`);
return url; return url;
} }
@ -272,35 +269,6 @@ class DoodstreamUploader {
); );
} }
/**
* Replicate the non-file fields of doodstream's CURRENT upload form so our
* POST matches what the browser actually submits. Doodstream dropped the old
* `utype` field and added file_title / fakefilepc / submit_btn; submitting a
* stale/incomplete field set can make the node accept the bytes but skip
* registration ( empty result form). We parse the live form rather than
* hardcode, so we track whatever fields doodstream uses now. The file input
* (type=file) is excluded the file is streamed separately.
*/
_parseUploadFormFields(html) {
const fields = {};
if (!html) return fields;
// Narrow to the upload form (its action points at a /upload/ node).
const formMatch = html.match(/<form[^>]*\baction=["'][^"']*\/upload\/[^"']*["'][\s\S]*?<\/form>/i);
const scope = formMatch ? formMatch[0] : html;
const re = /<(?:input|button)\b([^>]*)>/gi;
let m;
while ((m = re.exec(scope)) !== null) {
const attrs = m[1];
const typeM = attrs.match(/\btype=["']([^"']*)["']/i);
if (typeM && typeM[1].toLowerCase() === 'file') continue;
const nameM = attrs.match(/\bname=["']([^"']+)["']/i);
if (!nameM) continue;
const valM = attrs.match(/\bvalue=["']([^"']*)["']/i);
fields[nameM[1]] = valM ? valM[1] : '';
}
return fields;
}
/** /**
* Upload file using web session * Upload file using web session
*/ */
@ -317,16 +285,10 @@ class DoodstreamUploader {
// Build multipart form // Build multipart form
const boundary = `----WebKitFormBoundary${crypto.randomBytes(16).toString('hex')}`; const boundary = `----WebKitFormBoundary${crypto.randomBytes(16).toString('hex')}`;
// Build form parts. Submit the live form's fields (parsed in // Build form parts
// _getUploadServer) so our POST matches the browser; merge in sess_id (the
// fresh node token) and keep utype=reg as a harmless compatibility extra.
// Falls back to the minimal known-good set if the form wasn't parsed.
const formFields = { utype: 'reg', ...(this._uploadFormFields || {}) };
formFields.sess_id = this.sessId;
let preamble = ''; let preamble = '';
for (const [name, value] of Object.entries(formFields)) { preamble += `--${boundary}\r\nContent-Disposition: form-data; name="sess_id"\r\n\r\n${this.sessId}\r\n`;
preamble += `--${boundary}\r\nContent-Disposition: form-data; name="${name}"\r\n\r\n${value}\r\n`; preamble += `--${boundary}\r\nContent-Disposition: form-data; name="utype"\r\n\r\nreg\r\n`;
}
const safeFileName = fileName.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); const safeFileName = fileName.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
preamble += `--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${safeFileName}"\r\nContent-Type: application/octet-stream\r\n\r\n`; preamble += `--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${safeFileName}"\r\nContent-Type: application/octet-stream\r\n\r\n`;

View File

@ -468,8 +468,6 @@ async function _fetchDoodstreamFileList(apiKey, signal) {
} }
} }
const DOODSTREAM_POLL = { attempts: 12, delayMs: 2500 }; // test-tunable via __test
async function _resolveDoodstreamUploadByName(apiKey, fileName, baselineCodes, signal) { async function _resolveDoodstreamUploadByName(apiKey, fileName, baselineCodes, signal) {
// Same recovery byse uses: the upload POST returned no filecode, but the file // 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 // may register in the account a little later. Poll the list for a NEW file
@ -477,8 +475,8 @@ async function _resolveDoodstreamUploadByName(apiKey, fileName, baselineCodes, s
// (never "take the only new one") so parallel doodstream uploads can't claim // (never "take the only new one") so parallel doodstream uploads can't claim
// each other's files. // each other's files.
const expected = _normalizeFileTitle(fileName); const expected = _normalizeFileTitle(fileName);
const POLL_ATTEMPTS = DOODSTREAM_POLL.attempts; const POLL_ATTEMPTS = 12;
const POLL_DELAY_MS = DOODSTREAM_POLL.delayMs; const POLL_DELAY_MS = 2500;
for (let i = 0; i < POLL_ATTEMPTS; i++) { for (let i = 0; i < POLL_ATTEMPTS; i++) {
if (signal && signal.aborted) return null; if (signal && signal.aborted) return null;
const list = await _fetchDoodstreamFileList(apiKey, signal); const list = await _fetchDoodstreamFileList(apiKey, signal);
@ -640,7 +638,6 @@ module.exports = {
extractUploadServerUrl, extractUploadServerUrl,
parseVoeResult, parseVoeResult,
parseDoodstreamResult, parseDoodstreamResult,
parseByseResult, parseByseResult
DOODSTREAM_POLL
} }
}; };

View File

@ -1,6 +1,6 @@
{ {
"name": "multi-hoster-uploader", "name": "multi-hoster-uploader",
"version": "3.3.33", "version": "3.3.32",
"description": "Upload files to doodstream, voe, vidmoly, byse simultaneously", "description": "Upload files to doodstream, voe, vidmoly, byse simultaneously",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {

View File

@ -1,105 +0,0 @@
const { test, before, after } = require('node:test');
const assert = require('node:assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
// Mock the undici transport BEFORE requiring hosters so the destructured
// `request` picks up our stub. apiGet (getUploadServer) uses global fetch, which
// we override per-test. This exercises the FULL doodstream API upload + recovery
// orchestration against the doc-verified response shapes — the gap between the
// already-tested parseDoodstreamResult helper and the real uploadFile path.
// (mock.module needs an experimental flag npm test doesn't pass, so we reassign
// undici.request on the module object and refresh the hosters cache instead.)
let requestRouter = async () => ({ statusCode: 200, headers: {}, body: { text: async () => '{}' } });
const undici = require('undici');
const _origUndiciRequest = undici.request;
undici.request = (...a) => requestRouter(...a);
delete require.cache[require.resolve('../lib/hosters')];
const hostersMod = require('../lib/hosters');
const { uploadFile } = hostersMod;
let tmpFile;
let origFetch;
before(() => {
tmpFile = path.join(os.tmpdir(), `dood-itest-${process.pid}.mkv`);
fs.writeFileSync(tmpFile, Buffer.alloc(2048, 7));
origFetch = global.fetch;
// Keep the "never appears" recovery test fast (real default is 12 × 2.5 s).
hostersMod.__test.DOODSTREAM_POLL.attempts = 3;
hostersMod.__test.DOODSTREAM_POLL.delayMs = 5;
});
after(() => {
global.fetch = origFetch;
undici.request = _origUndiciRequest; // restore real transport for other test files
delete require.cache[require.resolve('../lib/hosters')];
try { fs.unlinkSync(tmpFile); } catch {}
});
// getUploadServer hits /api/upload/server via global fetch.
function stubUploadServer() {
global.fetch = async (url) => {
if (/upload\/server/.test(String(url))) {
return { status: 200, text: async () => JSON.stringify({ status: 200, result: 'https://node1.cloudatacdn.com/upload/01' }) };
}
return { status: 200, text: async () => '{"status":200}' };
};
}
// Build an undici-style router. uploadBody is the POST result; listBodies is a
// queue consumed by successive /api/file/list calls (baseline, then polls).
function routeWith(uploadBody, listBodies = []) {
return async (url, opts) => {
const u = String(url);
if (/\/api\/file\/list/.test(u)) {
const body = listBodies.length ? listBodies.shift() : '{"status":200,"result":{"files":[]}}';
return { statusCode: 200, headers: {}, body: { text: async () => body } };
}
// Upload POST: drain the streamed body so the file handle closes.
if (opts && opts.body && typeof opts.body[Symbol.asyncIterator] === 'function') {
for await (const chunk of opts.body) { if (chunk && chunk.length === -1) break; }
}
return { statusCode: uploadBody.status, headers: { 'content-type': 'application/json' }, body: { text: async () => uploadBody.body } };
};
}
test('doodstream API upload: filecode returned directly is used', async () => {
stubUploadServer();
requestRouter = routeWith({
status: 200,
body: JSON.stringify({ status: 200, result: [{ filecode: 'DOODCODE1234', download_url: 'https://doodstream.com/d/DOODCODE1234', protected_embed: 'https://doodstream.com/e/DOODCODE1234' }] })
});
const res = await uploadFile('doodstream.com', tmpFile, 'VALIDKEY', null, null, null);
assert.equal(res.file_code, 'DOODCODE1234');
assert.equal(res.download_url, 'https://doodstream.com/d/DOODCODE1234');
});
test('doodstream API upload: codeless result recovered via file-list name match', async () => {
stubUploadServer();
const fileName = path.basename(tmpFile).replace(/\.[^.]+$/, ''); // title doodstream stores
requestRouter = routeWith(
{ status: 200, body: JSON.stringify({ status: 200, msg: 'OK' }) }, // codeless upload
[
'{"status":200,"result":{"files":[]}}', // baseline (pre-upload)
`{"status":200,"result":{"files":[{"file_code":"RECOVER9999","title":"${fileName}"}]}}` // poll finds it
]
);
const res = await uploadFile('doodstream.com', tmpFile, 'VALIDKEY', null, null, null);
assert.equal(res.file_code, 'RECOVER9999');
assert.equal(res.download_url, 'https://doodstream.com/d/RECOVER9999');
});
test('doodstream API upload: codeless + file never appears → throws hosterTransient (no account poison)', async () => {
stubUploadServer();
requestRouter = routeWith(
{ status: 200, body: JSON.stringify({ status: 200, msg: 'OK' }) },
[] // every file/list returns empty
);
await assert.rejects(
() => uploadFile('doodstream.com', tmpFile, 'VALIDKEY', null, null, null),
(err) => {
assert.equal(err.hosterTransient, true, 'codeless result must be tagged hosterTransient');
return true;
}
);
});

View File

@ -64,31 +64,6 @@ test('happy path: link in result page wins', async () => {
assert.equal(res.file_code, 'jjsuhr931ds9'); assert.equal(res.file_code, 'jjsuhr931ds9');
}); });
// --- _parseUploadFormFields: replicate the current upload form faithfully ---
test('_parseUploadFormFields extracts the real form fields and excludes the file input', () => {
const up = new DoodstreamUploader();
const html = `
<form name="file" enctype="multipart/form-data" action="https://uxg.cloudatacdn.com/upload/01?TOK" method="post">
<input type="hidden" name="sess_id" value="TOK">
<input name="file" type="file" size="30" id="filepc">
<input name="fakefilepc" class="d-none" type="text" id="fakefilepc">
<input type="text" name="file_title" class="form-control">
<button type="submit" name="submit_btn" class="btn">Upload</button>
</form>`;
const f = up._parseUploadFormFields(html);
assert.equal(f.sess_id, 'TOK');
assert.equal(f.fakefilepc, '');
assert.equal(f.file_title, '');
assert.ok('submit_btn' in f);
assert.ok(!('file' in f), 'the file input must be excluded (streamed separately)');
});
test('_parseUploadFormFields returns {} for markup without a form', () => {
const up = new DoodstreamUploader();
assert.deepEqual(up._parseUploadFormFields('<div>no form here</div>'), {});
assert.deepEqual(up._parseUploadFormFields(''), {});
});
// --- deriveApiKey: pull + validate the account API key from the web session --- // --- deriveApiKey: pull + validate the account API key from the web session ---
test('_extractApiKeyCandidates finds the key in an input value and ranks api-context first', () => { test('_extractApiKeyCandidates finds the key in an input value and ranks api-context first', () => {
const up = new DoodstreamUploader(); const up = new DoodstreamUploader();