Compare commits

..

2 Commits

Author SHA1 Message Date
Administrator
6f8c2dcb38 release: v3.3.76 2026-06-17 13:42:19 +02:00
Administrator
7a04060e9b feat(rotation): per-hoster toggle to disable the "Bekanntes Größen-Limit" pre-skip
The byse suspect size-memo (softened in v3.3.73 to arm only after two
confirmed suspect rejections on the same account and only for strictly
larger files) still occasionally pre-skips a primary account with
"Bekanntes Größen-Limit auf diesem Account", which the user finds annoying
when they would rather just let every file attempt the upload for real.

Add a per-hoster opt-out: a new "Größen-Limit merken" checkbox (Accounts tab,
default ON so existing behavior is preserved for everyone) that, when
unchecked, disables the memo-based pre-skip entirely for that hoster.

- config-store.js / upload-manager.js DEFAULT_SETTINGS: sizeMemoEnabled: true.
- upload-manager.js _suspectMemoBlocks(): returns false up front when the
  hoster has sizeMemoEnabled === false. That single guard covers BOTH consult
  sites — the pre-job block (skips the guaranteed-fail upload) and the
  alternate-account skip — since both route through _suspectMemoBlocks. The
  memo is still recorded; it is simply never allowed to block. This moves the
  flow toward "fail open": with the toggle off, every file always gets a real
  attempt on its account.
- The genuine suspect-reject alternates walk is gated on err.suspectReject,
  not the memo, so disabling the memo does not weaken real failover — a file
  the account actually rejects still rolls over to the next account.
- renderer/app.js: checkbox data-hs="sizeMemoEnabled", checked when
  hs.sizeMemoEnabled !== false; persisted by the existing generic checkbox
  path in saveHosterSettingsFromDom(). The running manager picks it up on the
  next batch (fresh UploadManager) and immediately on save (updateSettings).

Tests: suspect-reject-alternates gains a disabled-memo case proving the larger
third file still gets a real primary attempt (key1 tried 3x, no
suspect-memo-skip event); config-store gains a default-true + persist-false
case. Full suite 305/305. Reviewed by a 4-lens adversarial workflow
(completeness / composition / persistence-ui / conventions): 0 findings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:41:38 +02:00
6 changed files with 49 additions and 3 deletions

View File

@ -11,7 +11,8 @@ const HOSTER_SETTINGS_DEFAULTS = {
timeIntervalSec: 0, // delay between jobs timeIntervalSec: 0, // delay between jobs
maxSizeMb: 0, // 0 = unlimited maxSizeMb: 0, // 0 = unlimited
logToFile: true, // write this hoster's successful links to fileuploader.log logToFile: true, // write this hoster's successful links to fileuploader.log
rotateAccounts: false rotateAccounts: false,
sizeMemoEnabled: true
}; };
// Template for each hoster type (used as defaults for new accounts) // Template for each hoster type (used as defaults for new accounts)

View File

