feat: filename collision detection, queue JSON validation, download-complete notification
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
76ecbc652d
commit
a4ca410641
53
src/main.ts
53
src/main.ts
@ -1,4 +1,4 @@
|
||||
import { app, BrowserWindow, ipcMain, dialog, shell, nativeTheme } from 'electron';
|
||||
import { app, BrowserWindow, ipcMain, dialog, shell, nativeTheme, Notification } from 'electron';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { spawn, ChildProcess, execSync, exec, spawnSync } from 'child_process';
|
||||
@ -340,10 +340,17 @@ function loadQueue(): QueueItem[] {
|
||||
try {
|
||||
if (fs.existsSync(QUEUE_FILE)) {
|
||||
const data = fs.readFileSync(QUEUE_FILE, 'utf-8');
|
||||
return JSON.parse(data);
|
||||
const parsed = JSON.parse(data);
|
||||
if (!Array.isArray(parsed)) return [];
|
||||
return parsed.filter((item: any) =>
|
||||
item && typeof item.id === 'string' &&
|
||||
typeof item.url === 'string' &&
|
||||
typeof item.status === 'string'
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error loading queue:', e);
|
||||
console.error('queue-load-error', { error: String(e) });
|
||||
}
|
||||
return [];
|
||||
}
|
||||
@ -1168,6 +1175,20 @@ function formatDurationDashed(seconds: number): string {
|
||||
return `${h.toString().padStart(2, '0')}-${m.toString().padStart(2, '0')}-${s.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
function ensureUniqueFilename(filePath: string): string {
|
||||
if (!fs.existsSync(filePath)) return filePath;
|
||||
const dir = path.dirname(filePath);
|
||||
const ext = path.extname(filePath);
|
||||
const base = path.basename(filePath, ext);
|
||||
let counter = 1;
|
||||
let candidate = filePath;
|
||||
while (fs.existsSync(candidate)) {
|
||||
candidate = path.join(dir, `${base}_${counter}${ext}`);
|
||||
counter++;
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
function sanitizeFilenamePart(input: string, fallback = 'unnamed'): string {
|
||||
const cleaned = (input || '')
|
||||
.replace(/[<>:"|?*\x00-\x1f]/g, '_')
|
||||
@ -2565,7 +2586,7 @@ async function splitMergedFile(
|
||||
|
||||
const startSec = i * partDurationSec;
|
||||
const thisDuration = Math.min(partDurationSec, totalDurationSec - startSec);
|
||||
const outputFile = path.join(outputFolder, filenameGenerator(i + 1));
|
||||
const outputFile = ensureUniqueFilename(path.join(outputFolder, filenameGenerator(i + 1)));
|
||||
|
||||
onProgress(i + 1, numParts);
|
||||
|
||||
@ -2890,7 +2911,7 @@ async function downloadVOD(
|
||||
const remainingDuration = clip.durationSec - (i * partDuration);
|
||||
const thisDuration = Math.min(partDuration, remainingDuration);
|
||||
|
||||
const partFilename = makeClipFilename(partNum, startOffset, thisDuration);
|
||||
const partFilename = ensureUniqueFilename(makeClipFilename(partNum, startOffset, thisDuration));
|
||||
|
||||
const result = await downloadVODPart(
|
||||
item.url,
|
||||
@ -2913,7 +2934,7 @@ async function downloadVOD(
|
||||
};
|
||||
} else {
|
||||
// Single clip file
|
||||
const filename = makeClipFilename(clip.startPart, clip.startSec, clip.durationSec);
|
||||
const filename = ensureUniqueFilename(makeClipFilename(clip.startPart, clip.startSec, clip.durationSec));
|
||||
return await downloadVODPart(
|
||||
item.url,
|
||||
filename,
|
||||
@ -2930,13 +2951,13 @@ async function downloadVOD(
|
||||
// Check download mode
|
||||
if (config.download_mode === 'full' || totalDuration <= config.part_minutes * 60) {
|
||||
// Full download
|
||||
const filename = makeTemplateFilename(
|
||||
const filename = ensureUniqueFilename(makeTemplateFilename(
|
||||
config.filename_template_vod,
|
||||
DEFAULT_FILENAME_TEMPLATE_VOD,
|
||||
1,
|
||||
0,
|
||||
totalDuration
|
||||
);
|
||||
));
|
||||
return await downloadVODPart(item.url, filename, null, null, onProgress, item.id, 1, 1);
|
||||
} else {
|
||||
// Part-based download
|
||||
@ -2951,13 +2972,13 @@ async function downloadVOD(
|
||||
const endSec = Math.min((i + 1) * partDuration, totalDuration);
|
||||
const duration = endSec - startSec;
|
||||
|
||||
const partFilename = makeTemplateFilename(
|
||||
const partFilename = ensureUniqueFilename(makeTemplateFilename(
|
||||
config.filename_template_parts,
|
||||
DEFAULT_FILENAME_TEMPLATE_PARTS,
|
||||
i + 1,
|
||||
startSec,
|
||||
duration
|
||||
);
|
||||
));
|
||||
|
||||
const result = await downloadVODPart(
|
||||
item.url,
|
||||
@ -3041,7 +3062,7 @@ async function processDownloadMergeGroup(
|
||||
saveQueue(downloadQueue);
|
||||
|
||||
const vodItem = mg.items[i];
|
||||
const tmpFilename = path.join(folder, `merge_tmp_${i}_${Date.now()}.mp4`);
|
||||
const tmpFilename = ensureUniqueFilename(path.join(folder, `merge_tmp_${i}_${Date.now()}.mp4`));
|
||||
|
||||
// Calculate progress weighting per VOD
|
||||
const vodDuration = parseDuration(vodItem.duration_str);
|
||||
@ -3364,6 +3385,18 @@ async function processQueue(): Promise<void> {
|
||||
saveQueue(downloadQueue);
|
||||
emitQueueUpdated();
|
||||
mainWindow?.webContents.send('download-finished');
|
||||
try {
|
||||
if (Notification.isSupported()) {
|
||||
const completed = downloadQueue.filter(i => i.status === 'completed').length;
|
||||
const failed = downloadQueue.filter(i => i.status === 'error').length;
|
||||
new Notification({
|
||||
title: 'Twitch VOD Manager',
|
||||
body: failed > 0
|
||||
? `${completed} Downloads fertig, ${failed} fehlgeschlagen`
|
||||
: `${completed} Downloads abgeschlossen`
|
||||
}).show();
|
||||
}
|
||||
} catch { }
|
||||
appendDebugLog('queue-finished', { items: downloadQueue.length });
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user