diff --git a/typescript-version/package-lock.json b/typescript-version/package-lock.json
index 415d946..fd0ad1a 100644
--- a/typescript-version/package-lock.json
+++ b/typescript-version/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "twitch-vod-manager",
- "version": "3.8.8",
+ "version": "3.9.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "twitch-vod-manager",
- "version": "3.8.8",
+ "version": "3.9.0",
"license": "MIT",
"dependencies": {
"axios": "^1.6.0",
diff --git a/typescript-version/package.json b/typescript-version/package.json
index f94e106..2c794a2 100644
--- a/typescript-version/package.json
+++ b/typescript-version/package.json
@@ -1,6 +1,6 @@
{
"name": "twitch-vod-manager",
- "version": "3.8.8",
+ "version": "3.9.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 305d937..ba2109b 100644
--- a/typescript-version/src/index.html
+++ b/typescript-version/src/index.html
@@ -130,6 +130,7 @@
+
@@ -343,9 +344,30 @@
Updates
-
Version: v3.8.8
+
Version: v3.9.0
+
+
+
System-Check
+
+
+
+
+
Noch kein Check ausgefuhrt.
+
+
+
+
Live Debug-Log
+
+
+
+
+
Lade...
+
@@ -354,7 +376,7 @@
Nicht verbunden
- v3.8.8
+ v3.9.0
diff --git a/typescript-version/src/main.ts b/typescript-version/src/main.ts
index b85981c..af33d90 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.8.8';
+const APP_VERSION = '3.9.0';
const UPDATE_CHECK_URL = 'http://24-music.de/version.json';
// Paths
@@ -88,6 +88,22 @@ interface DownloadResult {
error?: string;
}
+interface PreflightChecks {
+ internet: boolean;
+ streamlink: boolean;
+ ffmpeg: boolean;
+ ffprobe: boolean;
+ downloadPathWritable: boolean;
+}
+
+interface PreflightResult {
+ ok: boolean;
+ autoFixApplied: boolean;
+ checks: PreflightChecks;
+ messages: string[];
+ timestamp: string;
+}
+
interface DownloadProgress {
id: string;
progress: number;
@@ -214,6 +230,94 @@ function getStreamlinkPath(): string {
return 'streamlink';
}
+function sleep(ms: number): Promise {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+function isDownloadPathWritable(targetPath: string): boolean {
+ try {
+ fs.mkdirSync(targetPath, { recursive: true });
+ const probeFile = path.join(targetPath, `.write_test_${Date.now()}.tmp`);
+ fs.writeFileSync(probeFile, 'ok');
+ fs.unlinkSync(probeFile);
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+async function hasInternetConnection(): Promise {
+ try {
+ const res = await axios.get('https://id.twitch.tv/oauth2/validate', {
+ timeout: 5000,
+ validateStatus: () => true
+ });
+ return res.status > 0;
+ } catch {
+ return false;
+ }
+}
+
+async function runPreflight(autoFix = false): Promise {
+ appendDebugLog('preflight-start', { autoFix });
+
+ refreshBundledToolPaths();
+
+ const checks: PreflightChecks = {
+ internet: await hasInternetConnection(),
+ streamlink: false,
+ ffmpeg: false,
+ ffprobe: false,
+ downloadPathWritable: isDownloadPathWritable(config.download_path)
+ };
+
+ if (autoFix) {
+ await ensureStreamlinkInstalled();
+ await ensureFfmpegInstalled();
+ refreshBundledToolPaths();
+ }
+
+ const streamlinkCmd = getStreamlinkCommand();
+ checks.streamlink = canExecuteCommand(streamlinkCmd.command, [...streamlinkCmd.prefixArgs, '--version']);
+
+ const ffmpegPath = getFFmpegPath();
+ const ffprobePath = getFFprobePath();
+ checks.ffmpeg = canExecuteCommand(ffmpegPath, ['-version']);
+ checks.ffprobe = canExecuteCommand(ffprobePath, ['-version']);
+
+ const messages: string[] = [];
+ if (!checks.internet) messages.push('Keine Internetverbindung erkannt.');
+ if (!checks.streamlink) messages.push('Streamlink fehlt oder ist nicht startbar.');
+ if (!checks.ffmpeg) messages.push('FFmpeg fehlt oder ist nicht startbar.');
+ if (!checks.ffprobe) messages.push('FFprobe fehlt oder ist nicht startbar.');
+ if (!checks.downloadPathWritable) messages.push('Download-Ordner ist nicht beschreibbar.');
+
+ const result: PreflightResult = {
+ ok: messages.length === 0,
+ autoFixApplied: autoFix,
+ checks,
+ messages,
+ timestamp: new Date().toISOString()
+ };
+
+ appendDebugLog('preflight-finished', result);
+ return result;
+}
+
+function readDebugLog(lines = 200): string {
+ try {
+ if (!fs.existsSync(DEBUG_LOG_FILE)) {
+ return 'Debug-Log ist leer.';
+ }
+
+ const text = fs.readFileSync(DEBUG_LOG_FILE, 'utf-8');
+ const rows = text.split(/\r?\n/).filter(Boolean);
+ return rows.slice(-lines).join('\n') || 'Debug-Log ist leer.';
+ } catch (e) {
+ return `Debug-Log konnte nicht gelesen werden: ${String(e)}`;
+ }
+}
+
function canExecute(cmd: string): boolean {
try {
execSync(cmd, { stdio: 'ignore', windowsHide: true });
@@ -1352,13 +1456,47 @@ async function processQueue(): Promise {
item.last_error = '';
- const result = await downloadVOD(item, (progress) => {
- mainWindow?.webContents.send('download-progress', progress);
- });
+ let finalResult: DownloadResult = { success: false, error: 'Unbekannter Fehler beim Download' };
- item.status = result.success ? 'completed' : 'error';
- item.progress = result.success ? 100 : 0;
- item.last_error = result.success ? '' : (result.error || 'Unbekannter Fehler beim Download');
+ for (let attempt = 1; attempt <= MAX_RETRY_ATTEMPTS; attempt++) {
+ appendDebugLog('queue-item-attempt', { itemId: item.id, attempt, max: MAX_RETRY_ATTEMPTS });
+
+ const result = await downloadVOD(item, (progress) => {
+ mainWindow?.webContents.send('download-progress', progress);
+ });
+
+ if (result.success) {
+ finalResult = result;
+ break;
+ }
+
+ finalResult = result;
+
+ if (!isDownloading || currentDownloadCancelled) {
+ finalResult = { success: false, error: 'Download wurde abgebrochen.' };
+ break;
+ }
+
+ if (attempt < MAX_RETRY_ATTEMPTS) {
+ item.last_error = `Versuch ${attempt}/${MAX_RETRY_ATTEMPTS} fehlgeschlagen: ${result.error || 'Unbekannter Fehler'}`;
+ mainWindow?.webContents.send('download-progress', {
+ id: item.id,
+ progress: -1,
+ speed: '',
+ eta: '',
+ status: `Neuer Versuch in ${RETRY_DELAY_SECONDS}s...`,
+ currentPart: item.currentPart,
+ totalParts: item.totalParts
+ } as DownloadProgress);
+ saveQueue(downloadQueue);
+ mainWindow?.webContents.send('queue-updated', downloadQueue);
+ await sleep(RETRY_DELAY_SECONDS * 1000);
+ }
+ }
+
+ item.status = finalResult.success ? 'completed' : 'error';
+ item.progress = finalResult.success ? 100 : 0;
+ item.last_error = finalResult.success ? '' : (finalResult.error || 'Unbekannter Fehler beim Download');
appendDebugLog('queue-item-finished', {
itemId: item.id,
status: item.status,
@@ -1521,6 +1659,28 @@ ipcMain.handle('clear-completed', () => {
return downloadQueue;
});
+ipcMain.handle('retry-failed-downloads', () => {
+ downloadQueue = downloadQueue.map((item) => {
+ if (item.status !== 'error') return item;
+
+ return {
+ ...item,
+ status: 'pending',
+ progress: 0,
+ last_error: ''
+ };
+ });
+
+ saveQueue(downloadQueue);
+ mainWindow?.webContents.send('queue-updated', downloadQueue);
+
+ if (!isDownloading) {
+ void processQueue();
+ }
+
+ return downloadQueue;
+});
+
ipcMain.handle('start-download', async () => {
const hasPendingItems = downloadQueue.some(item => item.status !== 'completed');
if (!hasPendingItems) {
@@ -1636,6 +1796,14 @@ ipcMain.handle('download-clip', async (_, clipUrl: string) => {
});
});
+ipcMain.handle('run-preflight', async (_, autoFix: boolean = false) => {
+ return await runPreflight(autoFix);
+});
+
+ipcMain.handle('get-debug-log', async (_, lines: number = 200) => {
+ return readDebugLog(lines);
+});
+
ipcMain.handle('is-downloading', () => isDownloading);
// Video Cutter IPC
diff --git a/typescript-version/src/preload.ts b/typescript-version/src/preload.ts
index e2577aa..0d803c6 100644
--- a/typescript-version/src/preload.ts
+++ b/typescript-version/src/preload.ts
@@ -61,6 +61,7 @@ contextBridge.exposeInMainWorld('api', {
addToQueue: (item: Omit) => ipcRenderer.invoke('add-to-queue', item),
removeFromQueue: (id: string) => ipcRenderer.invoke('remove-from-queue', id),
clearCompleted: () => ipcRenderer.invoke('clear-completed'),
+ retryFailedDownloads: () => ipcRenderer.invoke('retry-failed-downloads'),
// Download
startDownload: () => ipcRenderer.invoke('start-download'),
@@ -91,6 +92,8 @@ contextBridge.exposeInMainWorld('api', {
downloadUpdate: () => ipcRenderer.invoke('download-update'),
installUpdate: () => ipcRenderer.invoke('install-update'),
openExternal: (url: string) => ipcRenderer.invoke('open-external', url),
+ runPreflight: (autoFix: boolean) => ipcRenderer.invoke('run-preflight', autoFix),
+ getDebugLog: (lines: number) => ipcRenderer.invoke('get-debug-log', lines),
// Events
onDownloadProgress: (callback: (progress: DownloadProgress) => void) => {
diff --git a/typescript-version/src/renderer-globals.d.ts b/typescript-version/src/renderer-globals.d.ts
index 9d7996d..7de094b 100644
--- a/typescript-version/src/renderer-globals.d.ts
+++ b/typescript-version/src/renderer-globals.d.ts
@@ -87,6 +87,22 @@ interface UpdateDownloadProgress {
total: number;
}
+interface PreflightChecks {
+ internet: boolean;
+ streamlink: boolean;
+ ffmpeg: boolean;
+ ffprobe: boolean;
+ downloadPathWritable: boolean;
+}
+
+interface PreflightResult {
+ ok: boolean;
+ autoFixApplied: boolean;
+ checks: PreflightChecks;
+ messages: string[];
+ timestamp: string;
+}
+
interface ApiBridge {
getConfig(): Promise;
saveConfig(config: Partial): Promise;
@@ -97,6 +113,7 @@ interface ApiBridge {
addToQueue(item: Omit): Promise;
removeFromQueue(id: string): Promise;
clearCompleted(): Promise;
+ retryFailedDownloads(): Promise;
startDownload(): Promise;
cancelDownload(): Promise;
isDownloading(): Promise;
@@ -115,6 +132,8 @@ interface ApiBridge {
downloadUpdate(): Promise<{ downloading?: boolean; error?: boolean }>;
installUpdate(): Promise;
openExternal(url: string): Promise;
+ runPreflight(autoFix: boolean): Promise;
+ getDebugLog(lines: number): Promise;
onDownloadProgress(callback: (progress: DownloadProgress) => void): void;
onQueueUpdated(callback: (queue: QueueItem[]) => void): void;
onDownloadStarted(callback: () => void): void;
diff --git a/typescript-version/src/renderer-locale-de.ts b/typescript-version/src/renderer-locale-de.ts
index 51eed3c..114619b 100644
--- a/typescript-version/src/renderer-locale-de.ts
+++ b/typescript-version/src/renderer-locale-de.ts
@@ -7,6 +7,7 @@ const UI_TEXT_DE = {
navMerge: 'Videos zusammenfugen',
navSettings: 'Einstellungen',
queueTitle: 'Warteschlange',
+ retryFailed: 'Fehler neu',
clearQueue: 'Leeren',
refresh: 'Aktualisieren',
streamerPlaceholder: 'Streamer hinzufugen...',
@@ -36,6 +37,13 @@ const UI_TEXT_DE = {
partMinutesLabel: 'Teil-Lange (Minuten)',
updateTitle: 'Updates',
checkUpdates: 'Nach Updates suchen',
+ preflightTitle: 'System-Check',
+ preflightRun: 'Check ausfuhren',
+ preflightFix: 'Auto-Fix Tools',
+ preflightEmpty: 'Noch kein Check ausgefuhrt.',
+ debugLogTitle: 'Live Debug-Log',
+ refreshLog: 'Aktualisieren',
+ autoRefresh: 'Auto-Refresh',
notConnected: 'Nicht verbunden'
},
status: {
diff --git a/typescript-version/src/renderer-locale-en.ts b/typescript-version/src/renderer-locale-en.ts
index 875dda3..79ef216 100644
--- a/typescript-version/src/renderer-locale-en.ts
+++ b/typescript-version/src/renderer-locale-en.ts
@@ -7,6 +7,7 @@ const UI_TEXT_EN = {
navMerge: 'Merge Videos',
navSettings: 'Settings',
queueTitle: 'Queue',
+ retryFailed: 'Retry failed',
clearQueue: 'Clear',
refresh: 'Refresh',
streamerPlaceholder: 'Add streamer...',
@@ -36,6 +37,13 @@ const UI_TEXT_EN = {
partMinutesLabel: 'Part Length (Minutes)',
updateTitle: 'Updates',
checkUpdates: 'Check for updates',
+ preflightTitle: 'System Check',
+ preflightRun: 'Run check',
+ preflightFix: 'Auto-fix tools',
+ preflightEmpty: 'No checks run yet.',
+ debugLogTitle: 'Live Debug Log',
+ refreshLog: 'Refresh',
+ autoRefresh: 'Auto refresh',
notConnected: 'Not connected'
},
status: {
diff --git a/typescript-version/src/renderer-queue.ts b/typescript-version/src/renderer-queue.ts
index dc936b4..d17f45b 100644
--- a/typescript-version/src/renderer-queue.ts
+++ b/typescript-version/src/renderer-queue.ts
@@ -19,6 +19,11 @@ async function clearCompleted(): Promise {
renderQueue();
}
+async function retryFailedDownloads(): Promise {
+ queue = await window.api.retryFailedDownloads();
+ renderQueue();
+}
+
function getQueueStatusLabel(item: QueueItem): string {
if (item.status === 'completed') return UI_TEXT.queue.statusDone;
if (item.status === 'error') return UI_TEXT.queue.statusFailed;
diff --git a/typescript-version/src/renderer-settings.ts b/typescript-version/src/renderer-settings.ts
index 04592ae..cf5d2be 100644
--- a/typescript-version/src/renderer-settings.ts
+++ b/typescript-version/src/renderer-settings.ts
@@ -40,6 +40,56 @@ 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]
+ ];
+
+ 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.';
+
+ byId('preflightResult').textContent = `${lines}${extra}`;
+}
+
+async function runPreflight(autoFix = false): Promise {
+ const btn = byId(autoFix ? 'btnPreflightFix' : 'btnPreflightRun');
+ const old = btn.textContent || '';
+ btn.disabled = true;
+ btn.textContent = autoFix ? 'Fixe...' : 'Prufe...';
+
+ try {
+ const result = await window.api.runPreflight(autoFix);
+ renderPreflightResult(result);
+ } finally {
+ btn.disabled = false;
+ btn.textContent = old;
+ }
+}
+
+async function refreshDebugLog(): Promise {
+ const text = await window.api.getDebugLog(250);
+ const panel = byId('debugLogOutput');
+ panel.textContent = text;
+ panel.scrollTop = panel.scrollHeight;
+}
+
+function toggleDebugAutoRefresh(enabled: boolean): void {
+ if (debugLogAutoRefreshTimer) {
+ clearInterval(debugLogAutoRefreshTimer);
+ debugLogAutoRefreshTimer = null;
+ }
+
+ if (enabled) {
+ debugLogAutoRefreshTimer = window.setInterval(() => {
+ void refreshDebugLog();
+ }, 1500);
+ }
+}
+
async function saveSettings(): Promise {
const clientId = byId('clientId').value.trim();
const clientSecret = byId('clientSecret').value.trim();
diff --git a/typescript-version/src/renderer-shared.ts b/typescript-version/src/renderer-shared.ts
index 36e8eac..9493b5b 100644
--- a/typescript-version/src/renderer-shared.ts
+++ b/typescript-version/src/renderer-shared.ts
@@ -38,3 +38,4 @@ let clipDialogData: ClipDialogData | null = null;
let clipTotalSeconds = 0;
let updateReady = false;
+let debugLogAutoRefreshTimer: number | null = null;
diff --git a/typescript-version/src/renderer-texts.ts b/typescript-version/src/renderer-texts.ts
index 560b202..e861b08 100644
--- a/typescript-version/src/renderer-texts.ts
+++ b/typescript-version/src/renderer-texts.ts
@@ -46,6 +46,7 @@ function applyLanguageToStaticUI(): void {
setText('navMergeText', UI_TEXT.static.navMerge);
setText('navSettingsText', UI_TEXT.static.navSettings);
setText('queueTitleText', UI_TEXT.static.queueTitle);
+ setText('btnRetryFailed', UI_TEXT.static.retryFailed);
setText('btnClear', UI_TEXT.static.clearQueue);
setText('refreshText', UI_TEXT.static.refresh);
setText('clipsHeading', UI_TEXT.static.clipsHeading);
@@ -74,6 +75,13 @@ function applyLanguageToStaticUI(): void {
setText('partMinutesLabel', UI_TEXT.static.partMinutesLabel);
setText('updateTitle', UI_TEXT.static.updateTitle);
setText('checkUpdateBtn', UI_TEXT.static.checkUpdates);
+ setText('preflightTitle', UI_TEXT.static.preflightTitle);
+ setText('btnPreflightRun', UI_TEXT.static.preflightRun);
+ setText('btnPreflightFix', UI_TEXT.static.preflightFix);
+ setText('preflightResult', UI_TEXT.static.preflightEmpty);
+ setText('debugLogTitle', UI_TEXT.static.debugLogTitle);
+ setText('btnRefreshLog', UI_TEXT.static.refreshLog);
+ setText('autoRefreshText', UI_TEXT.static.autoRefresh);
setText('updateText', UI_TEXT.updates.bannerDefault);
setText('updateButton', UI_TEXT.updates.downloadNow);
setPlaceholder('newStreamer', UI_TEXT.static.streamerPlaceholder);
diff --git a/typescript-version/src/renderer.ts b/typescript-version/src/renderer.ts
index ea4c477..24dc277 100644
--- a/typescript-version/src/renderer.ts
+++ b/typescript-version/src/renderer.ts
@@ -80,6 +80,9 @@ async function init(): Promise {
void checkUpdateSilent();
}, 3000);
+ void runPreflight(false);
+ void refreshDebugLog();
+
setInterval(() => {
void syncQueueAndDownloadState();
}, 2000);
diff --git a/typescript-version/src/styles.css b/typescript-version/src/styles.css
index 7da2527..e96c375 100644
--- a/typescript-version/src/styles.css
+++ b/typescript-version/src/styles.css
@@ -318,6 +318,15 @@ body {
transition: all 0.2s;
}
+.btn-retry {
+ background: #2a3344;
+ color: #d9e4f7;
+}
+
+.btn-retry:hover {
+ background: #33405a;
+}
+
.btn-start {
background: var(--success);
color: white;
@@ -574,6 +583,19 @@ body {
gap: 10px;
}
+.log-panel {
+ background: #11151c;
+ border: 1px solid rgba(255,255,255,0.12);
+ border-radius: 6px;
+ padding: 10px;
+ max-height: 220px;
+ overflow: auto;
+ white-space: pre-wrap;
+ color: #b8c7df;
+ font-size: 12px;
+ line-height: 1.35;
+}
+
.form-row input {
flex: 1;
}