Compare commits

..

No commits in common. "9a933a9febcd25ce125d36fdfc442e2461c1b3a0" and "950f103f9950e3552afb3624a3f1adcbeb28494f" have entirely different histories.

6 changed files with 17 additions and 137 deletions

View File

@ -4,24 +4,19 @@ function enabledAccountsFor(hosters, hoster, hasCreds) {
return list.filter(a => a && a.enabled !== false && hasCreds(hoster, a)); return list.filter(a => a && a.enabled !== false && hasCreds(hoster, a));
} }
function createAccountPicker({ hosters, hosterSettings, hasCreds, indices }) { function createAccountPicker({ hosters, hosterSettings, hasCreds }) {
const rotIdx = Object.assign(Object.create(null), indices || {}); const rotIdx = Object.create(null);
let dirty = false; return function pick(hoster) {
function pick(hoster) {
const enabled = enabledAccountsFor(hosters, hoster, hasCreds); const enabled = enabledAccountsFor(hosters, hoster, hasCreds);
if (enabled.length === 0) return null; if (enabled.length === 0) return null;
const hs = (hosterSettings && hosterSettings[hoster]) || {}; const hs = (hosterSettings && hosterSettings[hoster]) || {};
if (hs.rotateAccounts === true && enabled.length > 1) { if (hs.rotateAccounts === true && enabled.length > 1) {
const cursor = Number.isFinite(rotIdx[hoster]) ? rotIdx[hoster] : 0; const i = (rotIdx[hoster] || 0) % enabled.length;
rotIdx[hoster] = cursor + 1; rotIdx[hoster] = (rotIdx[hoster] || 0) + 1;
dirty = true; return enabled[i];
return enabled[cursor % enabled.length];
} }
return enabled[0]; return enabled[0];
} };
pick.indices = () => ({ ...rotIdx });
pick.dirty = () => dirty;
return pick;
} }
module.exports = { createAccountPicker, enabledAccountsFor }; module.exports = { createAccountPicker, enabledAccountsFor };

View File

@ -101,8 +101,7 @@ const DEFAULTS = {
allowInput: true allowInput: true
} }
}, },
history: [], history: []
rotationCursors: {}
}; };
const HISTORY_RETENTION_OPTIONS = [ const HISTORY_RETENTION_OPTIONS = [
@ -290,10 +289,7 @@ class ConfigStore {
// Downstream readers consume logMode only and must NOT derive from // Downstream readers consume logMode only and must NOT derive from
// sessionLog at call sites. // sessionLog at call sites.
globalSettings.logMode = normalizeLogMode(globalSettings); globalSettings.logMode = normalizeLogMode(globalSettings);
const rotationCursors = (data.rotationCursors && typeof data.rotationCursors === 'object' && !Array.isArray(data.rotationCursors)) const result = { hosters, hosterSettings, globalSettings, history: data.history || [] };
? data.rotationCursors
: {};
const result = { hosters, hosterSettings, globalSettings, history: data.history || [], rotationCursors };
// Decrypt credentials stored with safeStorage so the rest of the app // Decrypt credentials stored with safeStorage so the rest of the app
// keeps working with plaintext in memory. // keeps working with plaintext in memory.
secretStore.decryptCredentials(result); secretStore.decryptCredentials(result);
@ -401,14 +397,6 @@ class ConfigStore {
return this._atomicWrite(this._serializeForDisk(config)); return this._atomicWrite(this._serializeForDisk(config));
}); });
} }
saveRotationCursors(cursors) {
return this._enqueueWrite(() => {
const config = this.load();
config.rotationCursors = (cursors && typeof cursors === 'object' && !Array.isArray(cursors)) ? cursors : {};
return this._atomicWrite(this._serializeForDisk(config));
});
}
} }
module.exports = ConfigStore; module.exports = ConfigStore;

40
main.js
View File

