Compare commits

...

3 Commits

Author SHA1 Message Date
Administrator
e02926e849 release: v2.6.7 2026-04-06 23:02:20 +02:00
Administrator
31d157b695 test: 3 new tests for addJobs (74/74 pass)
- addJobs injects new tasks into running batch (verified concurrent execution)
- addJobs rejects duplicate jobIds already in batch
- addJobs returns added=0 when not running

These tests verify the fix in v2.6.3 (files added during upload now
get injected into the running batch via addJobsToBatch).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 22:56:26 +02:00
Administrator
cb6d61a406 🐛 fix: files added during upload now actually get uploaded
When user added new files during an active upload (drag-drop, picker
or folder monitor with pre-selected hosters), the files were pushed to
selectedFiles but NO queue jobs were created (because updateUploadView
skips buildQueuePreview during uploading=true).

The files briefly showed up via folder monitor's direct buildQueuePreview
call, but then handleBatchDone → syncSelectedFilesFromQueue removed them
from selectedFiles because they had no queue jobs.

Now: applyHosterSelection() and folder monitor both detect added files
during upload and:
1. Build preview jobs for the new files
2. Reset them to 'queued' status
3. Inject them into the running batch via addJobsToBatch IPC

The upload-manager has duplicate protection so re-injection is safe.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 22:46:31 +02:00
3 changed files with 103 additions and 2 deletions

View File

@ -1,6 +1,6 @@
{
"name": "multi-hoster-uploader",
"version": "2.6.6",
"version": "2.6.7",
"description": "Upload files to doodstream, voe, vidmoly, byse simultaneously",
"main": "main.js",
"scripts": {

View File

@ -119,10 +119,24 @@ async function init() {
}
}
if (newFiles.length > 0) {
const newPaths = new Set(newFiles.map(f => f.path));
selectedFiles.push(...newFiles);
buildQueuePreview();
updateUploadView();
if (fm.autoStart && !uploading && !healthCheckRunning) startUpload();
if (fm.autoStart && !uploading && !healthCheckRunning) {
startUpload();
} else if (uploading) {
// Inject new preview jobs into the running batch
const newJobs = queueJobs.filter(j => j.status === 'preview' && newPaths.has(j.file));
if (newJobs.length > 0) {
newJobs.forEach(j => { j.status = 'queued'; });
renderQueueTable();
window.api.addJobsToBatch({
jobs: newJobs.map(j => ({ id: j.id, file: j.file, fileName: j.fileName, hoster: j.hoster }))
}).then(result => { _markSkippedJobs(result); }).catch(() => {});
persistQueueStateSoon(true);
}
}
}
} else {
// No pre-selected hosters: open modal
@ -360,11 +374,29 @@ function applyHosterSelection() {
selectedUploadHosters = Array.from(document.querySelectorAll('input[data-hoster-modal]:checked'))
.map(input => input.dataset.hosterModal);
// Move pending files to selectedFiles on confirm
const pendingPaths = new Set(_pendingFiles.map(f => f.path));
if (_pendingFiles.length > 0) {
selectedFiles.push(..._pendingFiles);
_pendingFiles = [];
}
renderHosterSummary();
// During an active upload, build preview jobs for the new files and inject
// them into the running batch immediately (otherwise they'd be lost on
// handleBatchDone via syncSelectedFilesFromQueue)
if (uploading && pendingPaths.size > 0) {
buildQueuePreview(); // creates 'preview' jobs for new files
const newJobs = queueJobs.filter(j => j.status === 'preview' && pendingPaths.has(j.file));
if (newJobs.length > 0) {
newJobs.forEach(j => { j.status = 'queued'; });
renderQueueTable();
window.api.addJobsToBatch({
jobs: newJobs.map(j => ({ id: j.id, file: j.file, fileName: j.fileName, hoster: j.hoster }))
}).then(result => { _markSkippedJobs(result); }).catch(() => {});
persistQueueStateSoon(true);
}
}
updateUploadView();
persistQueueStateSoon(true); // immediate persist after adding files
document.getElementById('hosterModal').style.display = 'none';

View File

@ -409,6 +409,75 @@ describe('UploadManager', () => {
assert.ok(maxConcurrent <= 2, `scaleParallelUploads should cap at 2, was ${maxConcurrent}`);
});
it('addJobs injects new tasks into running batch', async () => {
let started = 0;
mockUploadFile.mock.mockImplementation(async (hoster, filePath, apiKey, onProgress) => {
started++;
await new Promise(r => setTimeout(r, 100));
if (onProgress) onProgress(fakeFileSize, fakeFileSize);
return { download_url: 'ok', embed_url: null, file_code: 'ok' };
});
const mgr = new UploadManager({});
let summary = null;
mgr.on('batch-done', (s) => { summary = s; });
// Start batch with 2 tasks
const batchPromise = mgr.startBatch([
{ jobId: 'job-1', file: '/test/a.mp4', hoster: 'doodstream.com', apiKey: 'k' },
{ jobId: 'job-2', file: '/test/b.mp4', hoster: 'doodstream.com', apiKey: 'k' }
]);
// After 30ms (during upload), inject 2 more tasks
await new Promise(r => setTimeout(r, 30));
const result = mgr.addJobs([
{ jobId: 'job-3', file: '/test/c.mp4', hoster: 'doodstream.com', apiKey: 'k' },
{ jobId: 'job-4', file: '/test/d.mp4', hoster: 'doodstream.com', apiKey: 'k' }
]);
assert.equal(result.added, 2, 'should add 2 new jobs');
assert.equal(result.alreadyInBatchJobIds.length, 0);
await batchPromise;
assert.ok(summary);
assert.equal(started, 4, 'all 4 jobs should have run');
});
it('addJobs rejects duplicates already in running batch', async () => {
mockUploadFile.mock.mockImplementation(async (hoster, filePath, apiKey, onProgress, signal) => {
// Slow upload so we can add jobs while it's running
await new Promise((resolve, reject) => {
const timer = setTimeout(resolve, 200);
if (signal) signal.addEventListener('abort', () => { clearTimeout(timer); reject(new Error('Aborted')); });
});
if (onProgress) onProgress(fakeFileSize, fakeFileSize);
return { download_url: 'ok', embed_url: null, file_code: 'ok' };
});
const mgr = new UploadManager({});
const batchPromise = mgr.startBatch([
{ jobId: 'job-A', file: '/test/x.mp4', hoster: 'doodstream.com', apiKey: 'k' }
]);
// Try to add the SAME jobId while it's running
await new Promise(r => setTimeout(r, 50));
const result = mgr.addJobs([
{ jobId: 'job-A', file: '/test/x.mp4', hoster: 'doodstream.com', apiKey: 'k' },
{ jobId: 'job-B', file: '/test/y.mp4', hoster: 'doodstream.com', apiKey: 'k' }
]);
assert.equal(result.added, 1, 'should skip duplicate jobId, add only the new one');
assert.deepEqual(result.alreadyInBatchJobIds, ['job-A']);
await batchPromise;
});
it('addJobs returns added=0 when not running', () => {
const mgr = new UploadManager({});
const result = mgr.addJobs([
{ jobId: 'job-1', file: '/test/a.mp4', hoster: 'doodstream.com', apiKey: 'k' }
]);
assert.equal(result.added, 0);
});
it('stats event contains expected fields', async () => {
// Make upload take long enough for stats interval to fire
mockUploadFile.mock.mockImplementation(async (hoster, filePath, apiKey, onProgress, signal) => {