Compare commits

..

3 Commits

Author SHA1 Message Date
Administrator
39ccb904ef release: v1.6.9 2026-03-11 13:47:19 +01:00
Administrator
e389b625d6 fix: prevent double context menu on recent files right-click
stopPropagation prevents the event from bubbling to the upload-view
handler which was showing a second context menu.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 13:46:54 +01:00
Administrator
25a6b77650 fix: multiple bugs found in deep code analysis
- Guard startBatch against null uploadManager in nextTick (race on fast cancel)
- Fix updateSettings not creating globalThrottle when none existed at start
- Fix updateSettings not updating globalSemaphore limit live
- Fix retry pause: 2500ms → 3000ms as intended
- Remove dead isError code in history (was always false after continue)
- Add signal.aborted check in API upload generator (hosters.js)
- Add extra signal check in throttle consume loop for faster abort
- Fix doodstream debug log path (process.cwd → __dirname)
- Fix updater fetchJson signal listener leak
- Make progress column sortable in queue table

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 04:16:50 +01:00
9 changed files with 25 additions and 12 deletions

View File

@ -10,7 +10,7 @@ const UPLOAD_TIMEOUT = 1800000; // 30 min
function _debugLog(msg) { function _debugLog(msg) {
try { try {
const ts = new Date().toISOString(); const ts = new Date().toISOString();
fs.appendFileSync(path.join(process.cwd(), 'doodstream-debug.log'), `[${ts}] ${msg}\n`); fs.appendFileSync(path.join(__dirname, '..', 'doodstream-debug.log'), `[${ts}] ${msg}\n`);
} catch {} } catch {}
} }

View File

@ -254,6 +254,7 @@ function createUploadBody(filePath, formFields, onProgress, throttle, signal) {
yield preambleBuf; yield preambleBuf;
const fileStream = fs.createReadStream(filePath, { highWaterMark: CHUNK_SIZE }); const fileStream = fs.createReadStream(filePath, { highWaterMark: CHUNK_SIZE });
for await (const chunk of fileStream) { for await (const chunk of fileStream) {
if (signal && signal.aborted) throw new Error('Aborted');
if (throttle) await throttle.consume(chunk.length, signal); if (throttle) await throttle.consume(chunk.length, signal);
bytesRead += chunk.length; bytesRead += chunk.length;
yield chunk; yield chunk;

View File

@ -21,6 +21,7 @@ class Throttle {
bytes -= available; bytes -= available;
} }
if (bytes > 0) { if (bytes > 0) {
if (signal && signal.aborted) return;
// Wait 50ms for tokens to refill // Wait 50ms for tokens to refill
await new Promise((r) => setTimeout(r, 50)); await new Promise((r) => setTimeout(r, 50));
} }

View File

@ -56,7 +56,8 @@ function findLatestYml(assets) {
async function fetchJson(url, signal) { async function fetchJson(url, signal) {
const controller = new AbortController(); const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), CHECK_TIMEOUT); const timeout = setTimeout(() => controller.abort(), CHECK_TIMEOUT);
if (signal) signal.addEventListener('abort', () => controller.abort()); const onAbort = () => controller.abort();
if (signal) signal.addEventListener('abort', onAbort);
try { try {
const res = await fetch(url, { const res = await fetch(url, {
@ -67,6 +68,7 @@ async function fetchJson(url, signal) {
return await res.json(); return await res.json();
} finally { } finally {
clearTimeout(timeout); clearTimeout(timeout);
if (signal) signal.removeEventListener('abort', onAbort);
} }
} }

View File

@ -48,13 +48,20 @@ class UploadManager extends EventEmitter {
sem.updateLimit(settings.parallelCount); sem.updateLimit(settings.parallelCount);
} }
// Update global throttle if speed limit changed // Update global throttle if speed limit changed
if (this.globalThrottle) {
const newKbs = (this.globalSettings.globalMaxSpeedKbs || 0); const newKbs = (this.globalSettings.globalMaxSpeedKbs || 0);
if (newKbs > 0) { if (newKbs > 0) {
if (this.globalThrottle) {
this.globalThrottle.updateRate(newKbs * 1024); this.globalThrottle.updateRate(newKbs * 1024);
} else {
this.globalThrottle = new Throttle(newKbs * 1024);
}
} else { } else {
this.globalThrottle = null; this.globalThrottle = null;
} }
// Update global semaphore live
const globalLimit = this._getGlobalParallelLimit();
if (globalLimit > 0 && this.globalSemaphore) {
this.globalSemaphore.updateLimit(globalLimit);
} }
} }
@ -262,7 +269,7 @@ class UploadManager extends EventEmitter {
attempt, attempt,
maxAttempts maxAttempts
}); });
await this._sleep(2500, signal); await this._sleep(3000, signal);
} }
const jobStart = Date.now(); const jobStart = Date.now();