@ -742,32 +742,9 @@ function buildTaskFromAccount(hoster, account, extra) {
return task; return task;
} }
let _rotationCursors = null; function buildUploadTasks(config, files, hosters) {
function rotationCursors() {
if (_rotationCursors === null) {
const persisted = configStore.load().rotationCursors;
_rotationCursors = (persisted && typeof persisted === 'object') ? { ...persisted } : {};
}
return _rotationCursors;
}
function makeAccountPicker(config) {
return createAccountPicker({
hosters: config.hosters,
hosterSettings: config.hosterSettings,
hasCreds: hosterAccountHasCreds,
indices: rotationCursors()
});
}
function persistRotation(pick) {
if (!pick.dirty()) return;
_rotationCursors = { ...rotationCursors(), ...pick.indices() };
configStore.saveRotationCursors(_rotationCursors);
}
function buildUploadTasks(config, files, hosters, pick) {
const tasks = []; const tasks = [];
const pick = createAccountPicker({ hosters: config.hosters, hosterSettings: config.hosterSettings, hasCreds: hosterAccountHasCreds });
for (const file of files) { for (const file of files) {
for (const hoster of hosters) { for (const hoster of hosters) {
const account = pick(hoster); const account = pick(hoster);
@ -778,8 +755,9 @@ function buildUploadTasks(config, files, hosters, pick) {
return tasks; return tasks;
} }
function buildUploadTasksFromJobs(config, jobs, pick) { function buildUploadTasksFromJobs(config, jobs) {
if (!Array.isArray(jobs)) return []; if (!Array.isArray(jobs)) return [];
const pick = createAccountPicker({ hosters: config.hosters, hosterSettings: config.hosterSettings, hasCreds: hosterAccountHasCreds });
const tasks = []; const tasks = [];
for (const job of jobs) { for (const job of jobs) {
if (!job || !job.file || !job.hoster) continue; if (!job || !job.file || !job.hoster) continue;
@ -1556,11 +1534,9 @@ ipcMain.handle('start-upload', (_event, payload) => {
logMarker('BATCH START', { files: files.length, hosters: hosters.length, jobs: jobs.length }); logMarker('BATCH START', { files: files.length, hosters: hosters.length, jobs: jobs.length });
debugLog(`start-upload: files=${files.length}, hosters=${hosters.length}, jobs=${jobs.length}`); debugLog(`start-upload: files=${files.length}, hosters=${hosters.length}, jobs=${jobs.length}`);
const pick = makeAccountPicker(config);
const tasks = jobs.length > 0 const tasks = jobs.length > 0
? buildUploadTasksFromJobs(config, jobs, pick) ? buildUploadTasksFromJobs(config, jobs)
: buildUploadTasks(config, files, hosters, pick); : buildUploadTasks(config, files, hosters);
persistRotation(pick);
// Identify jobs that were skipped (no account/credentials) // Identify jobs that were skipped (no account/credentials)
const taskJobIds = new Set(tasks.map(t => t.jobId).filter(Boolean)); const taskJobIds = new Set(tasks.map(t => t.jobId).filter(Boolean));
@ -1815,9 +1791,7 @@ ipcMain.handle('add-jobs-to-batch', (_event, payload) => {
} }
const config = configStore.load(); const config = configStore.load();
const jobs = payload && Array.isArray(payload.jobs) ? payload.jobs : []; const jobs = payload && Array.isArray(payload.jobs) ? payload.jobs : [];
const pick = makeAccountPicker(config); const tasks = buildUploadTasksFromJobs(config, jobs);
const tasks = buildUploadTasksFromJobs(config, jobs, pick);
persistRotation(pick);
const taskJobIds = new Set(tasks.map(t => t.jobId).filter(Boolean)); const taskJobIds = new Set(tasks.map(t => t.jobId).filter(Boolean));
const skippedJobs = jobs const skippedJobs = jobs
.filter(j => j && j.id && !taskJobIds.has(j.id)) .filter(j => j && j.id && !taskJobIds.has(j.id))

View File

@ -1,6 +1,6 @@
{ {
"name": "multi-hoster-uploader", "name": "multi-hoster-uploader",
"version": "3.3.75", "version": "3.3.74",
"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

@ -75,52 +75,3 @@ test('enabledAccountsFor filters disabled + no-creds and preserves order', () =>
assert.deepStrictEqual(enabledAccountsFor(hosters, 'byse.sx', hasCreds).map(a => a.id), ['a1', 'a4']); assert.deepStrictEqual(enabledAccountsFor(hosters, 'byse.sx', hasCreds).map(a => a.id), ['a1', 'a4']);
assert.deepStrictEqual(enabledAccountsFor(hosters, 'missing', hasCreds), []); assert.deepStrictEqual(enabledAccountsFor(hosters, 'missing', hasCreds), []);
}); });
test('seeded index resumes mid-cycle (restart / persisted cursor)', () => {
const hosters = { 'byse.sx': [acc('a1'), acc('a2'), acc('a3')] };
const pick = createAccountPicker({ hosters, hosterSettings: { 'byse.sx': { rotateAccounts: true } }, hasCreds, indices: { 'byse.sx': 1 } });
assert.deepStrictEqual(picks(pick, 'byse.sx', 4), ['a2', 'a3', 'a1', 'a2']);
});
test('drip-feed: fresh picker per call seeded from prior indices keeps rotating (no per-batch reset)', () => {
const hosters = { 'byse.sx': [acc('a1'), acc('a2'), acc('a3')] };
const settings = { 'byse.sx': { rotateAccounts: true } };
let cursors = {};
const landed = [];
for (let i = 0; i < 6; i++) {
const pick = createAccountPicker({ hosters, hosterSettings: settings, hasCreds, indices: cursors });
landed.push(pick('byse.sx').id);
cursors = pick.indices();
}
assert.deepStrictEqual(landed, ['a1', 'a2', 'a3', 'a1', 'a2', 'a3']);
});
test('dirty() is true only after an actual rotation advance', () => {
const hosters = { 'byse.sx': [acc('a1'), acc('a2')], 'voe.sx': [acc('v1'), acc('v2')] };
const offPick = createAccountPicker({ hosters, hosterSettings: {}, hasCreds });
offPick('byse.sx'); offPick('voe.sx');
assert.strictEqual(offPick.dirty(), false);
const singlePick = createAccountPicker({ hosters: { 'byse.sx': [acc('a1')] }, hosterSettings: { 'byse.sx': { rotateAccounts: true } }, hasCreds });
singlePick('byse.sx');
assert.strictEqual(singlePick.dirty(), false);
const onPick = createAccountPicker({ hosters, hosterSettings: { 'byse.sx': { rotateAccounts: true } }, hasCreds });
onPick('voe.sx');
assert.strictEqual(onPick.dirty(), false);
onPick('byse.sx');
assert.strictEqual(onPick.dirty(), true);
});
test('indices() carries forward unrotated seeded hosters alongside advanced ones', () => {
const hosters = { 'byse.sx': [acc('a1'), acc('a2')], 'voe.sx': [acc('v1'), acc('v2')] };
const pick = createAccountPicker({ hosters, hosterSettings: { 'byse.sx': { rotateAccounts: true } }, hasCreds, indices: { 'voe.sx': 5 } });
pick('byse.sx');
assert.deepStrictEqual(pick.indices(), { 'voe.sx': 5, 'byse.sx': 1 });
});
test('persisted cursor wraps correctly after the enabled-account count shrinks', () => {
const hosters = { 'byse.sx': [acc('a1'), acc('a2')] };
const pick = createAccountPicker({ hosters, hosterSettings: { 'byse.sx': { rotateAccounts: true } }, hasCreds, indices: { 'byse.sx': 7 } });
assert.deepStrictEqual(picks(pick, 'byse.sx', 3), ['a2', 'a1', 'a2']);
});

View File

@ -147,34 +147,6 @@ describe('ConfigStore', () => {
assert.equal(store.loadHistory().length, 0); assert.equal(store.loadHistory().length, 0);
}); });
it('rotationCursors default to an empty object', () => {
const config = store.load();
assert.deepEqual(config.rotationCursors, {});
});
it('saveRotationCursors round-trips and survives reload', async () => {
await store.saveRotationCursors({ 'byse.sx': 7, 'voe.sx': 2 });
const config = store.load();
assert.equal(config.rotationCursors['byse.sx'], 7);
assert.equal(config.rotationCursors['voe.sx'], 2);
});
it('an unrelated save() does not clobber persisted rotationCursors', async () => {
await store.saveRotationCursors({ 'byse.sx': 3 });
await store.save({ globalSettings: { alwaysOnTop: true } });
const config = store.load();
assert.equal(config.rotationCursors['byse.sx'], 3, 'cursor preserved across a settings save');
assert.equal(config.globalSettings.alwaysOnTop, true);
});
it('saveRotationCursors does not disturb credentials', async () => {
await store.save({ hosters: { 'byse.sx': [{ id: 'k1', enabled: true, authType: 'api', apiKey: 'secret-key' }] } });
await store.saveRotationCursors({ 'byse.sx': 1 });
const config = store.load();
assert.equal(config.hosters['byse.sx'][0].apiKey, 'secret-key');
assert.equal(config.rotationCursors['byse.sx'], 1);
});
it('corrupted JSON falls back to defaults', () => { it('corrupted JSON falls back to defaults', () => {
fs.writeFileSync(store.filePath, '{invalid json!!!', 'utf-8'); fs.writeFileSync(store.filePath, '{invalid json!!!', 'utf-8');
const config = store.load(); const config = store.load();