|
|
|
|
@ -132,6 +132,12 @@ class UploadManager extends EventEmitter {
|
|
|
|
|
this.globalSemaphore = null;
|
|
|
|
|
this.globalThrottle = null;
|
|
|
|
|
this.lastStartTime = {};
|
|
|
|
|
// Reset account-rotation state each batch. Otherwise a previously failed
|
|
|
|
|
// account (e.g. rate-limited during the last batch) stays permanently
|
|
|
|
|
// blacklisted until the app restarts — every upload would silently skip
|
|
|
|
|
// straight to the fallback even after the original recovered.
|
|
|
|
|
this._failedAccounts.clear();
|
|
|
|
|
this._accountOverrides.clear();
|
|
|
|
|
|
|
|
|
|
const { signal } = this.abortController;
|
|
|
|
|
const batchId = `batch-${Date.now()}`;
|
|
|
|
|
@ -461,79 +467,85 @@ class UploadManager extends EventEmitter {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Account fallback: if this account hasn't failed before, try switching
|
|
|
|
|
if (task.accountId && !this._failedAccounts.has(task.hoster + ':' + task.accountId)) {
|
|
|
|
|
// Account rotation: mark the current account failed, wait for main to
|
|
|
|
|
// resolve the next fallback, then retry. Loop so A → B → C → ... works
|
|
|
|
|
// for hosters with 3+ accounts (the old code only did one level: A → B
|
|
|
|
|
// and stopped, even if C would have worked).
|
|
|
|
|
while (task.accountId && !this._failedAccounts.has(task.hoster + ':' + task.accountId)) {
|
|
|
|
|
if (signal.aborted || this.stopAfterActive) break;
|
|
|
|
|
this._failedAccounts.set(task.hoster + ':' + task.accountId, true);
|
|
|
|
|
this.emit('account-failed', { hoster: task.hoster, accountId: task.accountId });
|
|
|
|
|
// Wait briefly for switchAccount() to be called from main process
|
|
|
|
|
await this._sleep(800, signal);
|
|
|
|
|
const override = this._accountOverrides.get(task.hoster);
|
|
|
|
|
if (override && !this._failedAccounts.has(task.hoster + ':' + override.id)) {
|
|
|
|
|
// Switch to fallback account and retry this file
|
|
|
|
|
task.accountId = override.id;
|
|
|
|
|
task.username = override.username;
|
|
|
|
|
task.password = override.password;
|
|
|
|
|
task.apiKey = override.apiKey;
|
|
|
|
|
this._emitProgress(uploadId, fileName, task.hoster, {
|
|
|
|
|
jobId, status: 'retrying', progress: 0, bytesUploaded: 0, bytesTotal: fileSize,
|
|
|
|
|
speedKbs: 0, elapsed: 0, remaining: 0,
|
|
|
|
|
error: 'Account-Wechsel zu Fallback', result: null, attempt: 1, maxAttempts
|
|
|
|
|
});
|
|
|
|
|
// Re-run retry loop with new account
|
|
|
|
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
|
|
|
if (signal.aborted || this.stopAfterActive) break;
|
|
|
|
|
if (attempt > 1) {
|
|
|
|
|
if (!override || this._failedAccounts.has(task.hoster + ':' + override.id)) break;
|
|
|
|
|
// Switch to fallback account and retry this file
|
|
|
|
|
task.accountId = override.id;
|
|
|
|
|
task.username = override.username;
|
|
|
|
|
task.password = override.password;
|
|
|
|
|
task.apiKey = override.apiKey;
|
|
|
|
|
this._emitProgress(uploadId, fileName, task.hoster, {
|
|
|
|
|
jobId, status: 'retrying', progress: 0, bytesUploaded: 0, bytesTotal: fileSize,
|
|
|
|
|
speedKbs: 0, elapsed: 0, remaining: 0,
|
|
|
|
|
error: 'Account-Wechsel zu Fallback', result: null, attempt: 1, maxAttempts
|
|
|
|
|
});
|
|
|
|
|
// Retry loop with the new account. On exhausted failure, the while
|
|
|
|
|
// loop iterates: marks this account failed too, asks main for the next
|
|
|
|
|
// fallback, and so on.
|
|
|
|
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
|
|
|
if (signal.aborted || this.stopAfterActive) break;
|
|
|
|
|
if (attempt > 1) {
|
|
|
|
|
this._emitProgress(uploadId, fileName, task.hoster, {
|
|
|
|
|
jobId, status: 'retrying', progress: 0, bytesUploaded: 0, bytesTotal: fileSize,
|
|
|
|
|
speedKbs: 0, elapsed: 0, remaining: 0,
|
|
|
|
|
error: lastError ? lastError.message : '', result: null, attempt, maxAttempts
|
|
|
|
|
});
|
|
|
|
|
await this._sleep(3000, signal);
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
const jobStart = Date.now();
|
|
|
|
|
let lastBytes = 0;
|
|
|
|
|
let lastSpeedTime = jobStart;
|
|
|
|
|
let currentSpeedKbs = 0;
|
|
|
|
|
const activeEntry = { jobId, speedKbs: 0, bytesUploaded: 0 };
|
|
|
|
|
this.activeJobs.set(uploadId, activeEntry);
|
|
|
|
|
|
|
|
|
|
const progressCb = (bytesUploaded, bytesTotal) => {
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
const timeDelta = (now - lastSpeedTime) / 1000;
|
|
|
|
|
if (timeDelta >= 1) {
|
|
|
|
|
currentSpeedKbs = Math.round((bytesUploaded - lastBytes) / timeDelta / 1024);
|
|
|
|
|
lastBytes = bytesUploaded;
|
|
|
|
|
lastSpeedTime = now;
|
|
|
|
|
}
|
|
|
|
|
activeEntry.speedKbs = currentSpeedKbs;
|
|
|
|
|
activeEntry.bytesUploaded = bytesUploaded;
|
|
|
|
|
const elapsed = Math.round((now - jobStart) / 1000);
|
|
|
|
|
const remaining = currentSpeedKbs > 0 ? Math.round((bytesTotal - bytesUploaded) / (currentSpeedKbs * 1024)) : 0;
|
|
|
|
|
this._emitProgress(uploadId, fileName, task.hoster, {
|
|
|
|
|
jobId, status: 'retrying', progress: 0, bytesUploaded: 0, bytesTotal: fileSize,
|
|
|
|
|
speedKbs: 0, elapsed: 0, remaining: 0,
|
|
|
|
|
error: lastError ? lastError.message : '', result: null, attempt, maxAttempts
|
|
|
|
|
jobId, status: 'uploading',
|
|
|
|
|
progress: bytesTotal > 0 ? Math.min(1, bytesUploaded / bytesTotal) : 0,
|
|
|
|
|
bytesUploaded, bytesTotal, speedKbs: currentSpeedKbs,
|
|
|
|
|
elapsed, remaining, error: null, result: null, attempt, maxAttempts
|
|
|
|
|
});
|
|
|
|
|
await this._sleep(3000, signal);
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
const jobStart = Date.now();
|
|
|
|
|
let lastBytes = 0;
|
|
|
|
|
let lastSpeedTime = jobStart;
|
|
|
|
|
let currentSpeedKbs = 0;
|
|
|
|
|
this.activeJobs.set(uploadId, { jobId, speedKbs: 0, bytesUploaded: 0 });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const progressCb = (bytesUploaded, bytesTotal) => {
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
const timeDelta = (now - lastSpeedTime) / 1000;
|
|
|
|
|
if (timeDelta >= 1) {
|
|
|
|
|
currentSpeedKbs = Math.round((bytesUploaded - lastBytes) / timeDelta / 1024);
|
|
|
|
|
lastBytes = bytesUploaded;
|
|
|
|
|
lastSpeedTime = now;
|
|
|
|
|
}
|
|
|
|
|
this.activeJobs.set(uploadId, { jobId, speedKbs: currentSpeedKbs, bytesUploaded });
|
|
|
|
|
const elapsed = Math.round((now - jobStart) / 1000);
|
|
|
|
|
const remaining = currentSpeedKbs > 0 ? Math.round((bytesTotal - bytesUploaded) / (currentSpeedKbs * 1024)) : 0;
|
|
|
|
|
this._emitProgress(uploadId, fileName, task.hoster, {
|
|
|
|
|
jobId, status: 'uploading',
|
|
|
|
|
progress: bytesTotal > 0 ? Math.min(1, bytesUploaded / bytesTotal) : 0,
|
|
|
|
|
bytesUploaded, bytesTotal, speedKbs: currentSpeedKbs,
|
|
|
|
|
elapsed, remaining, error: null, result: null, attempt, maxAttempts
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
const hosterThrottle = settings.maxSpeedKbs > 0 ? new Throttle(settings.maxSpeedKbs * 1024) : null;
|
|
|
|
|
const globalThrottle = this._getGlobalThrottle();
|
|
|
|
|
const throttle = hosterThrottle && globalThrottle
|
|
|
|
|
? { consume: async (bytes, sig) => { await hosterThrottle.consume(bytes, sig); await globalThrottle.consume(bytes, sig); } }
|
|
|
|
|
: hosterThrottle || globalThrottle;
|
|
|
|
|
|
|
|
|
|
const hosterThrottle = settings.maxSpeedKbs > 0 ? new Throttle(settings.maxSpeedKbs * 1024) : null;
|
|
|
|
|
const globalThrottle = this._getGlobalThrottle();
|
|
|
|
|
const throttle = hosterThrottle && globalThrottle
|
|
|
|
|
? { consume: async (bytes, sig) => { await hosterThrottle.consume(bytes, sig); await globalThrottle.consume(bytes, sig); } }
|
|
|
|
|
: hosterThrottle || globalThrottle;
|
|
|
|
|
|
|
|
|
|
const result = await this._executeUpload(task, progressCb, signal, throttle);
|
|
|
|
|
this.activeJobs.delete(uploadId);
|
|
|
|
|
this.sessionBytes += fileSize;
|
|
|
|
|
emitFinalStatus('done', { result, speedKbs: currentSpeedKbs, elapsed: Math.round((Date.now() - jobStart) / 1000), attempt });
|
|
|
|
|
recordFinalResult('done', { result });
|
|
|
|
|
return;
|
|
|
|
|
} catch (err) {
|
|
|
|
|
this.activeJobs.delete(uploadId);
|
|
|
|
|
lastError = err;
|
|
|
|
|
if (signal.aborted || this.stopAfterActive) break;
|
|
|
|
|
if (attempt >= maxAttempts) break;
|
|
|
|
|
}
|
|
|
|
|
const result = await this._executeUpload(task, progressCb, signal, throttle);
|
|
|
|
|
this.activeJobs.delete(uploadId);
|
|
|
|
|
this.sessionBytes += fileSize;
|
|
|
|
|
emitFinalStatus('done', { result, speedKbs: currentSpeedKbs, elapsed: Math.round((Date.now() - jobStart) / 1000), attempt });
|
|
|
|
|
recordFinalResult('done', { result });
|
|
|
|
|
return;
|
|
|
|
|
} catch (err) {
|
|
|
|
|
this.activeJobs.delete(uploadId);
|
|
|
|
|
lastError = err;
|
|
|
|
|
if (signal.aborted || this.stopAfterActive) break;
|
|
|
|
|
if (attempt >= maxAttempts) break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|