diff --git a/lib/config-store.js b/lib/config-store.js index 31a4d59..23ad58a 100644 --- a/lib/config-store.js +++ b/lib/config-store.js @@ -31,6 +31,7 @@ const DEFAULTS = { parallelUploadCount: 0, // 0 = use per-hoster limits only scaleParallelUploads: false, removeFromQueueOnDone: false, + globalMaxSpeedKbs: 0, // 0 = unlimited global speed pendingQueue: null, scramble: { active: false, diff --git a/lib/upload-manager.js b/lib/upload-manager.js index 98469d8..f583703 100644 --- a/lib/upload-manager.js +++ b/lib/upload-manager.js @@ -34,6 +34,8 @@ class UploadManager extends EventEmitter { this.jobAbortControllers = new Map(); // jobId -> AbortController this.cancelledJobIds = new Set(); this.sessionBytes = 0; + this.lastStartTime = {}; // hoster -> timestamp of last upload start + this.globalThrottle = null; } _getSettings(hoster) { @@ -62,6 +64,17 @@ class UploadManager extends EventEmitter { return this.globalSemaphore; } + _getGlobalThrottle() { + const kbs = Number(this.globalSettings.globalMaxSpeedKbs || 0); + if (!Number.isFinite(kbs) || kbs <= 0) return null; + if (!this.globalThrottle) { + this.globalThrottle = new Throttle(kbs * 1024); + } else { + this.globalThrottle.updateRate(kbs * 1024); + } + return this.globalThrottle; + } + _getSemaphore(hoster) { if (!this.semaphores[hoster]) { const settings = this._getSettings(hoster); @@ -83,6 +96,8 @@ class UploadManager extends EventEmitter { this.cancelledJobIds.clear(); this.semaphores = {}; this.globalSemaphore = null; + this.globalThrottle = null; + this.lastStartTime = {}; const { signal } = this.abortController; const batchId = `batch-${Date.now()}`; @@ -206,7 +221,7 @@ class UploadManager extends EventEmitter { hosterSlotAcquired = true; if (settings.timeIntervalSec > 0) { - await this._sleep(settings.timeIntervalSec * 1000, signal); + await this._waitForInterval(task.hoster, settings.timeIntervalSec * 1000, signal); } for (let attempt = 1; attempt <= maxAttempts; attempt++) { @@ -255,9 +270,13 @@ class UploadManager extends EventEmitter { maxAttempts }); - const throttle = settings.maxSpeedKbs > 0 + 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; if (settings.restartBelowKbs > 0) { speedAbort = new AbortController(); @@ -503,6 +522,16 @@ class UploadManager extends EventEmitter { }); } + async _waitForInterval(hoster, intervalMs, signal) { + const now = Date.now(); + const last = this.lastStartTime[hoster] || 0; + const elapsed = now - last; + if (elapsed < intervalMs) { + await this._sleep(intervalMs - elapsed, signal); + } + this.lastStartTime[hoster] = Date.now(); + } + cancelJobs(jobIds) { for (const jobId of jobIds || []) { if (!jobId) continue; diff --git a/main.js b/main.js index 540f8db..ef42c01 100644 --- a/main.js +++ b/main.js @@ -540,6 +540,13 @@ ipcMain.handle('start-upload', (_event, payload) => { if (data.status !== 'uploading') { debugLog(`progress: ${data.fileName} ${data.hoster} ${data.status} ${data.error || ''}`); } + // Write to fileuploader.log immediately when a single upload finishes + if (data.status === 'done' && data.result) { + const link = data.result.download_url || data.result.embed_url || ''; + if (link) { + appendUploadLog(data.hoster || '', link, data.fileName || ''); + } + } if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send('upload-progress', data); } @@ -554,18 +561,6 @@ ipcMain.handle('start-upload', (_event, payload) => { uploadManager.on('batch-done', async (summary) => { debugLog(`batch-done: total=${summary.total} ok=${summary.succeeded} fail=${summary.failed}`); await configStore.appendHistory(summary); - // Write successful uploads to fileuploader.log - for (const file of summary.files || []) { - for (const result of file.results || []) { - if (result.status === 'done' && (result.download_url || result.embed_url)) { - appendUploadLog( - result.hoster || '', - result.download_url || result.embed_url || '', - file.name || '' - ); - } - } - } if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send('upload-batch-done', summary); } diff --git a/renderer/app.js b/renderer/app.js index ffd79b1..9ec9698 100644 --- a/renderer/app.js +++ b/renderer/app.js @@ -923,6 +923,19 @@ async function startUpload() { async function cancelUpload() { await window.api.cancelUpload(); uploading = false; + // Reset all non-finished jobs back to queued state + for (const job of queueJobs) { + if (!['done', 'error', 'skipped'].includes(job.status)) { + job.status = 'queued'; + job.progress = 0; + job.bytesUploaded = 0; + job.speedKbs = 0; + job.elapsed = 0; + job.remaining = 0; + job.error = null; + } + } + renderQueueTable(); updateQueueActionButtons(); updateStatusBar(); persistQueueStateSoon(); @@ -975,6 +988,13 @@ function handleProgress(data) { maybeAddSessionFile(job); + // Remove finished jobs from queue immediately if setting is enabled + if (job.status === 'done' && config.globalSettings && config.globalSettings.removeFromQueueOnDone) { + removeJobFromIndex(job); + selectedJobIds.delete(job.id); + queueJobs = queueJobs.filter(j => j !== job); + } + scheduleQueueRender(); updateQueueActionButtons(); updateStatusBar(); @@ -984,6 +1004,20 @@ function handleProgress(data) { function handleBatchDone(summary) { uploading = false; applySummaryResults(summary); + + // Reset aborted jobs back to queued so they can be restarted + for (const job of queueJobs) { + if (job.status === 'aborted') { + job.status = 'queued'; + job.progress = 0; + job.bytesUploaded = 0; + job.speedKbs = 0; + job.elapsed = 0; + job.remaining = 0; + job.error = null; + } + } + syncSelectedFilesFromQueue(); updateQueueActionButtons(); renderQueueTable(); @@ -1307,32 +1341,40 @@ function renderSettings() { System