Compare commits
No commits in common. "141bfd3658b0c71463bd6bf499e436ba688d8d9e" and "1616ee8f14a62264ab5b8dec055b905d0756eedd" have entirely different histories.
141bfd3658
...
1616ee8f14
@ -227,7 +227,7 @@ class UploadManager extends EventEmitter {
|
|||||||
return this.semaphores[hoster];
|
return this.semaphores[hoster];
|
||||||
}
|
}
|
||||||
|
|
||||||
async startBatch(tasks, opts = {}) {
|
async startBatch(tasks) {
|
||||||
this.running = true;
|
this.running = true;
|
||||||
this.stopAfterActive = false;
|
this.stopAfterActive = false;
|
||||||
this.abortController = new AbortController();
|
this.abortController = new AbortController();
|
||||||
@ -240,25 +240,13 @@ class UploadManager extends EventEmitter {
|
|||||||
this.globalSemaphore = null;
|
this.globalSemaphore = null;
|
||||||
this.globalThrottle = null;
|
this.globalThrottle = null;
|
||||||
this.lastStartTime = {};
|
this.lastStartTime = {};
|
||||||
// Reset account-rotation state each batch — but optionally re-prime from
|
// Reset account-rotation state each batch. Otherwise a previously failed
|
||||||
// app-session memory so a "Retry failed" right after batch-done doesn't
|
// account (e.g. rate-limited during the last batch) stays permanently
|
||||||
// burn 5 retries on the account we already know is dead. Caller (main.js)
|
// blacklisted until the app restarts — every upload would silently skip
|
||||||
// passes the session-scoped failed/override state.
|
// straight to the fallback even after the original recovered.
|
||||||
this._failedAccounts.clear();
|
this._failedAccounts.clear();
|
||||||
this._accountOverrides.clear();
|
this._accountOverrides.clear();
|
||||||
if (Array.isArray(opts.primeFailedAccounts)) {
|
this._rotLog('batch-start', { taskCount: tasks.length });
|
||||||
for (const key of opts.primeFailedAccounts) this._failedAccounts.set(key, true);
|
|
||||||
}
|
|
||||||
if (Array.isArray(opts.primeOverrides)) {
|
|
||||||
for (const entry of opts.primeOverrides) {
|
|
||||||
if (Array.isArray(entry) && entry.length === 2) this._accountOverrides.set(entry[0], entry[1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this._rotLog('batch-start', {
|
|
||||||
taskCount: tasks.length,
|
|
||||||
primedFailed: this._failedAccounts.size,
|
|
||||||
primedOverrides: this._accountOverrides.size
|
|
||||||
});
|
|
||||||
|
|
||||||
const { signal } = this.abortController;
|
const { signal } = this.abortController;
|
||||||
const batchId = `batch-${Date.now()}`;
|
const batchId = `batch-${Date.now()}`;
|
||||||
|
|||||||
18
main.js
18
main.js
@ -20,12 +20,6 @@ let dropTargetWindow = null;
|
|||||||
let tray = null;
|
let tray = null;
|
||||||
const configStore = new ConfigStore(app);
|
const configStore = new ConfigStore(app);
|
||||||
let uploadManager = null;
|
let uploadManager = null;
|
||||||
// Rotation memory that survives batch-done → new UploadManager within the
|
|
||||||
// same app session. Without this, clicking "Retry failed" after a batch
|
|
||||||
// ended would burn the full retry budget on accounts we already know are
|
|
||||||
// dead. Cleared on app restart (which is the user's signal for "try fresh").
|
|
||||||
const _sessionFailedAccounts = new Map(); // "hoster:accountId" -> true
|
|
||||||
const _sessionAccountOverrides = new Map(); // hoster -> account object
|
|
||||||
const folderMonitor = new FolderMonitor();
|
const folderMonitor = new FolderMonitor();
|
||||||
let remoteServer = null;
|
let remoteServer = null;
|
||||||
let captureWindow = null;
|
let captureWindow = null;
|
||||||
@ -944,7 +938,6 @@ ipcMain.handle('save-config', async (_event, config) => {
|
|||||||
if (fallback) {
|
if (fallback) {
|
||||||
rotLog(`main: config-updated → late fallback ${fallback.id} for ${hoster} (was stuck on ${failedAccountId})`);
|
rotLog(`main: config-updated → late fallback ${fallback.id} for ${hoster} (was stuck on ${failedAccountId})`);
|
||||||
uploadManager.switchAccount(hoster, fallback);
|
uploadManager.switchAccount(hoster, fallback);
|
||||||
_sessionAccountOverrides.set(hoster, fallback);
|
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
mainWindow.webContents.send('account-switched', {
|
mainWindow.webContents.send('account-switched', {
|
||||||
hoster, fromAccountId: failedAccountId, toAccountId: fallback.id
|
hoster, fromAccountId: failedAccountId, toAccountId: fallback.id
|
||||||
@ -1156,15 +1149,11 @@ ipcMain.handle('start-upload', (_event, payload) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
uploadManager.on('account-failed', ({ hoster, accountId }) => {
|
uploadManager.on('account-failed', ({ hoster, accountId }) => {
|
||||||
// Persist to session cache so a subsequent batch (after batch-done)
|
|
||||||
// gets primed and won't burn retries on this account again.
|
|
||||||
_sessionFailedAccounts.set(hoster + ':' + accountId, true);
|
|
||||||
const cfg = configStore.load();
|
const cfg = configStore.load();
|
||||||
const fallback = getNextFallbackAccount(cfg, hoster, accountId);
|
const fallback = getNextFallbackAccount(cfg, hoster, accountId);
|
||||||
if (fallback) {
|
if (fallback) {
|
||||||
rotLog(`main: account-failed ${hoster} ${accountId} → resolved fallback ${fallback.id}`);
|
rotLog(`main: account-failed ${hoster} ${accountId} → resolved fallback ${fallback.id}`);
|
||||||
uploadManager.switchAccount(hoster, fallback);
|
uploadManager.switchAccount(hoster, fallback);
|
||||||
_sessionAccountOverrides.set(hoster, fallback);
|
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
mainWindow.webContents.send('account-switched', { hoster, fromAccountId: accountId, toAccountId: fallback.id });
|
mainWindow.webContents.send('account-switched', { hoster, fromAccountId: accountId, toAccountId: fallback.id });
|
||||||
}
|
}
|
||||||
@ -1204,11 +1193,8 @@ ipcMain.handle('start-upload', (_event, payload) => {
|
|||||||
// are not interleaved with the handle() response.
|
// are not interleaved with the handle() response.
|
||||||
process.nextTick(() => {
|
process.nextTick(() => {
|
||||||
if (!uploadManager) { debugLog('nextTick: uploadManager was nulled before startBatch'); return; }
|
if (!uploadManager) { debugLog('nextTick: uploadManager was nulled before startBatch'); return; }
|
||||||
debugLog(`nextTick: calling startBatch now (priming ${_sessionFailedAccounts.size} failed accounts, ${_sessionAccountOverrides.size} overrides from session)`);
|
debugLog('nextTick: calling startBatch now');
|
||||||
uploadManager.startBatch(tasks, {
|
uploadManager.startBatch(tasks).catch((err) => {
|
||||||
primeFailedAccounts: Array.from(_sessionFailedAccounts.keys()),
|
|
||||||
primeOverrides: Array.from(_sessionAccountOverrides.entries())
|
|
||||||
}).catch((err) => {
|
|
||||||
debugLog(`startBatch REJECTED: ${err && err.stack ? err.stack : err}`);
|
debugLog(`startBatch REJECTED: ${err && err.stack ? err.stack : err}`);
|
||||||
// Forward error to renderer as batch-done with failure
|
// Forward error to renderer as batch-done with failure
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "multi-hoster-uploader",
|
"name": "multi-hoster-uploader",
|
||||||
"version": "3.1.9",
|
"version": "3.1.8",
|
||||||
"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": {
|
||||||
|
|||||||
@ -650,66 +650,6 @@ describe('UploadManager', () => {
|
|||||||
assert.equal(mgr.getOverride('voe.sx'), null, 'unrelated hoster still has no override');
|
assert.equal(mgr.getOverride('voe.sx'), null, 'unrelated hoster still has no override');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('startBatch primes failed-accounts + overrides — retry after batch-done skips dead account', async () => {
|
|
||||||
// acc1 fails with disk-space; acc2 succeeds.
|
|
||||||
mockUploadFile.mock.mockImplementation(async (hoster, filePath, apiKey, onProgress) => {
|
|
||||||
if (apiKey === 'acc1-key') {
|
|
||||||
const err = new Error('Byse lehnte Datei ab: not enough disk space');
|
|
||||||
err.accountError = true;
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
if (onProgress) onProgress(fakeFileSize, fakeFileSize);
|
|
||||||
return { download_url: 'ok', embed_url: null, file_code: 'ok' };
|
|
||||||
});
|
|
||||||
|
|
||||||
const mgr = new UploadManager(
|
|
||||||
{ 'byse.sx': { retries: 3, parallelCount: 1, maxSpeedKbs: 0, restartBelowKbs: 0, timeIntervalSec: 0, maxSizeMb: 0 } }
|
|
||||||
);
|
|
||||||
const rotEvents = [];
|
|
||||||
mgr.on('rot-log', (e) => rotEvents.push(e));
|
|
||||||
|
|
||||||
// Simulate a retry-after-batch-done: main.js would pass the
|
|
||||||
// session-cached failed-accounts + overrides from the previous batch.
|
|
||||||
await mgr.startBatch([
|
|
||||||
{ file: '/test/a.mp4', hoster: 'byse.sx', apiKey: 'acc1-key', accountId: 'acc1', username: 'u1', password: 'p1' }
|
|
||||||
], {
|
|
||||||
primeFailedAccounts: ['byse.sx:acc1'],
|
|
||||||
primeOverrides: [['byse.sx', { id: 'acc2', username: 'u2', password: 'p2', apiKey: 'acc2-key' }]]
|
|
||||||
});
|
|
||||||
|
|
||||||
const events = rotEvents.map(e => e.event);
|
|
||||||
// pre-job-swap should fire on the very first attempt — no fast-fail
|
|
||||||
// because acc1 was never touched.
|
|
||||||
assert.ok(events.includes('pre-job-swap'),
|
|
||||||
`expected pre-job-swap from primed state; got: ${events.join(',')}`);
|
|
||||||
assert.ok(!events.includes('fast-fail'),
|
|
||||||
`must NOT burn a fast-fail on primed-dead acc1; got: ${events.join(',')}`);
|
|
||||||
assert.ok(!events.includes('mark-failed'),
|
|
||||||
`acc1 was already marked failed (primed); must not emit mark-failed again; got: ${events.join(',')}`);
|
|
||||||
|
|
||||||
// acc1 must not be touched at all.
|
|
||||||
const acc1Calls = mockUploadFile.mock.calls.filter(c => c.arguments[2] === 'acc1-key').length;
|
|
||||||
assert.equal(acc1Calls, 0, 'primed-dead acc1 must not receive any upload attempts');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('startBatch without prime opts still clears state (back-compat)', async () => {
|
|
||||||
const mgr = new UploadManager({});
|
|
||||||
mgr._failedAccounts.set('byse.sx:acc1', true);
|
|
||||||
mgr._accountOverrides.set('byse.sx', { id: 'leftover' });
|
|
||||||
|
|
||||||
mockUploadFile.mock.mockImplementation(async (hoster, filePath, apiKey, onProgress) => {
|
|
||||||
if (onProgress) onProgress(fakeFileSize, fakeFileSize);
|
|
||||||
return { download_url: 'ok', embed_url: null, file_code: 'ok' };
|
|
||||||
});
|
|
||||||
|
|
||||||
await mgr.startBatch([
|
|
||||||
{ file: '/test/a.mp4', hoster: 'doodstream.com', apiKey: 'k' }
|
|
||||||
]);
|
|
||||||
|
|
||||||
assert.equal(mgr._failedAccounts.size, 0, 'legacy callers still get a clean slate');
|
|
||||||
assert.equal(mgr._accountOverrides.size, 0, 'legacy callers still get a clean slate for overrides');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('transient network errors skip rotation (account stays fine)', () => {
|
it('transient network errors skip rotation (account stays fine)', () => {
|
||||||
const mgr = new UploadManager({});
|
const mgr = new UploadManager({});
|
||||||
const cases = [
|
const cases = [
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user