Restore the v2.1.19 application baseline and retain only the focused import preflight summary with duplicate, unavailable, destination, job, and size-limit visibility.
This commit is contained in:
+101
-510
@@ -3,7 +3,7 @@ const path = require('path');
|
||||
const { assertUploadConfirmation } = require('./upload-confirmation');
|
||||
const fs = require('fs');
|
||||
const crypto = require('crypto');
|
||||
const { uploadFile, prefetchBaseline, createRecoveryClaimRegistry, normalizeRecoveryTitle } = require('./hosters');
|
||||
const { uploadFile, prefetchBaseline } = require('./hosters');
|
||||
const VidmolyUploader = require('./vidmoly-upload');
|
||||
const VoeUploader = require('./voe-upload');
|
||||
const DoodstreamUploader = require('./doodstream-upload');
|
||||
@@ -12,7 +12,6 @@ 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,
|
||||
@@ -29,7 +28,6 @@ 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;
|
||||
@@ -54,11 +52,6 @@ class UploadManager extends EventEmitter {
|
||||
this._suspectGoodAccounts = new Map(); // hoster -> accountId that accepted a suspect-class file
|
||||
this._doodApiKeyCache = new Map(); // accountId/username -> derived doodstream API key ('' = tried, none)
|
||||
this._baselineCache = new Map(); // hoster:apiKey -> Promise<Set<file_code>> (one fetch shared across all jobs in batch)
|
||||
this._recoveryClaims = createRecoveryClaimRegistry();
|
||||
this._recoveryAuthModes = new Map();
|
||||
this._suspectResolutionGates = new Map();
|
||||
this._batchJobIds = new Set();
|
||||
this._batchTotal = 0;
|
||||
}
|
||||
|
||||
updateAccountPools(accountPools) {
|
||||
@@ -75,9 +68,6 @@ class UploadManager extends EventEmitter {
|
||||
this._suspectGoodAccounts.clear();
|
||||
this._doodApiKeyCache.clear();
|
||||
this._baselineCache.clear();
|
||||
if (!this.running) this._recoveryClaims.clear();
|
||||
if (!this.running) this._recoveryAuthModes.clear();
|
||||
if (!this.running) this._clearSuspectResolutionGates();
|
||||
}
|
||||
|
||||
switchAccount(hoster, fallbackAccount) {
|
||||
@@ -140,41 +130,6 @@ class UploadManager extends EventEmitter {
|
||||
return true;
|
||||
}
|
||||
|
||||
_swapFailedAccount(task, jobId, fileName) {
|
||||
if (!task.accountId || !this._failedAccounts.has(task.hoster + ':' + task.accountId)) return false;
|
||||
const override = this._accountOverrides.get(task.hoster);
|
||||
if (override && !this._failedAccounts.has(task.hoster + ':' + override.id)) {
|
||||
this._rotLog('pre-job-swap', {
|
||||
jobId, hoster: task.hoster, fileName, fromAccountId: task.accountId, toAccountId: override.id
|
||||
});
|
||||
task.accountId = override.id;
|
||||
task.username = override.username;
|
||||
task.password = override.password;
|
||||
task.apiKey = override.apiKey;
|
||||
return true;
|
||||
}
|
||||
this._rotLog('pre-job-swap-blocked', {
|
||||
jobId, hoster: task.hoster, fileName, accountId: task.accountId,
|
||||
hasOverride: !!override,
|
||||
overrideAlsoFailed: override ? this._failedAccounts.has(task.hoster + ':' + override.id) : false
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
async _coordinateAccountFailure(task, err, signal, jobId) {
|
||||
if (!task.accountId || (err && err.remoteCommitUncertain === true)) return;
|
||||
if (!this._shouldSkipRetryOnAccountError(err)) return;
|
||||
const key = task.hoster + ':' + task.accountId;
|
||||
if (this._failedAccounts.has(key)) return;
|
||||
this._failedAccounts.set(key, true);
|
||||
this._rotLog('mark-failed', {
|
||||
jobId, hoster: task.hoster, fileName: path.basename(task.file),
|
||||
accountId: task.accountId, lastError: err && err.message ? err.message : String(err)
|
||||
});
|
||||
this.emit('account-failed', { hoster: task.hoster, accountId: task.accountId });
|
||||
await this._sleep(800, signal);
|
||||
}
|
||||
|
||||
_rotLog(event, data) {
|
||||
this.emit('rot-log', { ts: Date.now(), event, ...data });
|
||||
}
|
||||
@@ -202,7 +157,6 @@ class UploadManager extends EventEmitter {
|
||||
// which takes priority in _shouldSkipRetryOnAccountError.
|
||||
_isFileRejectedError(err) {
|
||||
if (!err) return false;
|
||||
if (err.remoteCommitUncertain === true) return false;
|
||||
if (err.transientNetwork === true) return false;
|
||||
if (err.accountError === true) return false; // explicit account-level wins
|
||||
if (err.fileRejected === true) return true;
|
||||
@@ -307,7 +261,6 @@ 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);
|
||||
@@ -381,8 +334,6 @@ class UploadManager extends EventEmitter {
|
||||
async startBatch(tasks, opts = {}) {
|
||||
const pendingCancelledJobIds = new Set(this.pendingCancelledJobIds);
|
||||
const pendingCancelAll = this.pendingCancelAll;
|
||||
this._batchJobIds = new Set(tasks.map((task) => task.jobId).filter(Boolean));
|
||||
this._batchTotal = tasks.length;
|
||||
this.pendingCancelledJobIds.clear();
|
||||
this.pendingCancelAll = false;
|
||||
this.running = true;
|
||||
@@ -397,10 +348,6 @@ class UploadManager extends EventEmitter {
|
||||
for (const jobId of pendingCancelledJobIds) this.cancelledJobIds.add(jobId);
|
||||
this._doodApiKeyCache.clear(); // re-derive doodstream keys fresh each batch
|
||||
this._baselineCache.clear(); // re-fetch baselines per batch (a long batch could outlast remote-side relevance)
|
||||
this._recoveryClaims.clear();
|
||||
this._recoveryClaims = createRecoveryClaimRegistry();
|
||||
this._recoveryAuthModes.clear();
|
||||
this._clearSuspectResolutionGates();
|
||||
this.semaphores = {};
|
||||
this.globalSemaphore = null;
|
||||
this.globalThrottle = null;
|
||||
@@ -441,7 +388,7 @@ class UploadManager extends EventEmitter {
|
||||
for (let j = i; j < end; j++) {
|
||||
const task = tasks[j];
|
||||
if (!results.has(task.file)) {
|
||||
results.set(task.file, { name: path.basename(task.file), fileKey: task.fileKey || null, size: 0, results: [] });
|
||||
results.set(task.file, { name: path.basename(task.file), size: 0, results: [] });
|
||||
toStat.push(task.file);
|
||||
}
|
||||
}
|
||||
@@ -472,7 +419,7 @@ class UploadManager extends EventEmitter {
|
||||
this._emitStats();
|
||||
|
||||
const files = Array.from(results.values());
|
||||
const total = this._batchTotal;
|
||||
const total = tasks.length;
|
||||
const succeeded = files.reduce((count, file) => count + file.results.filter((result) => result.status === 'done').length, 0);
|
||||
const skipped = files.reduce((count, file) => count + file.results.filter((result) => result.status === 'skipped').length, 0);
|
||||
|
||||
@@ -481,21 +428,18 @@ class UploadManager extends EventEmitter {
|
||||
timestamp: new Date().toISOString(),
|
||||
total,
|
||||
succeeded,
|
||||
failed: Math.max(0, total - succeeded - skipped),
|
||||
failed: total - succeeded - skipped,
|
||||
skipped,
|
||||
files
|
||||
};
|
||||
|
||||
this._recoveryClaims.clear();
|
||||
this._recoveryAuthModes.clear();
|
||||
this._clearSuspectResolutionGates();
|
||||
this._doodApiKeyCache.clear();
|
||||
this._baselineCache.clear();
|
||||
this.emit('batch-done', summary);
|
||||
}
|
||||
|
||||
async _runJob(task, results, batchSignal) {
|
||||
const settings = this._getSettings(task.hoster);
|
||||
const hosterSemaphore = this._getSemaphore(task.hoster);
|
||||
const globalSemaphore = this._getGlobalSemaphore();
|
||||
const uploadId = crypto.randomBytes(8).toString('hex');
|
||||
const jobId = task.jobId || uploadId;
|
||||
const fileName = path.basename(task.file);
|
||||
@@ -515,6 +459,8 @@ class UploadManager extends EventEmitter {
|
||||
const { signal, cleanup: cleanupSignals } = this._combineSignals(batchSignal, jobAbortController.signal);
|
||||
this.jobAbortControllers.set(jobId, jobAbortController);
|
||||
|
||||
let hosterSlotAcquired = false;
|
||||
let globalSlotAcquired = false;
|
||||
let finalResultRecorded = false;
|
||||
let finalStatus = 'error';
|
||||
let lastError = null;
|
||||
@@ -527,7 +473,6 @@ class UploadManager extends EventEmitter {
|
||||
finalStatus = status;
|
||||
|
||||
const result = {
|
||||
jobId,
|
||||
hoster: task.hoster,
|
||||
status,
|
||||
error: payload.error || null,
|
||||
@@ -540,16 +485,12 @@ class UploadManager extends EventEmitter {
|
||||
embed_url: payload.result ? payload.result.embed_url || null : null,
|
||||
file_code: payload.result ? payload.result.file_code || null : null
|
||||
};
|
||||
if (payload.remoteCommitUncertain === true || payload.error?.remoteCommitUncertain === true || lastError?.remoteCommitUncertain === true) {
|
||||
result.remoteCommitUncertain = true;
|
||||
}
|
||||
|
||||
results.get(task.file).results.push(result);
|
||||
};
|
||||
|
||||
const emitFinalStatus = (status, payload = {}) => {
|
||||
if (status === 'aborted' && this.abortController.signal.aborted) return;
|
||||
const remoteCommitUncertain = payload.remoteCommitUncertain === true || payload.error?.remoteCommitUncertain === true || lastError?.remoteCommitUncertain === true;
|
||||
this._emitProgress(uploadId, fileName, task.hoster, { accountId: task.accountId,
|
||||
jobId,
|
||||
status,
|
||||
@@ -563,8 +504,7 @@ class UploadManager extends EventEmitter {
|
||||
failureDetails: payload.failureDetails || lastFailureDetails,
|
||||
result: payload.result || null,
|
||||
attempt: payload.attempt || maxAttempts,
|
||||
maxAttempts,
|
||||
remoteCommitUncertain
|
||||
maxAttempts
|
||||
});
|
||||
};
|
||||
|
||||
@@ -601,6 +541,9 @@ class UploadManager extends EventEmitter {
|
||||
// queueJobs array; the first event it actually needs from main is the
|
||||
// 'getting-server' / 'uploading' transition for the jobs that the
|
||||
// semaphore lets through.
|
||||
await hosterSemaphore.acquire(signal);
|
||||
hosterSlotAcquired = true;
|
||||
|
||||
let fileProbe = null;
|
||||
try {
|
||||
fileProbe = await probeFileHead(task.file, 512);
|
||||
@@ -615,19 +558,48 @@ class UploadManager extends EventEmitter {
|
||||
headHex: fileProbe && fileProbe.headHex ? fileProbe.headHex.slice(0, 32) : null
|
||||
});
|
||||
|
||||
if (globalSemaphore) {
|
||||
await globalSemaphore.acquire(signal);
|
||||
globalSlotAcquired = true;
|
||||
}
|
||||
|
||||
if (settings.timeIntervalSec > 0) {
|
||||
await this._waitForInterval(task.hoster, settings.timeIntervalSec * 1000, signal);
|
||||
}
|
||||
|
||||
// Pre-job-swap: if this account was marked failed WHILE this task was
|
||||
// waiting in the semaphore queue, jump straight to the override instead
|
||||
// of burning a guaranteed-to-fail upload attempt. Critical at scale:
|
||||
// with 500 queued jobs and 1 parallel slot, without this check every
|
||||
// job still hits the original dead account first.
|
||||
this._swapFailedAccount(task, jobId, fileName);
|
||||
if (task.accountId && this._failedAccounts.has(task.hoster + ':' + task.accountId)) {
|
||||
const override = this._accountOverrides.get(task.hoster);
|
||||
if (override && !this._failedAccounts.has(task.hoster + ':' + override.id)) {
|
||||
this._rotLog('pre-job-swap', {
|
||||
jobId, hoster: task.hoster, fileName, fromAccountId: task.accountId, toAccountId: override.id
|
||||
});
|
||||
task.accountId = override.id;
|
||||
task.username = override.username;
|
||||
task.password = override.password;
|
||||
task.apiKey = override.apiKey;
|
||||
} else {
|
||||
this._rotLog('pre-job-swap-blocked', {
|
||||
jobId, hoster: task.hoster, fileName, accountId: task.accountId,
|
||||
hasOverride: !!override,
|
||||
overrideAlsoFailed: override ? this._failedAccounts.has(task.hoster + ':' + override.id) : false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// A previous file of at least this size already got a suspect rejection
|
||||
// on this exact account — skip the guaranteed-to-fail multi-GB upload
|
||||
// and go straight to the alternate-account walk below.
|
||||
let memoSuspect = this._createSuspectMemoError(task, fileProbe, fileSize);
|
||||
if (memoSuspect) {
|
||||
this._beginSuspectResolution(task.hoster, jobId);
|
||||
let memoSuspect = null;
|
||||
if (fileProbe && fileProbe.isVideoLike === true && task.accountId
|
||||
&& this._suspectMemoBlocks(task.hoster, task.accountId, fileSize)) {
|
||||
memoSuspect = new Error('Bekanntes Größen-Limit auf diesem Account (frühere verdächtige Ablehnung)');
|
||||
memoSuspect.fileRejected = true;
|
||||
memoSuspect.suspectReject = true;
|
||||
lastError = memoSuspect;
|
||||
this._rotLog('suspect-memo-skip', {
|
||||
jobId, hoster: task.hoster, fileName, accountId: task.accountId, fileSize
|
||||
@@ -636,8 +608,6 @@ 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;
|
||||
|
||||
@@ -757,7 +727,9 @@ class UploadManager extends EventEmitter {
|
||||
} catch { /* progress callbacks must never throw — swallowing is correct, the stream keeps going */ }
|
||||
};
|
||||
|
||||
const result = await this._executeUploadWithAdmission(task, progressCb, uploadSignalBundle.signal, throttle, fileProbe, fileSize, true, jobId);
|
||||
const result = await this._executeUpload(task, progressCb, uploadSignalBundle.signal, throttle, fileProbe);
|
||||
|
||||
if (signal.aborted || this.cancelledJobIds.has(jobId)) throw new Error('Aborted');
|
||||
|
||||
const elapsed = Math.round((Date.now() - jobStart) / 1000);
|
||||
this.sessionBytes += fileSize;
|
||||
@@ -794,11 +766,6 @@ class UploadManager extends EventEmitter {
|
||||
payloadSnippet: lastFailureDetails ? lastFailureDetails.responseSnippet || null : null
|
||||
});
|
||||
}
|
||||
if (err && err.remoteCommitUncertain === true) {
|
||||
lastError = err;
|
||||
break;
|
||||
}
|
||||
|
||||
if (signal.aborted) {
|
||||
lastError = new Error('Abgebrochen');
|
||||
break;
|
||||
@@ -864,16 +831,6 @@ class UploadManager extends EventEmitter {
|
||||
|
||||
const wasStopped = this.stopAfterActive && !signal.aborted;
|
||||
const wasAborted = signal.aborted || this.cancelledJobIds.has(jobId);
|
||||
if (lastError && lastError.remoteCommitUncertain === true) {
|
||||
const cancelledUncertain = lastError.cancelledAfterUploadStart === true;
|
||||
const status = cancelledUncertain ? 'aborted' : 'error';
|
||||
const error = cancelledUncertain
|
||||
? 'Abgebrochen; Remote-Status konnte nicht bestätigt werden'
|
||||
: (lastError.message || 'Remote-Upload konnte nicht eindeutig bestätigt werden');
|
||||
emitFinalStatus(status, { error, remoteCommitUncertain: true });
|
||||
recordFinalResult(status, { error, remoteCommitUncertain: true });
|
||||
return;
|
||||
}
|
||||
if (wasStopped || wasAborted) {
|
||||
const error = wasStopped ? 'Warteschlange angehalten' : 'Abgebrochen';
|
||||
emitFinalStatus('aborted', { error });
|
||||
@@ -907,6 +864,12 @@ class UploadManager extends EventEmitter {
|
||||
this._noteSuspectReject(task.hoster, task.accountId, fileSize);
|
||||
const alt = await this._trySuspectRejectAlternates(task, { uploadId, jobId, fileName, fileSize, settings, signal, fileProbe });
|
||||
if (alt) {
|
||||
if (signal.aborted || this.cancelledJobIds.has(jobId)) {
|
||||
const error = 'Abgebrochen';
|
||||
emitFinalStatus('aborted', { error });
|
||||
recordFinalResult('aborted', { error });
|
||||
return;
|
||||
}
|
||||
emitFinalStatus('done', { result: alt.result, speedKbs: alt.speedKbs, elapsed: alt.elapsed, attempt: 1 });
|
||||
recordFinalResult('done', { result: alt.result });
|
||||
return;
|
||||
@@ -1028,8 +991,6 @@ 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) {
|
||||
@@ -1078,7 +1039,8 @@ class UploadManager extends EventEmitter {
|
||||
? { consume: async (bytes, sig) => { await hosterThrottle.consume(bytes, sig); await globalThrottle.consume(bytes, sig); } }
|
||||
: hosterThrottle || globalThrottle;
|
||||
|
||||
const result = await this._executeUploadWithAdmission(task, progressCb, signal, throttle, fileProbe, fileSize, true, jobId);
|
||||
const result = await this._executeUpload(task, progressCb, signal, throttle, fileProbe);
|
||||
if (signal.aborted || this.cancelledJobIds.has(jobId)) throw new Error('Aborted');
|
||||
this.activeJobs.delete(uploadId);
|
||||
this.sessionBytes += fileSize;
|
||||
emitFinalStatus('done', { result, speedKbs: currentSpeedKbs, elapsed: Math.round((Date.now() - jobStart) / 1000), attempt });
|
||||
@@ -1099,7 +1061,6 @@ class UploadManager extends EventEmitter {
|
||||
rotationRetry: true
|
||||
});
|
||||
}
|
||||
if (err && err.remoteCommitUncertain === true) break;
|
||||
if (signal.aborted || this.stopAfterActive) break;
|
||||
if (this._isFileRejectedError(err)) break;
|
||||
if (this._isHosterTransientError(err)) break;
|
||||
@@ -1111,16 +1072,6 @@ class UploadManager extends EventEmitter {
|
||||
|
||||
const stoppedLate = this.stopAfterActive && !signal.aborted;
|
||||
const abortedLate = signal.aborted || this.cancelledJobIds.has(jobId);
|
||||
if (lastError && lastError.remoteCommitUncertain === true) {
|
||||
const cancelledUncertain = lastError.cancelledAfterUploadStart === true;
|
||||
const status = cancelledUncertain ? 'aborted' : 'error';
|
||||
const error = cancelledUncertain
|
||||
? 'Abgebrochen; Remote-Status konnte nicht bestätigt werden'
|
||||
: (lastError.message || 'Remote-Upload konnte nicht eindeutig bestätigt werden');
|
||||
emitFinalStatus(status, { error, remoteCommitUncertain: true });
|
||||
recordFinalResult(status, { error, remoteCommitUncertain: true });
|
||||
return;
|
||||
}
|
||||
if (stoppedLate || abortedLate) {
|
||||
const error = stoppedLate ? 'Warteschlange angehalten' : 'Abgebrochen';
|
||||
emitFinalStatus('aborted', { error });
|
||||
@@ -1134,25 +1085,20 @@ class UploadManager extends EventEmitter {
|
||||
emitFinalStatus('error', { error });
|
||||
recordFinalResult('error', { error });
|
||||
} catch (err) {
|
||||
if (err && err.remoteCommitUncertain === true) lastError = err;
|
||||
const wasStopped = this.stopAfterActive && !signal.aborted;
|
||||
const remoteCommitUncertain = !!(err && err.remoteCommitUncertain === true);
|
||||
const cancelledUncertain = remoteCommitUncertain && err.cancelledAfterUploadStart === true;
|
||||
const error = remoteCommitUncertain
|
||||
? (cancelledUncertain ? 'Abgebrochen; Remote-Status konnte nicht bestätigt werden' : (err.message || 'Remote-Upload konnte nicht eindeutig bestätigt werden'))
|
||||
: wasStopped
|
||||
const error = wasStopped
|
||||
? 'Warteschlange angehalten'
|
||||
: (signal.aborted || this.cancelledJobIds.has(jobId) ? 'Abgebrochen' : (err && err.message ? err.message : 'Unbekannter Fehler'));
|
||||
const status = remoteCommitUncertain
|
||||
? (cancelledUncertain ? 'aborted' : 'error')
|
||||
: (signal.aborted || this.cancelledJobIds.has(jobId) || wasStopped ? 'aborted' : 'error');
|
||||
emitFinalStatus(status, { error, remoteCommitUncertain });
|
||||
recordFinalResult(status, { error, remoteCommitUncertain });
|
||||
const status = signal.aborted || this.cancelledJobIds.has(jobId) || wasStopped ? 'aborted' : 'error';
|
||||
emitFinalStatus(status, { error });
|
||||
recordFinalResult(status === 'error' ? 'error' : 'aborted', { error });
|
||||
} finally {
|
||||
this._endSuspectResolution(task.hoster, jobId);
|
||||
this.activeJobs.delete(uploadId);
|
||||
this.jobAbortControllers.delete(jobId);
|
||||
cleanupSignals();
|
||||
// Release in reverse order of acquire (global first, then hoster)
|
||||
if (globalSlotAcquired && globalSemaphore) globalSemaphore.release();
|
||||
if (hosterSlotAcquired) hosterSemaphore.release();
|
||||
this.emit('job-settled', {
|
||||
jobId,
|
||||
sourceCleanupToken: task.sourceCleanupToken || null,
|
||||
@@ -1188,8 +1134,6 @@ 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
|
||||
@@ -1238,7 +1182,8 @@ class UploadManager extends EventEmitter {
|
||||
? { consume: async (bytes, sig) => { await hosterThrottle.consume(bytes, sig); await globalThrottle.consume(bytes, sig); } }
|
||||
: hosterThrottle || globalThrottle;
|
||||
try {
|
||||
const result = await this._executeUploadWithAdmission(task, progressCb, signal, throttle, fileProbe, fileSize, false, jobId);
|
||||
const result = await this._executeUpload(task, progressCb, signal, throttle, fileProbe);
|
||||
if (signal.aborted || this.cancelledJobIds.has(jobId)) throw new Error('Aborted');
|
||||
this.activeJobs.delete(uploadId);
|
||||
this.sessionBytes += fileSize;
|
||||
this._suspectGoodAccounts.set(task.hoster, account.id);
|
||||
@@ -1256,7 +1201,6 @@ class UploadManager extends EventEmitter {
|
||||
suspectAlternate: true
|
||||
});
|
||||
}
|
||||
if (err && err.remoteCommitUncertain === true) throw err;
|
||||
if (signal.aborted || this.stopAfterActive) break;
|
||||
if (err && err.suspectReject === true) {
|
||||
this._noteSuspectReject(task.hoster, account.id, fileSize);
|
||||
@@ -1289,325 +1233,20 @@ class UploadManager extends EventEmitter {
|
||||
return null;
|
||||
}
|
||||
|
||||
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;
|
||||
const operation = async () => {
|
||||
this._throwIfUploadStartBlocked(signal);
|
||||
const hosterSemaphore = this._getSemaphore(task.hoster);
|
||||
const globalSemaphore = this._getGlobalSemaphore();
|
||||
let hosterSlotAcquired = false;
|
||||
let globalSlotAcquired = false;
|
||||
const releaseSlots = () => {
|
||||
if (globalSlotAcquired && globalSemaphore) globalSemaphore.release();
|
||||
if (hosterSlotAcquired) hosterSemaphore.release();
|
||||
globalSlotAcquired = false;
|
||||
hosterSlotAcquired = false;
|
||||
};
|
||||
const acquireSlots = async () => {
|
||||
await hosterSemaphore.acquire(signal);
|
||||
hosterSlotAcquired = true;
|
||||
this._throwIfUploadStartBlocked(signal);
|
||||
if (globalSemaphore) {
|
||||
await globalSemaphore.acquire(signal);
|
||||
globalSlotAcquired = true;
|
||||
this._throwIfUploadStartBlocked(signal);
|
||||
}
|
||||
};
|
||||
try {
|
||||
await this._waitForSuspectResolution(task.hoster, jobId, signal);
|
||||
this._throwIfUploadStartBlocked(signal);
|
||||
const accountFailed = !!task.accountId && this._failedAccounts.has(task.hoster + ':' + task.accountId);
|
||||
if (accountFailed) {
|
||||
if (this._swapFailedAccount(task, jobId, path.basename(task.file))) {
|
||||
retryAdmission = true;
|
||||
return null;
|
||||
}
|
||||
if (!coordinateAccountFailure) {
|
||||
const error = new Error('Account became unavailable before upload');
|
||||
error.accountUnavailable = true;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
const settings = this._getSettings(task.hoster);
|
||||
if (settings.timeIntervalSec > 0) {
|
||||
await this._waitForInterval(task.hoster, settings.timeIntervalSec * 1000, signal, acquireSlots);
|
||||
} else {
|
||||
await acquireSlots();
|
||||
}
|
||||
this._throwIfUploadStartBlocked(signal);
|
||||
if (this._hasForeignSuspectResolution(task.hoster, jobId)) {
|
||||
releaseSlots();
|
||||
await this._waitForSuspectResolution(task.hoster, jobId, signal);
|
||||
this._throwIfUploadStartBlocked(signal);
|
||||
retryAdmission = true;
|
||||
return null;
|
||||
}
|
||||
const lateMemoSuspect = coordinateAccountFailure
|
||||
? this._createSuspectMemoError(task, fileProbe, fileSize)
|
||||
: null;
|
||||
if (lateMemoSuspect) {
|
||||
this._rotLog('suspect-memo-skip', {
|
||||
jobId,
|
||||
hoster: task.hoster,
|
||||
fileName: path.basename(task.file),
|
||||
accountId: task.accountId,
|
||||
fileSize
|
||||
});
|
||||
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);
|
||||
} catch (err) {
|
||||
if (coordinateAccountFailure && err && err.suspectReject === true) {
|
||||
this._beginSuspectResolution(task.hoster, jobId);
|
||||
}
|
||||
if (err && err.remoteCommitUncertain === true) {
|
||||
if (context.recoveryClaim) throw context.recoveryClaim.markUncertain(err);
|
||||
throw err;
|
||||
}
|
||||
if (coordinateAccountFailure) await this._coordinateAccountFailure(task, err, signal, jobId);
|
||||
this._throwIfUploadStartBlocked(signal);
|
||||
throw err;
|
||||
}
|
||||
} finally {
|
||||
releaseSlots();
|
||||
}
|
||||
};
|
||||
const result = context.recoveryClaim
|
||||
? await context.recoveryClaim.runExclusive(operation, signal)
|
||||
: await operation();
|
||||
if (retryAdmission) {
|
||||
this._throwIfUploadStartBlocked(signal);
|
||||
continue;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
async _executeUpload(task, progressCb, signal, throttle, fileProbe) {
|
||||
const result = await this._executeUploadUnchecked(task, progressCb, signal, throttle, fileProbe);
|
||||
return assertUploadConfirmation(result, task.hoster);
|
||||
}
|
||||
|
||||
_throwIfUploadStartBlocked(signal) {
|
||||
if (signal && signal.aborted) {
|
||||
const error = new Error('Aborted');
|
||||
error.name = 'AbortError';
|
||||
throw error;
|
||||
}
|
||||
if (this.stopAfterActive) {
|
||||
const error = new Error('Warteschlange angehalten');
|
||||
error.stopAfterActive = true;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
_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;
|
||||
const error = new Error('Bekanntes Größen-Limit auf diesem Account (frühere verdächtige Ablehnung)');
|
||||
error.fileRejected = true;
|
||||
error.suspectReject = true;
|
||||
return error;
|
||||
}
|
||||
|
||||
_beginSuspectResolution(hoster, jobId) {
|
||||
const existing = this._suspectResolutionGates.get(hoster);
|
||||
if (existing) return existing.ownerJobId === jobId;
|
||||
let release;
|
||||
const promise = new Promise(resolve => {
|
||||
release = resolve;
|
||||
});
|
||||
this._suspectResolutionGates.set(hoster, { ownerJobId: jobId, promise, release });
|
||||
return true;
|
||||
}
|
||||
|
||||
_hasForeignSuspectResolution(hoster, jobId) {
|
||||
const gate = this._suspectResolutionGates.get(hoster);
|
||||
return !!gate && gate.ownerJobId !== jobId;
|
||||
}
|
||||
|
||||
_waitForSuspectResolution(hoster, jobId, signal) {
|
||||
const gate = this._suspectResolutionGates.get(hoster);
|
||||
if (!gate || gate.ownerJobId === jobId) return Promise.resolve();
|
||||
if (!signal) return gate.promise;
|
||||
if (signal.aborted) {
|
||||
const error = new Error('Aborted');
|
||||
error.name = 'AbortError';
|
||||
return Promise.reject(error);
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const onAbort = () => {
|
||||
signal.removeEventListener('abort', onAbort);
|
||||
const error = new Error('Aborted');
|
||||
error.name = 'AbortError';
|
||||
reject(error);
|
||||
};
|
||||
signal.addEventListener('abort', onAbort, { once: true });
|
||||
gate.promise.then(() => {
|
||||
signal.removeEventListener('abort', onAbort);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_endSuspectResolution(hoster, jobId) {
|
||||
const gate = this._suspectResolutionGates.get(hoster);
|
||||
if (!gate || gate.ownerJobId !== jobId) return false;
|
||||
this._suspectResolutionGates.delete(hoster);
|
||||
gate.release();
|
||||
return true;
|
||||
}
|
||||
|
||||
_clearSuspectResolutionGates() {
|
||||
for (const gate of this._suspectResolutionGates.values()) gate.release();
|
||||
this._suspectResolutionGates.clear();
|
||||
}
|
||||
|
||||
async _executeUpload(task, progressCb, signal, throttle, fileProbe, context) {
|
||||
const result = await this._executeUploadUnchecked(task, progressCb, signal, throttle, fileProbe, context);
|
||||
let confirmed;
|
||||
try {
|
||||
confirmed = assertUploadConfirmation(result, task.hoster);
|
||||
} catch (err) {
|
||||
if (context.recoveryClaim) throw context.recoveryClaim.markUncertain(err);
|
||||
throw err;
|
||||
}
|
||||
return confirmed;
|
||||
}
|
||||
|
||||
async _awaitUploadResult(resultPromise, signal) {
|
||||
if (!signal) return resultPromise;
|
||||
const createAbortUncertainty = () => {
|
||||
const error = new Error('Abgebrochen; Remote-Status konnte nicht bestätigt werden');
|
||||
error.name = 'AbortError';
|
||||
error.remoteCommitUncertain = true;
|
||||
error.cancelledAfterUploadStart = true;
|
||||
return error;
|
||||
};
|
||||
if (signal.aborted) {
|
||||
void Promise.resolve(resultPromise).catch(() => {});
|
||||
throw createAbortUncertainty();
|
||||
}
|
||||
let onAbort;
|
||||
const abortPromise = new Promise((resolve, reject) => {
|
||||
onAbort = () => {
|
||||
reject(createAbortUncertainty());
|
||||
};
|
||||
signal.addEventListener('abort', onAbort, { once: true });
|
||||
});
|
||||
try {
|
||||
return await Promise.race([resultPromise, abortPromise]);
|
||||
} catch (error) {
|
||||
if (!signal.aborted) throw error;
|
||||
if (error && error.remoteCommitUncertain === true) {
|
||||
error.cancelledAfterUploadStart = true;
|
||||
throw error;
|
||||
}
|
||||
throw createAbortUncertainty();
|
||||
} finally {
|
||||
signal.removeEventListener('abort', onAbort);
|
||||
}
|
||||
}
|
||||
|
||||
async _createRecoveryContext(task) {
|
||||
const fileName = path.basename(task.file);
|
||||
if ((task.hoster === 'vidmoly.me' || task.hoster === 'voe.sx') && task.username) {
|
||||
const accountIdentity = this._recoveryAccountIdentity(task);
|
||||
return {
|
||||
recoveryClaim: this._createRecoveryClaim(task, accountIdentity, fileName),
|
||||
doodApiKey: null
|
||||
};
|
||||
}
|
||||
if (task.hoster === 'doodstream.com' && task.username) {
|
||||
const doodApiKey = await this._resolveDoodstreamApiKey(task);
|
||||
const accountIdentity = doodApiKey || this._recoveryAccountIdentity(task);
|
||||
return {
|
||||
recoveryClaim: this._createRecoveryClaim(task, accountIdentity, fileName),
|
||||
doodApiKey
|
||||
};
|
||||
}
|
||||
if (task.hoster === 'byse.sx' || task.hoster === 'doodstream.com' || task.hoster === 'voe.sx') {
|
||||
const accountIdentity = task.hoster === 'byse.sx'
|
||||
? task.apiKey
|
||||
: (task.hoster === 'doodstream.com' ? task.apiKey : this._recoveryAccountIdentity(task));
|
||||
return {
|
||||
recoveryClaim: this._createRecoveryClaim(task, accountIdentity, fileName),
|
||||
doodApiKey: null
|
||||
};
|
||||
}
|
||||
return { recoveryClaim: null, doodApiKey: null };
|
||||
}
|
||||
|
||||
_recoveryAccountIdentity(task, fallbackIdentity = null) {
|
||||
for (const value of [task.accountId, task.apiKey, fallbackIdentity, task.username]) {
|
||||
if (value !== null && value !== undefined && String(value).trim()) return value;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
_createRecoveryClaim(task, accountIdentity, fileName) {
|
||||
const accountClaim = this._recoveryClaims.forUpload(task.hoster, accountIdentity, fileName);
|
||||
if (task.hoster !== 'doodstream.com' && task.hoster !== 'voe.sx') return accountClaim;
|
||||
const hosterClaim = this._recoveryClaims.forUpload(task.hoster, 'mixed-auth-recovery-boundary', fileName);
|
||||
const modeKey = `${task.hoster}\0${normalizeRecoveryTitle(fileName)}`;
|
||||
let modeState = this._recoveryAuthModes.get(modeKey);
|
||||
if (!modeState) {
|
||||
modeState = new Set();
|
||||
this._recoveryAuthModes.set(modeKey, modeState);
|
||||
}
|
||||
const authMode = task.username ? 'login' : 'api';
|
||||
return {
|
||||
has(code) {
|
||||
return accountClaim.has(code);
|
||||
},
|
||||
reserve(code) {
|
||||
return accountClaim.reserve(code);
|
||||
},
|
||||
markUncertain(error) {
|
||||
hosterClaim.markUncertain(error);
|
||||
return accountClaim.markUncertain(error);
|
||||
},
|
||||
isUncertain() {
|
||||
return hosterClaim.isUncertain() || accountClaim.isUncertain();
|
||||
},
|
||||
runExclusive(operation, signal) {
|
||||
return hosterClaim.runExclusive(
|
||||
async () => {
|
||||
if (Array.from(modeState).some(mode => mode !== authMode)) {
|
||||
const error = new Error('Gemischte Upload-Anmeldungen für denselben Remote-Titel wurden sicher blockiert');
|
||||
error.remoteCommitUncertain = true;
|
||||
error.hosterTransient = true;
|
||||
throw error;
|
||||
}
|
||||
const result = await accountClaim.runExclusive(operation, signal);
|
||||
if (result !== null && result !== undefined) modeState.add(authMode);
|
||||
return result;
|
||||
},
|
||||
signal
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async _executeUploadUnchecked(task, progressCb, signal, throttle, fileProbe, context) {
|
||||
async _executeUploadUnchecked(task, progressCb, signal, throttle, fileProbe) {
|
||||
if (task.hoster === 'vidmoly.me' && task.username) {
|
||||
return this._executeRecoveryAwareLoginUpload(task, VidmolyUploader, progressCb, signal, throttle, context.recoveryClaim);
|
||||
const vidmoly = new VidmolyUploader();
|
||||
await vidmoly.login(task.username, task.password);
|
||||
return vidmoly.upload(task.file, progressCb, signal, throttle);
|
||||
} else if (task.hoster === 'voe.sx' && task.username) {
|
||||
return this._executeRecoveryAwareLoginUpload(task, VoeUploader, progressCb, signal, throttle, context.recoveryClaim);
|
||||
const voe = new VoeUploader();
|
||||
await voe.login(task.username, task.password);
|
||||
return voe.upload(task.file, progressCb, signal, throttle);
|
||||
} else if (task.hoster === 'doodstream.com' && task.username) {
|
||||
// Login-path reliability fix: the web-form upload returns the filecode in
|
||||
// an HTML form that comes back empty for large files (doodstream backend
|
||||
@@ -1615,67 +1254,31 @@ class UploadManager extends EventEmitter {
|
||||
// session ONCE per batch and upload via the official API instead — it
|
||||
// returns result[0].filecode directly and has no empty-form failure mode.
|
||||
// Falls back to the web-form upload if no valid key can be derived.
|
||||
const apiKey = context.doodApiKey;
|
||||
const apiKey = await this._resolveDoodstreamApiKey(task);
|
||||
if (apiKey) {
|
||||
this._rotLog('doodstream-via-api', { accountId: task.accountId, fileName: path.basename(task.file) });
|
||||
return this._executeRecoveryAwareApiUpload('doodstream.com', task.file, apiKey, progressCb, signal, throttle, fileProbe, context.recoveryClaim);
|
||||
return uploadFile('doodstream.com', task.file, apiKey, progressCb, signal, throttle, {
|
||||
doodBaseline: await this._getBaseline('doodstream.com', apiKey, signal)
|
||||
});
|
||||
}
|
||||
this._rotLog('doodstream-via-web', { accountId: task.accountId, fileName: path.basename(task.file) });
|
||||
const dood = new DoodstreamUploader();
|
||||
await dood.login(task.username, task.password);
|
||||
let result;
|
||||
try {
|
||||
result = await this._awaitUploadResult(dood.upload(task.file, progressCb, signal, throttle), signal);
|
||||
} catch (err) {
|
||||
if (context.recoveryClaim && this._isDoodstreamRemoteCommitUncertain(err)) {
|
||||
throw context.recoveryClaim.markUncertain(err);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
if (result && result.file_code && !context.recoveryClaim.reserve(result.file_code)) {
|
||||
const error = new Error('Doodstream Upload lieferte eine bereits zugeordnete Remote-Identität');
|
||||
error.remoteIdentityClaimed = true;
|
||||
throw context.recoveryClaim.markUncertain(error);
|
||||
}
|
||||
return result;
|
||||
return dood.upload(task.file, progressCb, signal, throttle);
|
||||
} else if (task.hoster === 'clouddrop.cc') {
|
||||
const clouddrop = new ClouddropUploader(task.apiKey);
|
||||
return this._awaitUploadResult(clouddrop.upload(task.file, progressCb, signal, throttle), signal);
|
||||
return clouddrop.upload(task.file, progressCb, signal, throttle);
|
||||
} else {
|
||||
if (task.hoster === 'byse.sx' || task.hoster === 'doodstream.com' || task.hoster === 'voe.sx') {
|
||||
return this._executeRecoveryAwareApiUpload(task.hoster, task.file, task.apiKey, progressCb, signal, throttle, fileProbe, context.recoveryClaim);
|
||||
const baselineOpts = {};
|
||||
if (task.hoster === 'byse.sx') {
|
||||
baselineOpts.byseBaseline = await this._getBaseline('byse.sx', task.apiKey, signal);
|
||||
if (fileProbe && fileProbe.ok !== false) baselineOpts.probeIsVideoLike = fileProbe.isVideoLike === true;
|
||||
}
|
||||
return this._awaitUploadResult(uploadFile(task.hoster, task.file, task.apiKey, progressCb, signal, throttle, {}), signal);
|
||||
if (task.hoster === 'doodstream.com') baselineOpts.doodBaseline = await this._getBaseline('doodstream.com', task.apiKey, signal);
|
||||
return uploadFile(task.hoster, task.file, task.apiKey, progressCb, signal, throttle, baselineOpts);
|
||||
}
|
||||
}
|
||||
|
||||
async _executeRecoveryAwareLoginUpload(task, UploaderClass, progressCb, signal, throttle, recoveryClaim) {
|
||||
const uploader = new UploaderClass(recoveryClaim);
|
||||
await uploader.login(task.username, task.password);
|
||||
return this._awaitUploadResult(uploader.upload(task.file, progressCb, signal, throttle), signal);
|
||||
}
|
||||
|
||||
async _executeRecoveryAwareApiUpload(hosterName, filePath, apiKey, progressCb, signal, throttle, fileProbe, recoveryClaim) {
|
||||
const options = { recoveryClaim };
|
||||
if (hosterName === 'byse.sx') {
|
||||
options.byseBaseline = await this._getBaseline(hosterName, apiKey, signal);
|
||||
if (fileProbe && fileProbe.ok !== false) options.probeIsVideoLike = fileProbe.isVideoLike === true;
|
||||
} else if (hosterName === 'doodstream.com') {
|
||||
options.doodBaseline = await this._getBaseline(hosterName, apiKey, signal);
|
||||
}
|
||||
return this._awaitUploadResult(uploadFile(hosterName, filePath, apiKey, progressCb, signal, throttle, options), signal);
|
||||
}
|
||||
|
||||
_isDoodstreamRemoteCommitUncertain(error) {
|
||||
if (!error || typeof error !== 'object') return false;
|
||||
if (error.remoteCommitUncertain === true) return true;
|
||||
if (error.accountError === true || error.fileRejected === true) return false;
|
||||
const phase = error.diagnostic && error.diagnostic.phase;
|
||||
if (phase === 'upload-request') return true;
|
||||
return (phase === 'upload-response' || phase === 'upload-result-submit' || phase === 'upload-result')
|
||||
&& (error.hosterTransient === true || error.transientNetwork === true);
|
||||
}
|
||||
|
||||
_getBaseline(hosterName, apiKey, signal) {
|
||||
if (!apiKey) return Promise.resolve(null);
|
||||
const key = `${hosterName}:${apiKey}`;
|
||||
@@ -1692,27 +1295,19 @@ class UploadManager extends EventEmitter {
|
||||
// so a 40-file batch logs in + derives ONCE, not per file). The empty-string
|
||||
// sentinel distinguishes "tried, none" from "not yet tried" (undefined).
|
||||
async _resolveDoodstreamApiKey(task) {
|
||||
const accountId = task.accountId !== null && task.accountId !== undefined
|
||||
? String(task.accountId).normalize('NFKC').trim()
|
||||
: '';
|
||||
const cacheKey = accountId
|
||||
? `account:${accountId}`
|
||||
: `username:${String(task.username || '').normalize('NFKC').trim().toLowerCase()}`;
|
||||
const cacheKey = task.accountId || task.username;
|
||||
const cached = this._doodApiKeyCache.get(cacheKey);
|
||||
if (cached !== undefined) return (await cached) || null;
|
||||
if (cached !== undefined) return cached || null;
|
||||
|
||||
const pending = (async () => {
|
||||
try {
|
||||
const probe = new DoodstreamUploader();
|
||||
await probe.login(task.username, task.password);
|
||||
return (await probe.deriveApiKey()) || '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
})();
|
||||
this._doodApiKeyCache.set(cacheKey, pending);
|
||||
const key = await pending;
|
||||
if (this._doodApiKeyCache.get(cacheKey) === pending) this._doodApiKeyCache.set(cacheKey, key);
|
||||
let key = '';
|
||||
try {
|
||||
const probe = new DoodstreamUploader();
|
||||
await probe.login(task.username, task.password);
|
||||
key = (await probe.deriveApiKey()) || '';
|
||||
} catch {
|
||||
key = '';
|
||||
}
|
||||
this._doodApiKeyCache.set(cacheKey, key);
|
||||
return key || null;
|
||||
}
|
||||
|
||||
@@ -1828,7 +1423,7 @@ class UploadManager extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
_waitForInterval(hoster, intervalMs, signal, acquireSlots) {
|
||||
_waitForInterval(hoster, intervalMs, signal) {
|
||||
// Serialize interval waits per hoster so concurrent jobs queue up properly
|
||||
const prev = this.intervalLocks[hoster] || Promise.resolve();
|
||||
const next = prev.then(async () => {
|
||||
@@ -1838,7 +1433,6 @@ class UploadManager extends EventEmitter {
|
||||
if (elapsed < intervalMs) {
|
||||
await this._sleep(intervalMs - elapsed, signal);
|
||||
}
|
||||
await acquireSlots();
|
||||
this.lastStartTime[hoster] = Date.now();
|
||||
});
|
||||
this.intervalLocks[hoster] = next.catch(() => {});
|
||||
@@ -1854,18 +1448,16 @@ class UploadManager extends EventEmitter {
|
||||
const addResult = { added: 0, alreadyInBatchJobIds: [] };
|
||||
for (const task of tasks) {
|
||||
// Skip if this job is already being processed (prevent duplicates)
|
||||
if (task.jobId && this._batchJobIds.has(task.jobId)) {
|
||||
if (task.jobId && this.jobAbortControllers.has(task.jobId)) {
|
||||
addResult.alreadyInBatchJobIds.push(task.jobId);
|
||||
continue;
|
||||
}
|
||||
if (task.jobId) this._batchJobIds.add(task.jobId);
|
||||
const fileName = path.basename(task.file);
|
||||
if (!results.has(task.file)) {
|
||||
let size = 0;
|
||||
try { size = fs.statSync(task.file).size; } catch {}
|
||||
results.set(task.file, { name: fileName, fileKey: task.fileKey || null, size, results: [] });
|
||||
results.set(task.file, { name: fileName, size, results: [] });
|
||||
}
|
||||
this._batchTotal++;
|
||||
this._additionalPromises.push(this._runJob(task, results, signal));
|
||||
addResult.added++;
|
||||
}
|
||||
@@ -1886,7 +1478,6 @@ class UploadManager extends EventEmitter {
|
||||
|
||||
finishAfterActive() {
|
||||
this.stopAfterActive = true;
|
||||
this.uploadScheduleGate.wake();
|
||||
}
|
||||
|
||||
cancel() {
|
||||
|
||||
Reference in New Issue
Block a user