From e1b03605fa9e0203603a229ca43f5a08018dc7dd Mon Sep 17 00:00:00 2001 From: Administrator Date: Mon, 23 Mar 2026 18:15:31 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20retry/start=20selected=20jo?= =?UTF-8?q?bs=20while=20upload=20batch=20is=20running?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, 'Erneut versuchen' and 'Ausgewählte starten' did nothing when a batch was already running (uploading=true). Failed jobs were set to 'Wartet' but never actually uploaded because they couldn't be added to the running batch. New: upload-manager.addJobs() allows adding tasks to a running batch. When a batch is active and user retries/starts jobs, they're injected into the running batch via IPC 'add-jobs-to-batch'. The upload manager starts processing them immediately using the existing semaphores. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/upload-manager.js | 19 +++++++++++++++++++ main.js | 13 +++++++++++++ preload.js | 1 + renderer/app.js | 18 +++++++++++++++++- 4 files changed, 50 insertions(+), 1 deletion(-) diff --git a/lib/upload-manager.js b/lib/upload-manager.js index 247c61f..03c0e5f 100644 --- a/lib/upload-manager.js +++ b/lib/upload-manager.js @@ -135,6 +135,7 @@ class UploadManager extends EventEmitter { const { signal } = this.abortController; const batchId = `batch-${Date.now()}`; const results = new Map(); // filePath -> { name, size, results: [] } + this._batchResults = results; for (const task of tasks) { const fileName = path.basename(task.file); @@ -690,6 +691,24 @@ class UploadManager extends EventEmitter { return next; } + addJobs(tasks) { + if (!this.running || !tasks || tasks.length === 0) return; + const { signal } = this.abortController; + const results = this._batchResults || new Map(); + for (const task of tasks) { + 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, size, results: [] }); + } + } + // Start each new job — they'll acquire semaphores and run + for (const task of tasks) { + this._runJob(task, results, signal); + } + } + cancelJobs(jobIds) { for (const jobId of jobIds || []) { if (!jobId) continue; diff --git a/main.js b/main.js index 5e906f6..2709d4a 100644 --- a/main.js +++ b/main.js @@ -778,6 +778,19 @@ ipcMain.handle('cancel-selected-jobs', (_event, jobIds) => { return true; }); +ipcMain.handle('add-jobs-to-batch', (_event, payload) => { + if (!uploadManager || !uploadManager.running) { + return { error: 'Kein Upload aktiv' }; + } + const config = configStore.load(); + const jobs = payload && Array.isArray(payload.jobs) ? payload.jobs : []; + const tasks = buildUploadTasksFromJobs(config, jobs); + if (tasks.length === 0) return { added: 0 }; + uploadManager.addJobs(tasks); + debugLog(`add-jobs-to-batch: added ${tasks.length} tasks to running batch`); + return { added: tasks.length }; +}); + ipcMain.handle('finish-after-active', () => { if (uploadManager) { uploadManager.finishAfterActive(); diff --git a/preload.js b/preload.js index e1f5def..27f9318 100644 --- a/preload.js +++ b/preload.js @@ -34,6 +34,7 @@ contextBridge.exposeInMainWorld('api', { startUpload: (payload) => ipcRenderer.invoke('start-upload', payload), cancelUpload: () => ipcRenderer.invoke('cancel-upload'), cancelSelectedJobs: (jobIds) => ipcRenderer.invoke('cancel-selected-jobs', jobIds), + addJobsToBatch: (payload) => ipcRenderer.invoke('add-jobs-to-batch', payload), finishAfterActive: () => ipcRenderer.invoke('finish-after-active'), runHealthCheck: (payload) => ipcRenderer.invoke('run-health-check', payload), diff --git a/renderer/app.js b/renderer/app.js index 85c55e2..52d0fd0 100644 --- a/renderer/app.js +++ b/renderer/app.js @@ -1352,7 +1352,23 @@ function _markSkippedJobs(result) { } async function startSelectedUpload() { - if (uploading) return; + if (uploading) { + // Batch already running — add selected jobs to the running batch + const retryable = queueJobs.filter(j => selectedJobIds.has(j.id) && ['queued', 'error', 'aborted', 'skipped'].includes(j.status)); + if (retryable.length > 0) { + retryable.forEach(j => { + j.status = 'queued'; j.error = null; j.result = null; + j.bytesUploaded = 0; j.speedKbs = 0; j.progress = 0; j.uploadId = null; + }); + renderQueueTable(); + const result = await window.api.addJobsToBatch({ + jobs: retryable.map(j => ({ id: j.id, file: j.file, fileName: j.fileName, hoster: j.hoster })) + }); + _markSkippedJobs(result); + persistQueueStateSoon(); + } + return; + } uploading = true; // set immediately to prevent double-click race updateQueueActionButtons();