fix(doodstream): upload via the doodapi API when an API key exists

Root cause of the recurring "kein Filecode — Server gab leeren Link zurueck":
the web-session upload flow gets the filecode back inside an XFileSharing HTML
form, and on long/large uploads that form comes back empty (no fn). Verified
research: doodstream's server-side file-registration callback times out under
large-file load, so the upload "succeeds" (bytes sent, HTTP 200) but no filecode
is minted — and because registration failed, the file is NOT in the file list
either, so polling can't recover it. The web path also rides a per-page-load
sess_id token that ages over the multi-minute upload.

The official doodapi.co JSON API has no such failure mode for result retrieval:
the upload response returns result[0].filecode directly, and it authenticates
with a persistent api_key (no aging sess_id). Git history confirms the API was
doodstream's ORIGINAL upload path (initial commit); web login was added later
only "as an alternative to API key" — so preferring the key restores the
intended primary path rather than fighting a deliberate choice.

- lib/account-auth.js (new, pure, unit-tested): selectUploadAuth() prefers the
  doodstream API key over username/password; all other hosters unchanged.
- main.js buildTaskFromAccount delegates to it → a doodstream account with an
  apiKey now routes through hosters.uploadFile (doodapi API) instead of the web
  uploader; keyless accounts keep using web login.
- hosters.js: drop the stale hardcoded fallback node from the doodstream API
  config (same dead tr1128ve host removed from the web path) so a failed server
  lookup throws cleanly instead of uploading into a dead end.
- Tests: 8 routing cases (doodstream key-preference, keyless fallback, voe
  unaffected, authType=api, null-safety). Full suite 173/173.

This eliminates the empty-form failure mode for result retrieval when a key is
configured. It does NOT change doodstream's backend — whether the large-file
timeout recurs (now as a structured JSON error, not a silent empty form) is for
the server run to confirm. Requires a doodstream API key on the account.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Administrator 2026-05-28 21:42:19 +02:00
parent 13de55253b
commit a8d81cbf0d
4 changed files with 86 additions and 10 deletions

36
lib/account-auth.js Normal file
View File

@ -0,0 +1,36 @@
// Decides which credential an upload task should use for a given hoster.
// Extracted from main.js buildTaskFromAccount so the routing can be unit-tested
// without Electron.
//
// DOODSTREAM SPECIAL CASE: prefer the official doodapi.co API key whenever the
// account has one. The web-login path (username/password) drives doodstream's
// browser upload flow, which hands the filecode back inside an XFileSharing
// HTML form. On long/large uploads that form comes back empty (no fn) because a
// per-page-load sess_id token ages out over the multi-minute upload and/or the
// server-side file-registration callback times out — the upload then "succeeds"
// (bytes sent, HTTP 200) but yields no link. The JSON API returns the filecode
// directly in result[0].filecode and authenticates with a persistent api_key,
// so it has no empty-form failure mode for result retrieval. The API path was
// doodstream's ORIGINAL upload path (present since the initial commit); web
// login was added later only as an alternative for keyless accounts — so
// preferring the key here restores the intended primary path, it doesn't fight
// a deliberate choice. Keyless accounts keep using web login unchanged.
function selectUploadAuth(hoster, account) {
if (!account || typeof account !== 'object') return {};
if (hoster === 'doodstream.com' && account.apiKey) {
return { apiKey: account.apiKey };
}
if (account.authType === 'api' && account.apiKey) {
return { apiKey: account.apiKey };
}
if (account.username && account.password) {
return { username: account.username, password: account.password };
}
if (account.apiKey) {
return { apiKey: account.apiKey };
}
return {};
}
module.exports = { selectUploadAuth };

View File

