feat: add upload schedule windows

Add local weekly upload windows with overnight support, live settings updates, and a shared abortable upload gate.

Keep active transfers running while queued jobs wait outside the window without consuming attempts or parallel slots. Validate and localize the automation settings in German and English, including next-start status and hidden Electron coverage.
This commit is contained in:
Sucukdeluxe
2026-08-16 23:56:13 +02:00
parent 2eaa30c0a8
commit c8170b8556
11 changed files with 708 additions and 3 deletions
+23
View File
@@ -12,6 +12,7 @@ const Semaphore = require('./semaphore');
const Throttle = require('./throttle');
const { probeFileHead } = require('./file-probe');
const { normalizeFailureDetails } = require('./upload-diagnostics');
const { createUploadScheduleGate } = require('./upload-schedule');
const DEFAULT_SETTINGS = {
retries: 3,
@@ -28,6 +29,7 @@ class UploadManager extends EventEmitter {
super();
this.hosterSettings = hosterSettings || {};
this.globalSettings = globalSettings || {};
this.uploadScheduleGate = createUploadScheduleGate(this.globalSettings.uploadSchedule);
this.accountPools = accountPools || {};
this.semaphores = {};
this.globalSemaphore = null;
@@ -305,6 +307,7 @@ class UploadManager extends EventEmitter {
updateSettings(hosterSettings, globalSettings) {
this.hosterSettings = hosterSettings || this.hosterSettings;
this.globalSettings = globalSettings || this.globalSettings;
this.uploadScheduleGate.update(this.globalSettings.uploadSchedule);
// Live-update semaphores for running uploads
for (const [hoster, sem] of Object.entries(this.semaphores)) {
const settings = this._getSettings(hoster);
@@ -633,6 +636,8 @@ class UploadManager extends EventEmitter {
const attemptsAllowed = memoSuspect ? 0 : maxAttempts;
for (let attempt = 1; attempt <= attemptsAllowed; attempt++) {
await this._waitForUploadSchedule(signal);
this._throwIfUploadStartBlocked(signal);
finalAttempt = attempt;
if (signal.aborted || this.stopAfterActive) break;
@@ -1023,6 +1028,8 @@ class UploadManager extends EventEmitter {
// loop iterates: marks this account failed too, asks main for the next
// fallback, and so on.
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
await this._waitForUploadSchedule(signal);
this._throwIfUploadStartBlocked(signal);
finalAttempt = attempt;
if (signal.aborted || this.stopAfterActive) break;
if (attempt > 1) {
@@ -1181,6 +1188,8 @@ class UploadManager extends EventEmitter {
});
continue;
}
await this._waitForUploadSchedule(signal);
this._throwIfUploadStartBlocked(signal);
attempted += 1;
this._rotLog('suspect-reject-alt', {
jobId, hoster: task.hoster, fileName, fromAccountId: task.accountId, toAccountId: account.id
@@ -1282,6 +1291,8 @@ class UploadManager extends EventEmitter {
async _executeUploadWithAdmission(task, progressCb, signal, throttle, fileProbe, fileSize, coordinateAccountFailure = true, jobId = task.jobId) {
while (true) {
await this._waitForUploadSchedule(signal);
this._throwIfUploadStartBlocked(signal);
const context = await this._createRecoveryContext(task);
this._throwIfUploadStartBlocked(signal);
let retryAdmission = false;
@@ -1350,6 +1361,13 @@ class UploadManager extends EventEmitter {
this._beginSuspectResolution(task.hoster, jobId);
throw lateMemoSuspect;
}
if (!this.uploadScheduleGate.evaluate().allowed) {
releaseSlots();
await this._waitForUploadSchedule(signal);
this._throwIfUploadStartBlocked(signal);
retryAdmission = true;
return null;
}
try {
this._throwIfUploadStartBlocked(signal);
return await this._executeUpload(task, progressCb, signal, throttle, fileProbe, context);
@@ -1393,6 +1411,10 @@ class UploadManager extends EventEmitter {
}
}
_waitForUploadSchedule(signal) {
return this.uploadScheduleGate.wait(signal, () => this._throwIfUploadStartBlocked(signal));
}
_createSuspectMemoError(task, fileProbe, fileSize) {
if (!fileProbe || fileProbe.isVideoLike !== true || !task.accountId) return null;
if (!this._suspectMemoBlocks(task.hoster, task.accountId, fileSize)) return null;
@@ -1864,6 +1886,7 @@ class UploadManager extends EventEmitter {
finishAfterActive() {
this.stopAfterActive = true;
this.uploadScheduleGate.wake();
}
cancel() {