View File

@ -577,6 +577,7 @@ ipcMain.handle('start-upload', (_event, payload) => {
// This ensures webContents.send() calls from upload events // This ensures webContents.send() calls from upload events
// are not interleaved with the handle() response. // are not interleaved with the handle() response.
process.nextTick(() => { process.nextTick(() => {
if (!uploadManager) { debugLog('nextTick: uploadManager was nulled before startBatch'); return; }
debugLog('nextTick: calling startBatch now'); debugLog('nextTick: calling startBatch now');
uploadManager.startBatch(tasks).catch((err) => { uploadManager.startBatch(tasks).catch((err) => {
debugLog(`startBatch REJECTED: ${err && err.stack ? err.stack : err}`); debugLog(`startBatch REJECTED: ${err && err.stack ? err.stack : err}`);

View File

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

View File

@ -691,6 +691,7 @@ function sortQueueJobs(jobs) {
else if (key === 'host') cmp = a.hoster.localeCompare(b.hoster); else if (key === 'host') cmp = a.hoster.localeCompare(b.hoster);
else if (key === 'status') cmp = getStatusOrder(a.status) - getStatusOrder(b.status); else if (key === 'status') cmp = getStatusOrder(a.status) - getStatusOrder(b.status);
else if (key === 'speed') cmp = (a.speedKbs || 0) - (b.speedKbs || 0); else if (key === 'speed') cmp = (a.speedKbs || 0) - (b.speedKbs || 0);
else if (key === 'progress') cmp = (a.progress || 0) - (b.progress || 0);
return cmp * factor; return cmp * factor;
}); });
} }
@ -1951,12 +1952,11 @@ async function loadHistory() {
for (const file of (batch.files || [])) { for (const file of (batch.files || [])) {
for (const result of (file.results || [])) { for (const result of (file.results || [])) {
if (result.status === 'aborted' || result.status === 'error') continue; if (result.status === 'aborted' || result.status === 'error') continue;
const isError = result.status === 'error';
historyRowsData.push({ historyRowsData.push({
date: dt.text, dateTs: dt.ts, date: dt.text, dateTs: dt.ts,
filename: file.name || '', host: result.hoster || '', filename: file.name || '', host: result.hoster || '',
link: isError ? `[Fehler] ${result.error || ''}` : (result.download_url || result.embed_url || ''), link: result.download_url || result.embed_url || '',
isError, order: order++ isError: false, order: order++
}); });
} }
} }
@ -2135,6 +2135,7 @@ function setupListeners() {
const tr = e.target.closest('.recent-file-row'); const tr = e.target.closest('.recent-file-row');
if (!tr) return; if (!tr) return;
e.preventDefault(); e.preventDefault();
e.stopPropagation();
const id = parseInt(tr.dataset.order, 10); const id = parseInt(tr.dataset.order, 10);
if (!selectedRecentIds.has(id)) { if (!selectedRecentIds.has(id)) {
selectedRecentIds.clear(); selectedRecentIds.clear();

View File

@ -84,7 +84,7 @@
<th class="col-elapsed">Zeit</th> <th class="col-elapsed">Zeit</th>
<th class="col-remaining">Rest</th> <th class="col-remaining">Rest</th>
<th class="col-speed sortable" data-sort="speed">Speed</th> <th class="col-speed sortable" data-sort="speed">Speed</th>
<th class="col-progress">Fortschritt</th> <th class="col-progress sortable" data-sort="progress">Fortschritt</th>
</tr> </tr>
</thead> </thead>
<tbody id="queueBody"></tbody> <tbody id="queueBody"></tbody>