feat: import upload log to remove already-uploaded jobs from queue

New 'Log importieren' button in queue actions. Opens file picker for
.log/.txt files, parses the fileuploader.log format:
  date|hoster|link||filename|

Matches each log entry against queue jobs by filename+hoster (case-
insensitive). Removes matching jobs that are already uploaded,
shows toast with count.

Use case: after a crash/restart, import the log from a previous
session to skip files that were already successfully uploaded.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Administrator 2026-03-24 09:57:09 +01:00
parent ad9299e74c
commit e07db0532a
4 changed files with 72 additions and 0 deletions

26
main.js
View File

@ -845,6 +845,32 @@ ipcMain.handle('import-backup', async (_event, password) => {
return { ok: true, config: configStore.load() }; return { ok: true, config: configStore.load() };
}); });
ipcMain.handle('import-upload-log', async () => {
const { canceled, filePaths } = await dialog.showOpenDialog(mainWindow, {
title: 'Upload-Log importieren',
filters: [
{ name: 'Log-Dateien', extensions: ['log', 'txt'] },
{ name: 'Alle Dateien', extensions: ['*'] }
],
properties: ['openFile']
});
if (canceled || !filePaths.length) return { canceled: true };
const content = fs.readFileSync(filePaths[0], 'utf-8');
// Parse log format: date|hoster|link||filename|
const entries = [];
for (const line of content.split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const parts = trimmed.split('|');
if (parts.length >= 5) {
const hoster = (parts[1] || '').trim();
const fileName = (parts[4] || '').trim();
if (hoster && fileName) entries.push({ hoster, fileName });
}
}
return { entries, path: filePaths[0] };
});
ipcMain.handle('copy-to-clipboard', (_event, text) => { ipcMain.handle('copy-to-clipboard', (_event, text) => {
clipboard.writeText(text); clipboard.writeText(text);
return true; return true;

View File

@ -38,6 +38,9 @@ contextBridge.exposeInMainWorld('api', {
finishAfterActive: () => ipcRenderer.invoke('finish-after-active'), finishAfterActive: () => ipcRenderer.invoke('finish-after-active'),
runHealthCheck: (payload) => ipcRenderer.invoke('run-health-check', payload), runHealthCheck: (payload) => ipcRenderer.invoke('run-health-check', payload),
// Log import
importUploadLog: () => ipcRenderer.invoke('import-upload-log'),
// Clipboard // Clipboard
copyToClipboard: (text) => ipcRenderer.invoke('copy-to-clipboard', text), copyToClipboard: (text) => ipcRenderer.invoke('copy-to-clipboard', text),

View File

@ -3098,6 +3098,7 @@ function setupListeners() {
queueJobs.forEach(j => { if (j.status === 'error') selectedJobIds.add(j.id); }); queueJobs.forEach(j => { if (j.status === 'error') selectedJobIds.add(j.id); });
retrySelectedJobs(); retrySelectedJobs();
}); });
document.getElementById('importLogBtn').addEventListener('click', importUploadLog);
document.getElementById('confirmHosterModalBtn').addEventListener('click', applyHosterSelection); document.getElementById('confirmHosterModalBtn').addEventListener('click', applyHosterSelection);
document.getElementById('cancelHosterModalBtn').addEventListener('click', cancelHosterModal); document.getElementById('cancelHosterModalBtn').addEventListener('click', cancelHosterModal);
document.getElementById('closeHosterModalBtn').addEventListener('click', cancelHosterModal); document.getElementById('closeHosterModalBtn').addEventListener('click', cancelHosterModal);
@ -3280,6 +3281,47 @@ function handleShutdownCountdown(data) {
}, 1000); }, 1000);
} }
// --- Log import: remove already-uploaded file+hoster combos from queue ---
async function importUploadLog() {
const result = await window.api.importUploadLog();
if (!result || result.canceled) return;
const entries = result.entries || [];
if (entries.length === 0) {
showCopyToast('Keine Einträge im Log gefunden');
return;
}
// Build lookup Set: "filename_lower|hoster"
const logKeys = new Set();
for (const entry of entries) {
logKeys.add(`${entry.fileName.toLowerCase()}|${entry.hoster.toLowerCase()}`);
}
// Find queue jobs that match (already uploaded)
let removed = 0;
queueJobs = queueJobs.filter(job => {
const key = `${job.fileName.toLowerCase()}|${job.hoster.toLowerCase()}`;
if (logKeys.has(key) && job.status !== 'done') {
removeJobFromIndex(job);
removed++;
return false;
}
return true;
});
if (removed > 0) {
selectedJobIds.clear();
syncSelectedFilesFromQueue();
rebuildJobIndex();
renderQueueTable();
updateUploadView();
updateStatusBar();
persistQueueStateSoon(true);
}
showCopyToast(`${removed} bereits hochgeladene Jobs aus Queue entfernt (${entries.length} Log-Einträge gelesen)`);
}
// --- Link operations --- // --- Link operations ---
function copyAllLinks() { function copyAllLinks() {
const links = queueJobs const links = queueJobs

View File

@ -94,6 +94,7 @@
<div class="queue-actions" id="queueActions" style="display:none"> <div class="queue-actions" id="queueActions" style="display:none">
<button class="btn btn-xs btn-primary" id="copyAllLinksBtn">Alle Links kopieren</button> <button class="btn btn-xs btn-primary" id="copyAllLinksBtn">Alle Links kopieren</button>
<button class="btn btn-xs btn-secondary" id="retryFailedBtn" style="display:none">Fehlgeschlagene erneut</button> <button class="btn btn-xs btn-secondary" id="retryFailedBtn" style="display:none">Fehlgeschlagene erneut</button>
<button class="btn btn-xs btn-secondary" id="importLogBtn" title="Log importieren — bereits hochgeladene aus Queue entfernen">Log importieren</button>
</div> </div>
<div class="resize-handle" id="recentFilesResizer"></div> <div class="resize-handle" id="recentFilesResizer"></div>