diff --git a/typescript-version/package-lock.json b/typescript-version/package-lock.json
index fd0ad1a..d4747fb 100644
--- a/typescript-version/package-lock.json
+++ b/typescript-version/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "twitch-vod-manager",
- "version": "3.9.0",
+ "version": "4.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "twitch-vod-manager",
- "version": "3.9.0",
+ "version": "4.0.0",
"license": "MIT",
"dependencies": {
"axios": "^1.6.0",
diff --git a/typescript-version/package.json b/typescript-version/package.json
index 2c794a2..89c5ac6 100644
--- a/typescript-version/package.json
+++ b/typescript-version/package.json
@@ -1,6 +1,6 @@
{
"name": "twitch-vod-manager",
- "version": "3.9.0",
+ "version": "4.0.0",
"description": "Twitch VOD Manager - Download Twitch VODs easily",
"main": "dist/main.js",
"author": "xRangerDE",
diff --git a/typescript-version/src/index.html b/typescript-version/src/index.html
index ba2109b..6d0d9f8 100644
--- a/typescript-version/src/index.html
+++ b/typescript-version/src/index.html
@@ -125,12 +125,13 @@
-
+
@@ -300,8 +301,8 @@
@@ -344,7 +345,7 @@
Updates
-
Version: v3.9.0
+
Version: v4.0.0
@@ -376,7 +377,7 @@
Nicht verbunden
- v3.9.0
+ v4.0.0
diff --git a/typescript-version/src/main.ts b/typescript-version/src/main.ts
index af33d90..fad7fda 100644
--- a/typescript-version/src/main.ts
+++ b/typescript-version/src/main.ts
@@ -8,7 +8,7 @@ import { autoUpdater } from 'electron-updater';
// ==========================================
// CONFIG & CONSTANTS
// ==========================================
-const APP_VERSION = '3.9.0';
+const APP_VERSION = '4.0.0';
const UPDATE_CHECK_URL = 'http://24-music.de/version.json';
// Paths
@@ -71,7 +71,7 @@ interface QueueItem {
date: string;
streamer: string;
duration_str: string;
- status: 'pending' | 'downloading' | 'completed' | 'error';
+ status: 'pending' | 'downloading' | 'paused' | 'completed' | 'error';
progress: number;
currentPart?: number;
totalParts?: number;
@@ -190,6 +190,7 @@ let downloadQueue: QueueItem[] = loadQueue();
let isDownloading = false;
let currentProcess: ChildProcess | null = null;
let currentDownloadCancelled = false;
+let pauseRequested = false;
let downloadStartTime = 0;
let downloadedBytes = 0;
const userIdLoginCache = new Map();
@@ -1440,12 +1441,13 @@ async function processQueue(): Promise {
appendDebugLog('queue-start', { items: downloadQueue.length });
isDownloading = true;
+ pauseRequested = false;
mainWindow?.webContents.send('download-started');
mainWindow?.webContents.send('queue-updated', downloadQueue);
for (const item of downloadQueue) {
- if (!isDownloading) break;
- if (item.status === 'completed') continue;
+ if (!isDownloading || pauseRequested) break;
+ if (item.status === 'completed' || item.status === 'error' || item.status === 'paused') continue;
appendDebugLog('queue-item-start', { itemId: item.id, title: item.title, url: item.url });
@@ -1472,8 +1474,8 @@ async function processQueue(): Promise {
finalResult = result;
- if (!isDownloading || currentDownloadCancelled) {
- finalResult = { success: false, error: 'Download wurde abgebrochen.' };
+ if (!isDownloading || currentDownloadCancelled || pauseRequested) {
+ finalResult = { success: false, error: pauseRequested ? 'Download wurde pausiert.' : 'Download wurde abgebrochen.' };
break;
}
@@ -1494,9 +1496,10 @@ async function processQueue(): Promise {
}
}
- item.status = finalResult.success ? 'completed' : 'error';
- item.progress = finalResult.success ? 100 : 0;
- item.last_error = finalResult.success ? '' : (finalResult.error || 'Unbekannter Fehler beim Download');
+ const wasPaused = pauseRequested || (finalResult.error || '').includes('pausiert');
+ item.status = finalResult.success ? 'completed' : (wasPaused ? 'paused' : 'error');
+ item.progress = finalResult.success ? 100 : item.progress;
+ item.last_error = finalResult.success || wasPaused ? '' : (finalResult.error || 'Unbekannter Fehler beim Download');
appendDebugLog('queue-item-finished', {
itemId: item.id,
status: item.status,
@@ -1507,6 +1510,7 @@ async function processQueue(): Promise {
}
isDownloading = false;
+ pauseRequested = false;
saveQueue(downloadQueue);
mainWindow?.webContents.send('queue-updated', downloadQueue);
mainWindow?.webContents.send('download-finished');
@@ -1659,6 +1663,20 @@ ipcMain.handle('clear-completed', () => {
return downloadQueue;
});
+ipcMain.handle('reorder-queue', (_, orderIds: string[]) => {
+ const order = new Map(orderIds.map((id, idx) => [id, idx]));
+ const withOrder = [...downloadQueue].sort((a, b) => {
+ const ai = order.has(a.id) ? (order.get(a.id) as number) : Number.MAX_SAFE_INTEGER;
+ const bi = order.has(b.id) ? (order.get(b.id) as number) : Number.MAX_SAFE_INTEGER;
+ return ai - bi;
+ });
+
+ downloadQueue = withOrder;
+ saveQueue(downloadQueue);
+ mainWindow?.webContents.send('queue-updated', downloadQueue);
+ return downloadQueue;
+});
+
ipcMain.handle('retry-failed-downloads', () => {
downloadQueue = downloadQueue.map((item) => {
if (item.status !== 'error') return item;
@@ -1682,18 +1700,35 @@ ipcMain.handle('retry-failed-downloads', () => {
});
ipcMain.handle('start-download', async () => {
- const hasPendingItems = downloadQueue.some(item => item.status !== 'completed');
+ downloadQueue = downloadQueue.map((item) => item.status === 'paused' ? { ...item, status: 'pending' } : item);
+
+ const hasPendingItems = downloadQueue.some(item => item.status === 'pending');
if (!hasPendingItems) {
mainWindow?.webContents.send('queue-updated', downloadQueue);
return false;
}
+ saveQueue(downloadQueue);
+ mainWindow?.webContents.send('queue-updated', downloadQueue);
+
processQueue();
return true;
});
+ipcMain.handle('pause-download', () => {
+ if (!isDownloading) return false;
+
+ pauseRequested = true;
+ currentDownloadCancelled = true;
+ if (currentProcess) {
+ currentProcess.kill();
+ }
+ return true;
+});
+
ipcMain.handle('cancel-download', () => {
isDownloading = false;
+ pauseRequested = false;
currentDownloadCancelled = true;
if (currentProcess) {
currentProcess.kill();
diff --git a/typescript-version/src/preload.ts b/typescript-version/src/preload.ts
index 0d803c6..a9c0c4b 100644
--- a/typescript-version/src/preload.ts
+++ b/typescript-version/src/preload.ts
@@ -15,7 +15,7 @@ interface QueueItem {
date: string;
streamer: string;
duration_str: string;
- status: 'pending' | 'downloading' | 'completed' | 'error';
+ status: 'pending' | 'downloading' | 'paused' | 'completed' | 'error';
progress: number;
currentPart?: number;
totalParts?: number;
@@ -60,11 +60,13 @@ contextBridge.exposeInMainWorld('api', {
getQueue: () => ipcRenderer.invoke('get-queue'),
addToQueue: (item: Omit) => ipcRenderer.invoke('add-to-queue', item),
removeFromQueue: (id: string) => ipcRenderer.invoke('remove-from-queue', id),
+ reorderQueue: (orderIds: string[]) => ipcRenderer.invoke('reorder-queue', orderIds),
clearCompleted: () => ipcRenderer.invoke('clear-completed'),
retryFailedDownloads: () => ipcRenderer.invoke('retry-failed-downloads'),
// Download
startDownload: () => ipcRenderer.invoke('start-download'),
+ pauseDownload: () => ipcRenderer.invoke('pause-download'),
cancelDownload: () => ipcRenderer.invoke('cancel-download'),
isDownloading: () => ipcRenderer.invoke('is-downloading'),
downloadClip: (url: string) => ipcRenderer.invoke('download-clip', url),
diff --git a/typescript-version/src/renderer-globals.d.ts b/typescript-version/src/renderer-globals.d.ts
index 7de094b..1873655 100644
--- a/typescript-version/src/renderer-globals.d.ts
+++ b/typescript-version/src/renderer-globals.d.ts
@@ -35,7 +35,7 @@ interface QueueItem {
date: string;
streamer: string;
duration_str: string;
- status: 'pending' | 'downloading' | 'completed' | 'error';
+ status: 'pending' | 'downloading' | 'paused' | 'completed' | 'error';
progress: number;
currentPart?: number;
totalParts?: number;
@@ -112,9 +112,11 @@ interface ApiBridge {
getQueue(): Promise;
addToQueue(item: Omit): Promise;
removeFromQueue(id: string): Promise;
+ reorderQueue(orderIds: string[]): Promise;
clearCompleted(): Promise;
retryFailedDownloads(): Promise;
startDownload(): Promise;
+ pauseDownload(): Promise;
cancelDownload(): Promise;
isDownloading(): Promise;
downloadClip(url: string): Promise<{ success: boolean; error?: string }>;
diff --git a/typescript-version/src/renderer-locale-de.ts b/typescript-version/src/renderer-locale-de.ts
index 114619b..5bbe3d3 100644
--- a/typescript-version/src/renderer-locale-de.ts
+++ b/typescript-version/src/renderer-locale-de.ts
@@ -7,7 +7,12 @@ const UI_TEXT_DE = {
navMerge: 'Videos zusammenfugen',
navSettings: 'Einstellungen',
queueTitle: 'Warteschlange',
- retryFailed: 'Fehler neu',
+ retryFailed: 'Wiederholen',
+ retryFailedHint: 'Nur fehlgeschlagene Downloads erneut starten',
+ healthUnknown: 'System: Unbekannt',
+ healthGood: 'System: Stabil',
+ healthWarn: 'System: Warnung',
+ healthBad: 'System: Problem',
clearQueue: 'Leeren',
refresh: 'Aktualisieren',
streamerPlaceholder: 'Streamer hinzufugen...',
@@ -22,8 +27,8 @@ const UI_TEXT_DE = {
designTitle: 'Design',
themeLabel: 'Theme',
languageLabel: 'Sprache',
- languageDe: 'DE - Deutsch',
- languageEn: 'EN - Englisch',
+ languageDe: 'π©πͺ Deutsch',
+ languageEn: 'πΊπΈ Englisch',
apiTitle: 'Twitch API',
clientIdLabel: 'Client ID',
clientSecretLabel: 'Client Secret',
@@ -41,6 +46,14 @@ const UI_TEXT_DE = {
preflightRun: 'Check ausfuhren',
preflightFix: 'Auto-Fix Tools',
preflightEmpty: 'Noch kein Check ausgefuhrt.',
+ preflightChecking: 'Prufe...',
+ preflightFixing: 'Fixe...',
+ preflightReady: 'Alles bereit.',
+ preflightInternet: 'Internet',
+ preflightStreamlink: 'Streamlink',
+ preflightFfmpeg: 'FFmpeg',
+ preflightFfprobe: 'FFprobe',
+ preflightPath: 'Download-Pfad',
debugLogTitle: 'Live Debug-Log',
refreshLog: 'Aktualisieren',
autoRefresh: 'Auto-Refresh',
@@ -62,10 +75,12 @@ const UI_TEXT_DE = {
queue: {
empty: 'Keine Downloads in der Warteschlange',
start: 'Start',
- stop: 'Stoppen',
+ stop: 'Pausieren',
+ resume: 'Fortsetzen',
statusDone: 'Abgeschlossen',
statusFailed: 'Fehlgeschlagen',
statusRunning: 'Laeuft',
+ statusPaused: 'Pausiert',
statusWaiting: 'Wartet',
progressError: 'Fehler',
progressReady: 'Bereit',
diff --git a/typescript-version/src/renderer-locale-en.ts b/typescript-version/src/renderer-locale-en.ts
index 79ef216..bd40d7a 100644
--- a/typescript-version/src/renderer-locale-en.ts
+++ b/typescript-version/src/renderer-locale-en.ts
@@ -7,7 +7,12 @@ const UI_TEXT_EN = {
navMerge: 'Merge Videos',
navSettings: 'Settings',
queueTitle: 'Queue',
- retryFailed: 'Retry failed',
+ retryFailed: 'Retry',
+ retryFailedHint: 'Retry failed downloads only',
+ healthUnknown: 'System: Unknown',
+ healthGood: 'System: Stable',
+ healthWarn: 'System: Warning',
+ healthBad: 'System: Problem',
clearQueue: 'Clear',
refresh: 'Refresh',
streamerPlaceholder: 'Add streamer...',
@@ -22,8 +27,8 @@ const UI_TEXT_EN = {
designTitle: 'Design',
themeLabel: 'Theme',
languageLabel: 'Language',
- languageDe: 'DE - German',
- languageEn: 'EN - English',
+ languageDe: 'π©πͺ German',
+ languageEn: 'πΊπΈ English',
apiTitle: 'Twitch API',
clientIdLabel: 'Client ID',
clientSecretLabel: 'Client Secret',
@@ -41,6 +46,14 @@ const UI_TEXT_EN = {
preflightRun: 'Run check',
preflightFix: 'Auto-fix tools',
preflightEmpty: 'No checks run yet.',
+ preflightChecking: 'Checking...',
+ preflightFixing: 'Fixing...',
+ preflightReady: 'Everything is ready.',
+ preflightInternet: 'Internet',
+ preflightStreamlink: 'Streamlink',
+ preflightFfmpeg: 'FFmpeg',
+ preflightFfprobe: 'FFprobe',
+ preflightPath: 'Download path',
debugLogTitle: 'Live Debug Log',
refreshLog: 'Refresh',
autoRefresh: 'Auto refresh',
@@ -62,10 +75,12 @@ const UI_TEXT_EN = {
queue: {
empty: 'No downloads in queue',
start: 'Start',
- stop: 'Stop',
+ stop: 'Pause',
+ resume: 'Resume',
statusDone: 'Completed',
statusFailed: 'Failed',
statusRunning: 'Running',
+ statusPaused: 'Paused',
statusWaiting: 'Waiting',
progressError: 'Error',
progressReady: 'Ready',
diff --git a/typescript-version/src/renderer-queue.ts b/typescript-version/src/renderer-queue.ts
index d17f45b..3e4a57c 100644
--- a/typescript-version/src/renderer-queue.ts
+++ b/typescript-version/src/renderer-queue.ts
@@ -27,6 +27,7 @@ async function retryFailedDownloads(): Promise {
function getQueueStatusLabel(item: QueueItem): string {
if (item.status === 'completed') return UI_TEXT.queue.statusDone;
if (item.status === 'error') return UI_TEXT.queue.statusFailed;
+ if (item.status === 'paused') return UI_TEXT.queue.statusPaused;
if (item.status === 'downloading') return UI_TEXT.queue.statusRunning;
return UI_TEXT.queue.statusWaiting;
}
@@ -34,6 +35,7 @@ function getQueueStatusLabel(item: QueueItem): string {
function getQueueProgressText(item: QueueItem): string {
if (item.status === 'completed') return '100%';
if (item.status === 'error') return UI_TEXT.queue.progressError;
+ if (item.status === 'paused') return UI_TEXT.queue.progressReady;
if (item.status === 'pending') return UI_TEXT.queue.progressReady;
if (item.progress > 0) return `${Math.max(0, Math.min(100, item.progress)).toFixed(1)}%`;
return item.progressStatus || UI_TEXT.queue.progressLoading;
@@ -62,6 +64,10 @@ function getQueueMetaText(item: QueueItem): string {
parts.push(UI_TEXT.queue.readyToDownload);
}
+ if (!parts.length && item.status === 'paused') {
+ parts.push(UI_TEXT.queue.statusPaused);
+ }
+
if (!parts.length && item.status === 'downloading') {
parts.push(item.progressStatus || UI_TEXT.queue.started);
}
@@ -84,6 +90,9 @@ function renderQueue(): void {
const list = byId('queueList');
byId('queueCount').textContent = String(queue.length);
+ const retryBtn = byId('btnRetryFailed');
+ const hasFailed = queue.some((item) => item.status === 'error');
+ retryBtn.disabled = !hasFailed;
if (queue.length === 0) {
list.innerHTML = `${UI_TEXT.queue.empty}
`;
@@ -124,7 +133,7 @@ function renderQueue(): void {
async function toggleDownload(): Promise {
if (downloading) {
- await window.api.cancelDownload();
+ await window.api.pauseDownload();
return;
}
diff --git a/typescript-version/src/renderer-settings.ts b/typescript-version/src/renderer-settings.ts
index cf5d2be..cb559ae 100644
--- a/typescript-version/src/renderer-settings.ts
+++ b/typescript-version/src/renderer-settings.ts
@@ -42,24 +42,42 @@ function changeLanguage(lang: string): void {
function renderPreflightResult(result: PreflightResult): void {
const entries = [
- ['Internet', result.checks.internet],
- ['Streamlink', result.checks.streamlink],
- ['FFmpeg', result.checks.ffmpeg],
- ['FFprobe', result.checks.ffprobe],
- ['Download-Pfad', result.checks.downloadPathWritable]
+ [UI_TEXT.static.preflightInternet, result.checks.internet],
+ [UI_TEXT.static.preflightStreamlink, result.checks.streamlink],
+ [UI_TEXT.static.preflightFfmpeg, result.checks.ffmpeg],
+ [UI_TEXT.static.preflightFfprobe, result.checks.ffprobe],
+ [UI_TEXT.static.preflightPath, result.checks.downloadPathWritable]
];
const lines = entries.map(([name, ok]) => `${ok ? 'OK' : 'FAIL'} ${name}`).join('\n');
- const extra = result.messages.length ? `\n\n${result.messages.join('\n')}` : '\n\nAlles bereit.';
+ const extra = result.messages.length ? `\n\n${result.messages.join('\n')}` : `\n\n${UI_TEXT.static.preflightReady}`;
byId('preflightResult').textContent = `${lines}${extra}`;
+
+ const badge = byId('healthBadge');
+ badge.classList.remove('good', 'warn', 'bad', 'unknown');
+
+ if (result.ok) {
+ badge.classList.add('good');
+ badge.textContent = UI_TEXT.static.healthGood;
+ return;
+ }
+
+ const failCount = Object.values(result.checks).filter((ok) => !ok).length;
+ if (failCount <= 2) {
+ badge.classList.add('warn');
+ badge.textContent = UI_TEXT.static.healthWarn;
+ } else {
+ badge.classList.add('bad');
+ badge.textContent = UI_TEXT.static.healthBad;
+ }
}
async function runPreflight(autoFix = false): Promise {
const btn = byId(autoFix ? 'btnPreflightFix' : 'btnPreflightRun');
const old = btn.textContent || '';
btn.disabled = true;
- btn.textContent = autoFix ? 'Fixe...' : 'Prufe...';
+ btn.textContent = autoFix ? UI_TEXT.static.preflightFixing : UI_TEXT.static.preflightChecking;
try {
const result = await window.api.runPreflight(autoFix);
diff --git a/typescript-version/src/renderer-shared.ts b/typescript-version/src/renderer-shared.ts
index 9493b5b..05535c0 100644
--- a/typescript-version/src/renderer-shared.ts
+++ b/typescript-version/src/renderer-shared.ts
@@ -39,3 +39,4 @@ let clipTotalSeconds = 0;
let updateReady = false;
let debugLogAutoRefreshTimer: number | null = null;
+let draggedQueueItemId: string | null = null;
diff --git a/typescript-version/src/renderer-texts.ts b/typescript-version/src/renderer-texts.ts
index e861b08..04348a0 100644
--- a/typescript-version/src/renderer-texts.ts
+++ b/typescript-version/src/renderer-texts.ts
@@ -31,6 +31,11 @@ function setPlaceholder(id: string, value: string): void {
if (node) node.placeholder = value;
}
+function setTitle(id: string, value: string): void {
+ const node = document.getElementById(id);
+ if (node) node.setAttribute('title', value);
+}
+
function setLanguage(lang: string): LanguageCode {
currentLanguage = lang === 'en' ? 'en' : 'de';
UI_TEXT = UI_TEXTS[currentLanguage];
@@ -46,7 +51,9 @@ function applyLanguageToStaticUI(): void {
setText('navMergeText', UI_TEXT.static.navMerge);
setText('navSettingsText', UI_TEXT.static.navSettings);
setText('queueTitleText', UI_TEXT.static.queueTitle);
+ setText('healthBadge', UI_TEXT.static.healthUnknown);
setText('btnRetryFailed', UI_TEXT.static.retryFailed);
+ setTitle('btnRetryFailed', UI_TEXT.static.retryFailedHint);
setText('btnClear', UI_TEXT.static.clearQueue);
setText('refreshText', UI_TEXT.static.refresh);
setText('clipsHeading', UI_TEXT.static.clipsHeading);
diff --git a/typescript-version/src/renderer.ts b/typescript-version/src/renderer.ts
index 24dc277..fe9733c 100644
--- a/typescript-version/src/renderer.ts
+++ b/typescript-version/src/renderer.ts
@@ -117,7 +117,8 @@ function mergeQueueState(nextQueue: QueueItem[]): QueueItem[] {
function updateDownloadButtonState(): void {
const btn = byId('btnStart');
- btn.textContent = downloading ? UI_TEXT.queue.stop : UI_TEXT.queue.start;
+ const hasPaused = queue.some((item) => item.status === 'paused');
+ btn.textContent = downloading ? UI_TEXT.queue.stop : (hasPaused ? UI_TEXT.queue.resume : UI_TEXT.queue.start);
btn.classList.toggle('downloading', downloading);
}
diff --git a/typescript-version/src/styles.css b/typescript-version/src/styles.css
index e96c375..e4c4aa3 100644
--- a/typescript-version/src/styles.css
+++ b/typescript-version/src/styles.css
@@ -179,6 +179,7 @@ body {
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
+ gap: 8px;
}
.queue-title {
@@ -199,6 +200,33 @@ body {
overflow-y: auto;
}
+.health-badge {
+ font-size: 10px;
+ padding: 2px 8px;
+ border-radius: 999px;
+ border: 1px solid transparent;
+ white-space: nowrap;
+}
+
+.health-badge.good {
+ background: rgba(0, 200, 83, 0.2);
+ border-color: rgba(0, 200, 83, 0.45);
+ color: #93efb9;
+}
+
+.health-badge.warn {
+ background: rgba(255, 171, 0, 0.2);
+ border-color: rgba(255, 171, 0, 0.45);
+ color: #ffd98e;
+}
+
+.health-badge.bad,
+.health-badge.unknown {
+ background: rgba(255, 68, 68, 0.2);
+ border-color: rgba(255, 68, 68, 0.45);
+ color: #ffaaaa;
+}
+
.queue-item {
display: flex;
align-items: flex-start;