diff --git a/lib/upload-log.js b/lib/upload-log.js index 0f04630..6025791 100644 --- a/lib/upload-log.js +++ b/lib/upload-log.js @@ -17,7 +17,10 @@ const parts = trimmed.split('|'); if (parts.length < 5) return null; const hoster = (parts[1] || '').trim(); - const fileName = (parts[4] || '').trim(); + let fileName = ''; + for (let i = parts.length - 1; i >= 4; i--) { + if (parts[i].trim() !== '') { fileName = parts[i]; break; } + } if (!hoster || !fileName) return null; const tsStr = (parts[0] || '').trim(); const tsParsed = tsStr ? Date.parse(tsStr.replace(' ', 'T')) : NaN; diff --git a/renderer/app.js b/renderer/app.js index d0b5572..74ad8fa 100644 --- a/renderer/app.js +++ b/renderer/app.js @@ -671,6 +671,12 @@ function applyHosterSelection() { selectedFiles.push(..._pendingFiles); _pendingFiles = []; } + if (pendingPaths.size > 0) { + for (const key of [..._completedUploadKeys]) { + const sep = key.lastIndexOf('|'); + if (sep > 0 && pendingPaths.has(key.slice(0, sep))) _completedUploadKeys.delete(key); + } + } renderHosterSummary(); // During an active upload, build preview jobs for the new files and inject @@ -713,6 +719,12 @@ function restoreQueueStateFromConfig() { ? pending.savedAt : null; + if (Array.isArray(pending.completedKeys)) { + for (const k of pending.completedKeys) { + if (typeof k === 'string' && k) _completedUploadKeys.add(k); + } + } + selectedUploadHosters = Array.isArray(pending.selectedUploadHosters) ? pending.selectedUploadHosters.filter(Boolean) : selectedUploadHosters; @@ -785,10 +797,16 @@ function buildPersistedQueueState() { // consistent "Bereit" for everything that didn't actually terminate. // Only true terminal states (done / error / skipped) survive as-is. const TERMINAL = new Set(['done', 'error', 'skipped']); + const completedKeys = []; + for (const k of _completedUploadKeys) { + const sep = k.lastIndexOf('|'); + if (sep > 0 && selectedFileMap.has(k.slice(0, sep))) completedKeys.push(k); + } return { savedAt: Date.now(), selectedUploadHosters: getSelectedHosters(), selectedFiles: Array.from(selectedFileMap.values()), + completedKeys, queueJobs: queueJobs.map(job => { const isTerminal = TERMINAL.has(job.status); return { @@ -1060,7 +1078,7 @@ function buildQueuePreview() { // Build a Set for fast existence checks const existingKeys = new Set(); for (const j of queueJobs) { - if (j.status !== 'error') existingKeys.add(`${j.file}|${j.hoster}`); + existingKeys.add(`${j.file}|${j.hoster}`); } for (const file of selectedFiles) { @@ -1099,14 +1117,14 @@ function indexJob(job) { if (job.uploadId) _jobIndexByUploadId.set(job.uploadId, job); } -function removeJobFromIndex(job) { +function removeJobFromIndex(job, keepCompletedKey) { _jobIndexById.delete(job.id); if (job.uploadId) _jobIndexByUploadId.delete(job.uploadId); // Track deletion so handleProgress() won't re-create this job from stale callbacks _deletedJobIds.add(job.id); if (job.uploadId) _deletedJobIds.add(job.uploadId); // Allow re-uploading same file+hoster after deletion - if (job.file && job.hoster) _completedUploadKeys.delete(`${job.file}|${job.hoster}`); + if (!keepCompletedKey && job.file && job.hoster) _completedUploadKeys.delete(`${job.file}|${job.hoster}`); } // --- Queue Table Rendering (debounced with virtual scrolling) --- @@ -2249,7 +2267,7 @@ function _handleProgressImpl(data) { // updated synchronously so subsequent lookups see the right state — only // the array rewrite is deferred. if (job.status === 'done' && config.globalSettings && config.globalSettings.removeFromQueueOnDone) { - removeJobFromIndex(job); + removeJobFromIndex(job, true); selectedJobIds.delete(job.id); if (_doneRemovalCoalescer) { _doneRemovalCoalescer.add(job.id); @@ -2315,7 +2333,7 @@ function handleBatchDone(summary) { const nextJobs = []; for (const job of queueJobs) { if (job.status === 'done') { - removeJobFromIndex(job); + removeJobFromIndex(job, true); selectedJobIds.delete(job.id); } else { nextJobs.push(job); @@ -2337,7 +2355,7 @@ function handleBatchDone(summary) { const result = window.QueuePrune?.pruneOldestTerminalJobs(queueJobs, TERMINAL_KEEP_LIMIT); if (result) { for (const j of result.dropped) { - removeJobFromIndex(j); + removeJobFromIndex(j, true); selectedJobIds.delete(j.id); } queueJobs = result.kept; diff --git a/tests/upload-log.test.js b/tests/upload-log.test.js index ce21225..45da127 100644 --- a/tests/upload-log.test.js +++ b/tests/upload-log.test.js @@ -50,3 +50,31 @@ test('parseUploadLogLine: missing/garbage timestamp yields ts=undefined (legacy assert.equal(parsed.fileName, 'a.mkv'); assert.equal(parsed.ts, undefined); }); + +test('parseUploadLogLine: a pipe in the link does NOT shift the filename field (entry not lost)', () => { + const line = formatUploadLogLine(new Date(2026, 5, 19, 12, 0, 0), 'byse.sx', 'https://h.io/a|b', 'movie.mkv'); + const parsed = parseUploadLogLine(line); + assert.equal(parsed.hoster, 'byse.sx'); + assert.equal(parsed.fileName, 'movie.mkv', 'filename is taken as the last non-empty field, robust to link pipes'); +}); + +test('parseUploadLogLine: two pipes in the link still parse the correct filename', () => { + const line = formatUploadLogLine(new Date(2026, 5, 19, 12, 0, 0), 'byse.sx', 'https://h.io/a|b|c', 'movie.mkv'); + const parsed = parseUploadLogLine(line); + assert.equal(parsed.fileName, 'movie.mkv'); +}); + +test('parseUploadLogLine: a leading-space filename is preserved (matches the untrimmed queue-job key)', () => { + const line = formatUploadLogLine(new Date(2026, 5, 19, 12, 0, 0), 'voe.sx', 'https://h.io/a', ' movie.mkv'); + const parsed = parseUploadLogLine(line); + assert.equal(parsed.fileName, ' movie.mkv', 'filename is NOT trimmed, so it matches the OS basename verbatim'); +}); + +test('SEAM: a leading-space filename round-trips and the gate still drops its ghost', () => { + const completion = new Date(2026, 5, 19, 12, 0, 30); + const parsed = parseUploadLogLine(formatUploadLogLine(completion, 'voe.sx', 'l', ' spaced.mp4')); + const savedAt = completion.getTime() - 5000; + const job = { status: 'preview', fileName: ' spaced.mp4', hoster: 'voe.sx', file: 'C:/dl/ spaced.mp4' }; + const { removed } = partitionRestoredJobsByLog([job], [parsed], savedAt); + assert.equal(removed.length, 1, 'leading-space filename now matches end-to-end (was a mismatch before)'); +});