Compare commits

..

No commits in common. "6a32387addd183b2c588280e879079e4f73cf2a5" and "b7499c87a313f7452df3372a30e04d0b8db0198a" have entirely different histories.

6 changed files with 18 additions and 89 deletions

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "twitch-vod-manager", "name": "twitch-vod-manager",
"version": "4.4.0", "version": "4.3.4",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "twitch-vod-manager", "name": "twitch-vod-manager",
"version": "4.4.0", "version": "4.3.4",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"axios": "^1.6.0", "axios": "^1.6.0",

View File

@ -1,6 +1,6 @@
{ {
"name": "twitch-vod-manager", "name": "twitch-vod-manager",
"version": "4.4.0", "version": "4.3.4",
"description": "Twitch VOD Manager - Download Twitch VODs easily", "description": "Twitch VOD Manager - Download Twitch VODs easily",
"main": "dist/main.js", "main": "dist/main.js",
"author": "xRangerDE", "author": "xRangerDE",

View File

@ -105,21 +105,6 @@ function run() {
const iIndex = args.indexOf('-i'); const iIndex = args.indexOf('-i');
assert(ssIndex < iIndex, `FFmpeg args: -ss (${ssIndex}) must be before -i (${iIndex})`); assert(ssIndex < iIndex, `FFmpeg args: -ss (${ssIndex}) must be before -i (${iIndex})`);
// ---- Test 9: ensureUniqueFilename pattern ----
function ensureUnique(base, ext, existingFiles) {
let candidate = base + ext;
if (!existingFiles.includes(candidate)) return candidate;
let counter = 1;
while (existingFiles.includes(candidate)) {
candidate = `${base}_${counter}${ext}`;
counter++;
}
return candidate;
}
assert(ensureUnique('video', '.mp4', []) === 'video.mp4', 'Unique: no conflict');
assert(ensureUnique('video', '.mp4', ['video.mp4']) === 'video_1.mp4', 'Unique: one conflict');
assert(ensureUnique('video', '.mp4', ['video.mp4', 'video_1.mp4']) === 'video_2.mp4', 'Unique: two conflicts');
// ---- Results ---- // ---- Results ----
if (failures.length > 0) { if (failures.length > 0) {
console.error(`FAIL: ${failures.length} test(s) failed:`); console.error(`FAIL: ${failures.length} test(s) failed:`);

View File

@ -1,4 +1,4 @@
import { app, BrowserWindow, ipcMain, dialog, shell, nativeTheme, Notification } from 'electron'; import { app, BrowserWindow, ipcMain, dialog, shell, nativeTheme } from 'electron';
import * as path from 'path'; import * as path from 'path';
import * as fs from 'fs'; import * as fs from 'fs';
import { spawn, ChildProcess, execSync, exec, spawnSync } from 'child_process'; import { spawn, ChildProcess, execSync, exec, spawnSync } from 'child_process';
@ -340,17 +340,10 @@ function loadQueue(): QueueItem[] {
try { try {
if (fs.existsSync(QUEUE_FILE)) { if (fs.existsSync(QUEUE_FILE)) {
const data = fs.readFileSync(QUEUE_FILE, 'utf-8'); const data = fs.readFileSync(QUEUE_FILE, 'utf-8');
const parsed = JSON.parse(data); return 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) { } catch (e) {
console.error('Error loading queue:', e); console.error('Error loading queue:', e);
console.error('queue-load-error', { error: String(e) });
} }
return []; return [];
} }
@ -1175,20 +1168,6 @@ function formatDurationDashed(seconds: number): string {
return `${h.toString().padStart(2, '0')}-${m.toString().padStart(2, '0')}-${s.toString().padStart(2, '0')}`; 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 { function sanitizeFilenamePart(input: string, fallback = 'unnamed'): string {
const cleaned = (input || '') const cleaned = (input || '')
.replace(/[<>:"|?*\x00-\x1f]/g, '_') .replace(/[<>:"|?*\x00-\x1f]/g, '_')
@ -2586,7 +2565,7 @@ async function splitMergedFile(
const startSec = i * partDurationSec; const startSec = i * partDurationSec;
const thisDuration = Math.min(partDurationSec, totalDurationSec - startSec); const thisDuration = Math.min(partDurationSec, totalDurationSec - startSec);
const outputFile = ensureUniqueFilename(path.join(outputFolder, filenameGenerator(i + 1))); const outputFile = path.join(outputFolder, filenameGenerator(i + 1));
onProgress(i + 1, numParts); onProgress(i + 1, numParts);
@ -2911,7 +2890,7 @@ async function downloadVOD(
const remainingDuration = clip.durationSec - (i * partDuration); const remainingDuration = clip.durationSec - (i * partDuration);
const thisDuration = Math.min(partDuration, remainingDuration); const thisDuration = Math.min(partDuration, remainingDuration);
const partFilename = ensureUniqueFilename(makeClipFilename(partNum, startOffset, thisDuration)); const partFilename = makeClipFilename(partNum, startOffset, thisDuration);
const result = await downloadVODPart( const result = await downloadVODPart(
item.url, item.url,
@ -2934,7 +2913,7 @@ async function downloadVOD(
}; };
} else { } else {
// Single clip file // Single clip file
const filename = ensureUniqueFilename(makeClipFilename(clip.startPart, clip.startSec, clip.durationSec)); const filename = makeClipFilename(clip.startPart, clip.startSec, clip.durationSec);
return await downloadVODPart( return await downloadVODPart(
item.url, item.url,
filename, filename,
@ -2951,13 +2930,13 @@ async function downloadVOD(
// Check download mode // Check download mode
if (config.download_mode === 'full' || totalDuration <= config.part_minutes * 60) { if (config.download_mode === 'full' || totalDuration <= config.part_minutes * 60) {
// Full download // Full download
const filename = ensureUniqueFilename(makeTemplateFilename( const filename = makeTemplateFilename(
config.filename_template_vod, config.filename_template_vod,
DEFAULT_FILENAME_TEMPLATE_VOD, DEFAULT_FILENAME_TEMPLATE_VOD,
1, 1,
0, 0,
totalDuration totalDuration
)); );
return await downloadVODPart(item.url, filename, null, null, onProgress, item.id, 1, 1); return await downloadVODPart(item.url, filename, null, null, onProgress, item.id, 1, 1);
} else { } else {
// Part-based download // Part-based download
@ -2972,13 +2951,13 @@ async function downloadVOD(
const endSec = Math.min((i + 1) * partDuration, totalDuration); const endSec = Math.min((i + 1) * partDuration, totalDuration);
const duration = endSec - startSec; const duration = endSec - startSec;
const partFilename = ensureUniqueFilename(makeTemplateFilename( const partFilename = makeTemplateFilename(
config.filename_template_parts, config.filename_template_parts,
DEFAULT_FILENAME_TEMPLATE_PARTS, DEFAULT_FILENAME_TEMPLATE_PARTS,
i + 1, i + 1,
startSec, startSec,
duration duration
)); );
const result = await downloadVODPart( const result = await downloadVODPart(
item.url, item.url,
@ -3062,7 +3041,7 @@ async function processDownloadMergeGroup(
saveQueue(downloadQueue); saveQueue(downloadQueue);
const vodItem = mg.items[i]; const vodItem = mg.items[i];
const tmpFilename = ensureUniqueFilename(path.join(folder, `merge_tmp_${i}_${Date.now()}.mp4`)); const tmpFilename = path.join(folder, `merge_tmp_${i}_${Date.now()}.mp4`);
// Calculate progress weighting per VOD // Calculate progress weighting per VOD
const vodDuration = parseDuration(vodItem.duration_str); const vodDuration = parseDuration(vodItem.duration_str);
@ -3385,18 +3364,6 @@ async function processQueue(): Promise<void> {
saveQueue(downloadQueue); saveQueue(downloadQueue);
emitQueueUpdated(); emitQueueUpdated();
mainWindow?.webContents.send('download-finished'); 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 }); appendDebugLog('queue-finished', { items: downloadQueue.length });
} }

View File

@ -179,25 +179,6 @@ async function createMergeGroupFromSelection(): Promise<void> {
updateMergeGroupButton(); updateMergeGroupButton();
} }
function updateQueueItemProgress(progress: DownloadProgress): void {
const items = byId('queueList').children;
const idx = queue.findIndex(i => i.id === progress.id);
if (idx < 0 || idx >= items.length) return;
const el = items[idx];
const bar = el.querySelector('.queue-progress-bar') as HTMLElement;
const text = el.querySelector('.queue-progress-text') as HTMLElement;
const meta = el.querySelector('.queue-meta') as HTMLElement;
if (bar) {
const pct = progress.progress > 0 ? Math.min(100, progress.progress) : 0;
bar.style.width = `${pct}%`;
bar.className = `queue-progress-bar${progress.progress <= 0 ? ' indeterminate' : ''}`;
}
if (text) text.textContent = getQueueProgressText(queue[idx]);
if (meta) meta.textContent = getQueueMetaText(queue[idx]);
}
function renderQueue(): void { function renderQueue(): void {
if (!Array.isArray(queue)) { if (!Array.isArray(queue)) {
queue = []; queue = [];

View File

@ -5,18 +5,14 @@ const QUEUE_SYNC_HIDDEN_MS = 9000;
const QUEUE_SYNC_RECENT_ACTIVITY_WINDOW_MS = 15000; const QUEUE_SYNC_RECENT_ACTIVITY_WINDOW_MS = 15000;
async function init(): Promise<void> { async function init(): Promise<void> {
const [loadedConfig, initialQueue, isDown, version] = await Promise.all([ config = await window.api.getConfig();
window.api.getConfig(),
window.api.getQueue(),
window.api.isDownloading(),
window.api.getVersion()
]);
config = loadedConfig;
const language = setLanguage((config.language as string) || 'en'); const language = setLanguage((config.language as string) || 'en');
config.language = language; config.language = language;
const initialQueue = await window.api.getQueue();
queue = Array.isArray(initialQueue) ? initialQueue : []; queue = Array.isArray(initialQueue) ? initialQueue : [];
downloading = isDown; downloading = await window.api.isDownloading();
markQueueActivity(); markQueueActivity();
const version = await window.api.getVersion();
byId('versionText').textContent = `v${version}`; byId('versionText').textContent = `v${version}`;
byId('versionInfo').textContent = `Version: v${version}`; byId('versionInfo').textContent = `Version: v${version}`;
@ -70,7 +66,7 @@ async function init(): Promise<void> {
item.downloadedBytes = progress.downloadedBytes; item.downloadedBytes = progress.downloadedBytes;
item.totalBytes = progress.totalBytes; item.totalBytes = progress.totalBytes;
item.progressStatus = progress.status; item.progressStatus = progress.status;
updateQueueItemProgress(progress); renderQueue();
markQueueActivity(); markQueueActivity();
}); });