diff --git a/lib/upload-manager.js b/lib/upload-manager.js index 0068dc7..b92d2f7 100644 --- a/lib/upload-manager.js +++ b/lib/upload-manager.js @@ -698,13 +698,18 @@ class UploadManager extends EventEmitter { } addJobs(tasks) { - if (!this.running || !tasks || tasks.length === 0) return 0; + if (!this.running || !tasks || tasks.length === 0) { + return { added: 0, alreadyInBatchJobIds: [] }; + } const { signal } = this.abortController; const results = this._batchResults || new Map(); - let added = 0; + const addResult = { added: 0, alreadyInBatchJobIds: [] }; for (const task of tasks) { // Skip if this job is already being processed (prevent duplicates) - if (task.jobId && this.jobAbortControllers.has(task.jobId)) continue; + if (task.jobId && this.jobAbortControllers.has(task.jobId)) { + addResult.alreadyInBatchJobIds.push(task.jobId); + continue; + } const fileName = path.basename(task.file); if (!results.has(task.file)) { let size = 0; @@ -712,9 +717,9 @@ class UploadManager extends EventEmitter { results.set(task.file, { name: fileName, size, results: [] }); } this._additionalPromises.push(this._runJob(task, results, signal)); - added++; + addResult.added++; } - return added; + return addResult; } cancelJobs(jobIds) { diff --git a/main.js b/main.js index f758bc9..4db690d 100644 --- a/main.js +++ b/main.js @@ -785,10 +785,26 @@ ipcMain.handle('add-jobs-to-batch', (_event, payload) => { 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 }; - const added = uploadManager.addJobs(tasks); - debugLog(`add-jobs-to-batch: ${added} of ${tasks.length} tasks added (${tasks.length - added} already in batch)`); - return { added }; + const taskJobIds = new Set(tasks.map(t => t.jobId).filter(Boolean)); + const skippedJobs = jobs + .filter(j => j && j.id && !taskJobIds.has(j.id)) + .map(j => ({ jobId: j.id, hoster: j.hoster, reason: 'Kein gültiger Account für diesen Hoster' })); + + if (tasks.length === 0) { + debugLog(`add-jobs-to-batch: 0 tasks built (${skippedJobs.length} skipped: no account)`); + return { added: 0, skippedJobs, alreadyInBatchJobIds: [] }; + } + + const addResult = uploadManager.addJobs(tasks); + const added = typeof addResult === 'number' ? addResult : (addResult && addResult.added) || 0; + const alreadyInBatchJobIds = (addResult && Array.isArray(addResult.alreadyInBatchJobIds)) + ? addResult.alreadyInBatchJobIds + : []; + + debugLog( + `add-jobs-to-batch: ${added} of ${tasks.length} tasks added (${alreadyInBatchJobIds.length} already in batch, ${skippedJobs.length} skipped)` + ); + return { added, skippedJobs, alreadyInBatchJobIds }; }); ipcMain.handle('finish-after-active', () => { diff --git a/renderer/app.js b/renderer/app.js index ca91688..2fb555a 100644 --- a/renderer/app.js +++ b/renderer/app.js @@ -1363,20 +1363,44 @@ async function startSelectedUpload() { j.bytesUploaded = 0; j.speedKbs = 0; j.progress = 0; j.uploadId = null; }); renderQueueTable(); - const result = await window.api.addJobsToBatch({ - jobs: addable.map(j => ({ id: j.id, file: j.file, fileName: j.fileName, hoster: j.hoster })) - }); + let result = null; + try { + result = await window.api.addJobsToBatch({ + jobs: addable.map(j => ({ id: j.id, file: j.file, fileName: j.fileName, hoster: j.hoster })) + }); + } catch (err) { + showCopyToast(`Jobs konnten nicht hinzugefuegt werden: ${err.message}`); + return; + } + + // If the batch ended between UI-state and IPC call, start a fresh batch immediately + if (result && result.error === 'Kein Upload aktiv') { + uploading = false; + updateQueueActionButtons(); + updateStatusBar(); + await startSelectedUpload(); + return; + } _markSkippedJobs(result); persistQueueStateSoon(); - const added = result && result.added || 0; - const alreadyInBatch = addable.length - added; - if (added > 0 && alreadyInBatch > 0) { - showCopyToast(`${added} Jobs hinzugefügt, ${alreadyInBatch} waren schon im Batch`); - } else if (added > 0) { - showCopyToast(`${added} Jobs zum laufenden Upload hinzugefügt`); + const added = Number(result && result.added) || 0; + // Use ASCII-only toast text here to avoid encoding artifacts on some systems. + const skipped = Array.isArray(result && result.skippedJobs) ? result.skippedJobs.length : 0; + const alreadyInBatch = Array.isArray(result && result.alreadyInBatchJobIds) + ? result.alreadyInBatchJobIds.length + : Math.max(0, addable.length - added - skipped); + const toastParts = []; + if (added > 0) toastParts.push(`${added} hinzugefuegt`); + if (alreadyInBatch > 0) toastParts.push(`${alreadyInBatch} bereits im Batch`); + if (skipped > 0) toastParts.push(`${skipped} ohne gueltigen Account`); + if (result && result.error) { + showCopyToast(`Jobs konnten nicht hinzugefuegt werden: ${result.error}`); + } else if (toastParts.length > 0) { + showCopyToast(`Jobs: ${toastParts.join(', ')}`); } else { - showCopyToast(`${addable.length} Jobs sind bereits im laufenden Batch`); + showCopyToast('Keine Jobs hinzugefuegt'); } + return; } return; } diff --git a/tests/upload-manager.test.js b/tests/upload-manager.test.js index 3b2717f..da57c56 100644 --- a/tests/upload-manager.test.js +++ b/tests/upload-manager.test.js @@ -242,6 +242,55 @@ describe('UploadManager', () => { assert.ok(statuses.some((entry) => entry.jobId === 'selected-job' && entry.status === 'aborted')); }); + it('addJobs returns duplicate info and still runs newly queued jobs', async () => { + let releaseFirst = null; + + mockUploadFile.mock.mockImplementation(async (hoster, filePath, apiKey, onProgress, signal) => { + if (filePath.endsWith('/first.mp4')) { + await new Promise((resolve, reject) => { + releaseFirst = resolve; + if (signal) { + signal.addEventListener('abort', () => reject(new Error('Aborted')), { once: true }); + } + }); + } else { + await new Promise((resolve) => setTimeout(resolve, 20)); + } + if (onProgress) onProgress(fakeFileSize, fakeFileSize); + return { download_url: 'https://test/ok', embed_url: null, file_code: 'ok' }; + }); + + const mgr = new UploadManager({ + 'doodstream.com': { retries: 0, parallelCount: 1, maxSpeedKbs: 0, restartBelowKbs: 0, timeIntervalSec: 0, maxSizeMb: 0 } + }); + const statuses = []; + mgr.on('progress', (data) => statuses.push({ jobId: data.jobId, status: data.status })); + + const batchPromise = mgr.startBatch([ + { jobId: 'job-first', file: '/test/first.mp4', hoster: 'doodstream.com', apiKey: 'key1' } + ]); + + for (let i = 0; i < 50 && !releaseFirst; i++) { + await new Promise((resolve) => setTimeout(resolve, 10)); + } + assert.equal(typeof releaseFirst, 'function', 'first job should be running before addJobs'); + + const addResult = mgr.addJobs([ + { jobId: 'job-first', file: '/test/first.mp4', hoster: 'doodstream.com', apiKey: 'key1' }, + { jobId: 'job-second', file: '/test/second.mp4', hoster: 'doodstream.com', apiKey: 'key1' }, + { jobId: 'job-third', file: '/test/third.mp4', hoster: 'doodstream.com', apiKey: 'key1' } + ]); + + assert.equal(addResult.added, 2); + assert.deepEqual(addResult.alreadyInBatchJobIds, ['job-first']); + + releaseFirst(); + await batchPromise; + + assert.ok(statuses.some((entry) => entry.jobId === 'job-second' && entry.status === 'done')); + assert.ok(statuses.some((entry) => entry.jobId === 'job-third' && entry.status === 'done')); + }); + it('_combineSignals propagates abort from either source', () => { const mgr = new UploadManager({}); const ac1 = new AbortController();