Compare commits
No commits in common. "6a32387addd183b2c588280e879079e4f73cf2a5" and "b7499c87a313f7452df3372a30e04d0b8db0198a" have entirely different histories.
6a32387add
...
b7499c87a3
4
package-lock.json
generated
4
package-lock.json
generated
@ -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",
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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:`);
|
||||||
|
|||||||
53
src/main.ts
53
src/main.ts
@ -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 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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 = [];
|
||||||
|
|||||||
@ -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();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user