Compare commits

..

No commits in common. "5afb56b987f9e224745b8611d8a10738466a6a57" and "c70f105685ad02995a78b0654296f0541fcb5b98" have entirely different histories.

4 changed files with 1 additions and 135 deletions

View File

@ -64,19 +64,6 @@ class UploadManager extends EventEmitter {
return this._accountOverrides.get(hoster) || null;
}
// True if the hoster has a usable override stored that differs from the
// account currently in the task and isn't itself already marked failed.
// Used by the retry loop to decide "retry on same account vs break to
// rotation" — skipping wasted attempts on a likely-bad primary when a
// pre-resolved fallback is ready to try.
_hasPendingOverride(hoster, currentAccountId) {
const override = this._accountOverrides.get(hoster);
if (!override) return false;
if (override.id === currentAccountId) return false;
if (this._failedAccounts.has(hoster + ':' + override.id)) return false;
return true;
}
_rotLog(event, data) {
this.emit('rot-log', { ts: Date.now(), event, ...data });
}
@ -608,19 +595,6 @@ class UploadManager extends EventEmitter {
});
break;
}
// Generic non-transient error AND a fallback is already resolved for
// this hoster: bail to rotation instead of burning more retries on a
// possibly-dead primary. The fallback (pre-resolved at batch-start)
// deserves a real shot. Transient network errors stay on the same
// account — the network is the issue, not the account.
if (!this._isTransientNetworkError(err) &&
this._hasPendingOverride(task.hoster, task.accountId)) {
this._rotLog('try-alternate-after-fail', {
jobId, hoster: task.hoster, fileName, accountId: task.accountId,
attempt, error: err && err.message ? err.message : String(err)
});
break;
}
if (attempt >= maxAttempts) break;
// Wait 3 seconds before retry
await this._sleep(3000, signal);

20
main.js
View File