@ -34,7 +34,10 @@ const HOSTER_CONFIGS = {
'doodstream.com': { 'doodstream.com': {
apiBase: 'https://doodapi.co', apiBase: 'https://doodapi.co',
serverEndpoints: ['/api/upload/server'], serverEndpoints: ['/api/upload/server'],
fallbackUploadServers: ['https://tr1128ve.cloudatacdn.com/upload/01'], // No hardcoded fallback node: that stale CDN host (tr1128ve.cloudatacdn.com)
// accepts the bytes but returns an empty result form with no filecode, so a
// failed server lookup must throw cleanly rather than upload ~1 GB into a
// dead end. (Same reasoning as the web-session path's fail-fast.)
buildUploadUrl: (url, key) => appendRawQuery(url, key), buildUploadUrl: (url, key) => appendRawQuery(url, key),
formFields: (key) => ({ api_key: key }), formFields: (key) => ({ api_key: key }),
parseResult: parseDoodstreamResult parseResult: parseDoodstreamResult

11
main.js
View File

@ -8,6 +8,7 @@ const { HOSTER_CONFIGS } = require('./lib/hosters');
const VidmolyUploader = require('./lib/vidmoly-upload'); const VidmolyUploader = require('./lib/vidmoly-upload');
const VoeUploader = require('./lib/voe-upload'); const VoeUploader = require('./lib/voe-upload');
const DoodstreamUploader = require('./lib/doodstream-upload'); const DoodstreamUploader = require('./lib/doodstream-upload');
const { selectUploadAuth } = require('./lib/account-auth');
const ClouddropUploader = require('./lib/clouddrop-upload'); const ClouddropUploader = require('./lib/clouddrop-upload');
const { checkForUpdate, installUpdate, abortUpdate } = require('./lib/updater'); const { checkForUpdate, installUpdate, abortUpdate } = require('./lib/updater');
const backupCrypto = require('./lib/backup-crypto'); const backupCrypto = require('./lib/backup-crypto');
@ -568,15 +569,7 @@ function getNextFallbackAccount(config, hosterName, failedAccountId) {
} }
function buildTaskFromAccount(hoster, account, extra) { function buildTaskFromAccount(hoster, account, extra) {
const task = { ...extra, hoster, accountId: account.id }; const task = { ...extra, hoster, accountId: account.id, ...selectUploadAuth(hoster, account) };
if (account.authType === 'api' && account.apiKey) {
task.apiKey = account.apiKey;
} else if (account.username && account.password) {
task.username = account.username;
task.password = account.password;
} else if (account.apiKey) {
task.apiKey = account.apiKey;
}
return task; return task;
} }

View File

@ -0,0 +1,44 @@
const { test } = require('node:test');
const assert = require('node:assert');
const { selectUploadAuth } = require('../lib/account-auth');
test('doodstream prefers the API key even when username/password are also set', () => {
const auth = selectUploadAuth('doodstream.com', {
apiKey: 'KEY123', username: 'u', password: 'p'
});
assert.deepEqual(auth, { apiKey: 'KEY123' }); // API path — no username leaks through
});
test('doodstream with only username/password uses web login (keyless fallback)', () => {
const auth = selectUploadAuth('doodstream.com', { username: 'u', password: 'p' });
assert.deepEqual(auth, { username: 'u', password: 'p' });
});
test('doodstream with empty apiKey + creds falls back to web login (no false API route)', () => {
const auth = selectUploadAuth('doodstream.com', { apiKey: '', username: 'u', password: 'p' });
assert.deepEqual(auth, { username: 'u', password: 'p' });
});
test('doodstream with nothing usable returns empty', () => {
assert.deepEqual(selectUploadAuth('doodstream.com', { apiKey: '', username: '', password: '' }), {});
});
test('voe.sx is unaffected by the doodstream special-case: username/password wins', () => {
// voe also supports both, but the empty-form bug is doodstream-specific; do
// not change voe routing.
const auth = selectUploadAuth('voe.sx', { apiKey: 'VKEY', username: 'u', password: 'p' });
assert.deepEqual(auth, { username: 'u', password: 'p' });
});
test('authType=api forces the API key for any hoster', () => {
assert.deepEqual(selectUploadAuth('voe.sx', { authType: 'api', apiKey: 'K', username: 'u', password: 'p' }), { apiKey: 'K' });
});
test('api-key-only account (no creds) uses the key', () => {
assert.deepEqual(selectUploadAuth('byse.sx', { apiKey: 'BKEY' }), { apiKey: 'BKEY' });
});
test('null / non-object account does not throw', () => {
assert.deepEqual(selectUploadAuth('doodstream.com', null), {});
assert.deepEqual(selectUploadAuth('doodstream.com', undefined), {});
});