@ -17,7 +17,8 @@ const DEFAULT_SETTINGS = {
parallelCount: 2, parallelCount: 2,
restartBelowKbs: 0, restartBelowKbs: 0,
timeIntervalSec: 0, timeIntervalSec: 0,
maxSizeMb: 0 maxSizeMb: 0,
sizeMemoEnabled: true
}; };
class UploadManager extends EventEmitter { class UploadManager extends EventEmitter {
@ -116,6 +117,7 @@ class UploadManager extends EventEmitter {
} }
_suspectMemoBlocks(hoster, accountId, fileSize) { _suspectMemoBlocks(hoster, accountId, fileSize) {
if (this.hosterSettings[hoster] && this.hosterSettings[hoster].sizeMemoEnabled === false) return false;
const memo = this._suspectSizeMemo.get(hoster + ':' + accountId); const memo = this._suspectSizeMemo.get(hoster + ':' + accountId);
return !!memo && memo.count >= 2 && fileSize > memo.size; return !!memo && memo.count >= 2 && fileSize > memo.size;
} }

View File

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

@ -3678,6 +3678,11 @@ function _buildHosterSettingsHtml(name) {
<input type="checkbox" class="hs-input" data-hoster="${name}" data-hs="rotateAccounts" ${hs.rotateAccounts === true ? 'checked' : ''}> <input type="checkbox" class="hs-input" data-hoster="${name}" data-hs="rotateAccounts" ${hs.rotateAccounts === true ? 'checked' : ''}>
<span class="hint">Verteilt die Dateien reihum auf alle aktiven Accounts dieses Hosters (Datei 1 Account 1, Datei 2 Account 2 ). Hält z. B. byse-Accounts aktiv. Nur ein Account = kein Effekt.</span> <span class="hint">Verteilt die Dateien reihum auf alle aktiven Accounts dieses Hosters (Datei 1 Account 1, Datei 2 Account 2 ). Hält z. B. byse-Accounts aktiv. Nur ein Account = kein Effekt.</span>
</div> </div>
<div class="settings-row">
<label>Größen-Limit merken</label>
<input type="checkbox" class="hs-input" data-hoster="${name}" data-hs="sizeMemoEnabled" ${hs.sizeMemoEnabled !== false ? 'checked' : ''}>
<span class="hint">Überspringt nach zwei verdächtigen Ablehnungen auf einem Account größere Dateien dort vorab ("Bekanntes Größen-Limit"). Abschalten = jede Datei wird immer wirklich versucht.</span>
</div>
</div> </div>
</div> </div>
</div>`; </div>`;

View File

@ -112,6 +112,17 @@ describe('ConfigStore', () => {
} }
}); });
it('sizeMemoEnabled defaults to true for every hoster and persists when disabled', async () => {
const fresh = store.load();
for (const name of ['doodstream.com', 'voe.sx', 'vidmoly.me', 'byse.sx', 'clouddrop.cc']) {
assert.equal(fresh.hosterSettings[name].sizeMemoEnabled, true, `${name} should default sizeMemoEnabled=true`);
}
await store.save({ hosterSettings: { 'byse.sx': { sizeMemoEnabled: false } } });
const config = store.load();
assert.equal(config.hosterSettings['byse.sx'].sizeMemoEnabled, false, 'explicit false preserved');
assert.equal(config.hosterSettings['voe.sx'].sizeMemoEnabled, true, 'other hoster still defaults on');
});
it('logToFile=false persists and survives reload', async () => { it('logToFile=false persists and survives reload', async () => {
await store.save({ hosterSettings: { 'voe.sx': { logToFile: false } } }); await store.save({ hosterSettings: { 'voe.sx': { logToFile: false } } });
const config = store.load(); const config = store.load();

View File

@ -205,6 +205,33 @@ describe('suspect-reject alternate accounts', () => {
assert.ok(rotEvents.includes('suspect-memo-skip'), 'the larger third file must skip its primary via the armed size memo'); assert.ok(rotEvents.includes('suspect-memo-skip'), 'the larger third file must skip its primary via the armed size memo');
}); });
it('sizeMemoEnabled:false disables the pre-skip — the larger third file still gets a real attempt on its primary', async () => {
const mgr = poolMgr([
{ id: 'acc1', apiKey: 'key1' },
{ id: 'acc2', apiKey: 'key2' },
{ id: 'acc3', apiKey: 'key3' }
], { parallelCount: 1, sizeMemoEnabled: false });
mockUploadFile.mock.mockImplementation(async (hoster, file, apiKey) => {
if (apiKey === 'key1' || apiKey === 'key2') throw suspectErr();
return { download_url: 'https://byse.sx/d/three', embed_url: null, file_code: 'three' };
});
const rotEvents = [];
mgr.on('rot-log', (e) => rotEvents.push(e.event));
let summary = null;
mgr.on('batch-done', (s) => { summary = s; });
await mgr.startBatch([
{ file: '/test/a-1gb.mkv', hoster: 'byse.sx', apiKey: 'key1', accountId: 'acc1' },
{ file: '/test/b-1gb.mkv', hoster: 'byse.sx', apiKey: 'key1', accountId: 'acc1' },
{ file: '/test/c-2gb.mkv', hoster: 'byse.sx', apiKey: 'key1', accountId: 'acc1' }
]);
assert.equal(summary.succeeded, 3);
const keys = mockUploadFile.mock.calls.map(c => c.arguments[2]);
assert.equal(keys.filter(k => k === 'key1').length, 3, 'with the memo off every file gets a real attempt on the primary — including the larger third');
assert.ok(!rotEvents.includes('suspect-memo-skip'), 'the disabled memo must never short-circuit a primary');
});
it('plain fileRejected without suspect flag keeps the old fast-fail behavior', async () => { it('plain fileRejected without suspect flag keeps the old fast-fail behavior', async () => {
const mgr = poolMgr([ const mgr = poolMgr([
{ id: 'acc1', apiKey: 'key1' }, { id: 'acc1', apiKey: 'key1' },