@ -1133,26 +1133,6 @@ ipcMain.handle('start-upload', (_event, payload) => {
if (tasks.length === 0) return { error: 'Keine gültigen Zugangsdaten für die gewählten Hoster.', skippedJobs };
// Pre-resolve a fallback for every hoster that has one. Lets the upload
// manager break out of the retry loop after a single generic failure and
// try the alternate account immediately, instead of hammering a probably-
// dead primary 5× before the account-failed event even fires. Doesn't
// trigger pre-job-swap (which only fires when the current account is in
// _failedAccounts), so jobs still start on the primary as expected.
const hostersInBatch = new Set(tasks.map(t => t.hoster).filter(Boolean));
for (const hoster of hostersInBatch) {
if (_sessionAccountOverrides.has(hoster)) continue; // already learned from past batch
const accounts = config.hosters && config.hosters[hoster];
if (!Array.isArray(accounts) || accounts.length < 2) continue;
const primary = accounts.find(a => a && a.enabled !== false && hosterAccountHasCreds(hoster, a));
if (!primary) continue;
const next = getNextFallbackAccount(config, hoster, primary.id);
if (next) {
_sessionAccountOverrides.set(hoster, next);
rotLog(`main: pre-resolved fallback for ${hoster}${next.id} (primary ${primary.id} will try acc2 on first failure)`);
}
}
// Fresh collector for this new batch — old entries from the previous
// batch's jobs are dropped (user's signal for "fresh log" is starting a
// new upload; addJobs during a running batch keeps them).

View File

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

View File

@ -692,94 +692,6 @@ describe('UploadManager', () => {
assert.equal(acc1Calls, 0, 'primed-dead acc1 must not receive any upload attempts');
});
it('generic error + pre-resolved override: rotates after 1 attempt (no more 5x on primary)', async () => {
// acc1 throws a generic non-transient, non-account-specific error.
// acc2 succeeds. With a pre-resolved override (from main.js at batch
// start), the retry loop must break after 1 attempt on acc1 and rotate.
let acc1Calls = 0;
let acc2Calls = 0;
mockUploadFile.mock.mockImplementation(async (hoster, filePath, apiKey, onProgress) => {
if (apiKey === 'acc1-key') {
acc1Calls++;
throw new Error('VOE Upload: irgendein generischer Fehler');
}
acc2Calls++;
if (onProgress) onProgress(fakeFileSize, fakeFileSize);
return { download_url: 'ok', embed_url: null, file_code: 'ok' };
});
const mgr = new UploadManager(
{ 'voe.sx': { retries: 5, parallelCount: 1, maxSpeedKbs: 0, restartBelowKbs: 0, timeIntervalSec: 0, maxSizeMb: 0 } }
);
const events = [];
mgr.on('rot-log', (e) => events.push(e.event));
await mgr.startBatch([
{ file: '/test/a.mp4', hoster: 'voe.sx', apiKey: 'acc1-key', accountId: 'acc1' }
], {
// Main.js pre-resolves the fallback at batch start.
primeOverrides: [['voe.sx', { id: 'acc2', apiKey: 'acc2-key' }]]
});
assert.equal(acc1Calls, 1, 'acc1 must get exactly 1 attempt before rotation kicks in');
assert.ok(acc2Calls >= 1, 'acc2 must take over');
assert.ok(events.includes('try-alternate-after-fail'),
`expected try-alternate-after-fail; got: ${events.join(',')}`);
});
it('transient error + pre-resolved override: retries SAME acc (network, not acc, is the issue)', async () => {
let acc1Calls = 0;
let acc2Calls = 0;
mockUploadFile.mock.mockImplementation(async (hoster, filePath, apiKey, onProgress) => {
if (apiKey === 'acc1-key') {
acc1Calls++;
if (acc1Calls <= 2) throw new Error('connect ECONNRESET 1.2.3.4:443');
if (onProgress) onProgress(fakeFileSize, fakeFileSize);
return { download_url: 'ok-on-acc1-retry', embed_url: null, file_code: 'ok' };
}
acc2Calls++;
if (onProgress) onProgress(fakeFileSize, fakeFileSize);
return { download_url: 'ok', embed_url: null, file_code: 'ok' };
});
const mgr = new UploadManager(
{ 'voe.sx': { retries: 5, parallelCount: 1, maxSpeedKbs: 0, restartBelowKbs: 0, timeIntervalSec: 0, maxSizeMb: 0 } }
);
const events = [];
mgr.on('rot-log', (e) => events.push(e.event));
await mgr.startBatch([
{ file: '/test/a.mp4', hoster: 'voe.sx', apiKey: 'acc1-key', accountId: 'acc1' }
], {
primeOverrides: [['voe.sx', { id: 'acc2', apiKey: 'acc2-key' }]]
});
assert.equal(acc1Calls, 3, 'transient must retry same acc until success');
assert.equal(acc2Calls, 0, 'must NOT rotate away on transient network errors');
assert.ok(!events.includes('try-alternate-after-fail'),
`transient should NOT trigger try-alternate-after-fail; got: ${events.join(',')}`);
});
it('generic error + NO override: falls back to classic retry on same account', async () => {
let acc1Calls = 0;
mockUploadFile.mock.mockImplementation(async (hoster, filePath, apiKey, onProgress) => {
acc1Calls++;
throw new Error('something generic');
});
const mgr = new UploadManager(
{ 'voe.sx': { retries: 3, parallelCount: 1, maxSpeedKbs: 0, restartBelowKbs: 0, timeIntervalSec: 0, maxSizeMb: 0 } }
);
await mgr.startBatch([
{ file: '/test/a.mp4', hoster: 'voe.sx', apiKey: 'acc1-key', accountId: 'acc1' }
]);
// retries=3 → maxAttempts=4 (retries + 1). Without an override to rotate
// to, must exhaust all 4 attempts on acc1.
assert.equal(acc1Calls, 4, 'single-account hoster must retry N+1 times on same account');
});
it('startBatch without prime opts still clears state (back-compat)', async () => {
const mgr = new UploadManager({});
mgr._failedAccounts.set('byse.sx:acc1', true);