Ship v3.9.0 system reliability and UX toolkit
Add an in-app preflight diagnostics center with optional auto-fix, introduce backend retry handling for failed downloads, provide live debug log viewing in settings, and expand queue controls with retry-failed actions while keeping language switching instant and locale data organized.
This commit is contained in:
parent
2579198e8b
commit
551690d09c
4
typescript-version/package-lock.json
generated
4
typescript-version/package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "twitch-vod-manager",
|
"name": "twitch-vod-manager",
|
||||||
"version": "3.8.8",
|
"version": "3.9.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "twitch-vod-manager",
|
"name": "twitch-vod-manager",
|
||||||
"version": "3.8.8",
|
"version": "3.9.0",
|
||||||
"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": "3.8.8",
|
"version": "3.9.0",
|
||||||
"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",
|
||||||
|
|||||||
@ -130,6 +130,7 @@
|
|||||||
<div class="queue-list" id="queueList"></div>
|
<div class="queue-list" id="queueList"></div>
|
||||||
<div class="queue-actions">
|
<div class="queue-actions">
|
||||||
<button class="btn btn-start" id="btnStart" onclick="toggleDownload()">Start</button>
|
<button class="btn btn-start" id="btnStart" onclick="toggleDownload()">Start</button>
|
||||||
|
<button class="btn btn-retry" id="btnRetryFailed" onclick="retryFailedDownloads()">Fehler neu</button>
|
||||||
<button class="btn btn-clear" id="btnClear" onclick="clearCompleted()">Leeren</button>
|
<button class="btn btn-clear" id="btnClear" onclick="clearCompleted()">Leeren</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -343,9 +344,30 @@
|
|||||||
|
|
||||||
<div class="settings-card">
|
<div class="settings-card">
|
||||||
<h3 id="updateTitle">Updates</h3>
|
<h3 id="updateTitle">Updates</h3>
|
||||||
<p id="versionInfo" style="margin-bottom: 10px; color: var(--text-secondary);">Version: v3.8.8</p>
|
<p id="versionInfo" style="margin-bottom: 10px; color: var(--text-secondary);">Version: v3.9.0</p>
|
||||||
<button class="btn-secondary" id="checkUpdateBtn" onclick="checkUpdate()">Nach Updates suchen</button>
|
<button class="btn-secondary" id="checkUpdateBtn" onclick="checkUpdate()">Nach Updates suchen</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-card">
|
||||||
|
<h3 id="preflightTitle">System-Check</h3>
|
||||||
|
<div class="form-row" style="margin-bottom: 10px;">
|
||||||
|
<button class="btn-secondary" id="btnPreflightRun" onclick="runPreflight(false)">Check ausfuhren</button>
|
||||||
|
<button class="btn-secondary" id="btnPreflightFix" onclick="runPreflight(true)">Auto-Fix Tools</button>
|
||||||
|
</div>
|
||||||
|
<pre id="preflightResult" class="log-panel">Noch kein Check ausgefuhrt.</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-card">
|
||||||
|
<h3 id="debugLogTitle">Live Debug-Log</h3>
|
||||||
|
<div class="form-row" style="margin-bottom: 10px; align-items: center;">
|
||||||
|
<button class="btn-secondary" id="btnRefreshLog" onclick="refreshDebugLog()">Aktualisieren</button>
|
||||||
|
<label style="display:flex; align-items:center; gap:6px; font-size:13px; color: var(--text-secondary);">
|
||||||
|
<input type="checkbox" id="debugAutoRefresh" onchange="toggleDebugAutoRefresh(this.checked)">
|
||||||
|
<span id="autoRefreshText">Auto-Refresh</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<pre id="debugLogOutput" class="log-panel">Lade...</pre>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -354,7 +376,7 @@
|
|||||||
<div class="status-dot" id="statusDot"></div>
|
<div class="status-dot" id="statusDot"></div>
|
||||||
<span id="statusText">Nicht verbunden</span>
|
<span id="statusText">Nicht verbunden</span>
|
||||||
</div>
|
</div>
|
||||||
<span id="versionText">v3.8.8</span>
|
<span id="versionText">v3.9.0</span>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import { autoUpdater } from 'electron-updater';
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
// CONFIG & CONSTANTS
|
// CONFIG & CONSTANTS
|
||||||
// ==========================================
|
// ==========================================
|
||||||
const APP_VERSION = '3.8.8';
|
const APP_VERSION = '3.9.0';
|
||||||
const UPDATE_CHECK_URL = 'http://24-music.de/version.json';
|
const UPDATE_CHECK_URL = 'http://24-music.de/version.json';
|
||||||
|
|
||||||
// Paths
|
// Paths
|
||||||
@ -88,6 +88,22 @@ interface DownloadResult {
|
|||||||
error?: string;
|
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 {
|
interface DownloadProgress {
|
||||||
id: string;
|
id: string;
|
||||||
progress: number;
|
progress: number;
|
||||||
@ -214,6 +230,94 @@ function getStreamlinkPath(): string {
|
|||||||
return 'streamlink';
|
return 'streamlink';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sleep(ms: number): Promise<void> {
|
||||||
|
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<boolean> {
|
||||||
|
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<PreflightResult> {
|
||||||
|
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 {
|
function canExecute(cmd: string): boolean {
|
||||||
try {
|
try {
|
||||||
execSync(cmd, { stdio: 'ignore', windowsHide: true });
|
execSync(cmd, { stdio: 'ignore', windowsHide: true });
|
||||||
@ -1352,13 +1456,47 @@ async function processQueue(): Promise<void> {
|
|||||||
|
|
||||||
item.last_error = '';
|
item.last_error = '';
|
||||||
|
|
||||||
|
let finalResult: DownloadResult = { success: false, 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) => {
|
const result = await downloadVOD(item, (progress) => {
|
||||||
mainWindow?.webContents.send('download-progress', progress);
|
mainWindow?.webContents.send('download-progress', progress);
|
||||||
});
|
});
|
||||||
|
|
||||||
item.status = result.success ? 'completed' : 'error';
|
if (result.success) {
|
||||||
item.progress = result.success ? 100 : 0;
|
finalResult = result;
|
||||||
item.last_error = result.success ? '' : (result.error || 'Unbekannter Fehler beim Download');
|
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', {
|
appendDebugLog('queue-item-finished', {
|
||||||
itemId: item.id,
|
itemId: item.id,
|
||||||
status: item.status,
|
status: item.status,
|
||||||
@ -1521,6 +1659,28 @@ ipcMain.handle('clear-completed', () => {
|
|||||||
return downloadQueue;
|
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 () => {
|
ipcMain.handle('start-download', async () => {
|
||||||
const hasPendingItems = downloadQueue.some(item => item.status !== 'completed');
|
const hasPendingItems = downloadQueue.some(item => item.status !== 'completed');
|
||||||
if (!hasPendingItems) {
|
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);
|
ipcMain.handle('is-downloading', () => isDownloading);
|
||||||
|
|
||||||
// Video Cutter IPC
|
// Video Cutter IPC
|
||||||
|
|||||||
@ -61,6 +61,7 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
addToQueue: (item: Omit<QueueItem, 'id' | 'status' | 'progress'>) => ipcRenderer.invoke('add-to-queue', item),
|
addToQueue: (item: Omit<QueueItem, 'id' | 'status' | 'progress'>) => ipcRenderer.invoke('add-to-queue', item),
|
||||||
removeFromQueue: (id: string) => ipcRenderer.invoke('remove-from-queue', id),
|
removeFromQueue: (id: string) => ipcRenderer.invoke('remove-from-queue', id),
|
||||||
clearCompleted: () => ipcRenderer.invoke('clear-completed'),
|
clearCompleted: () => ipcRenderer.invoke('clear-completed'),
|
||||||
|
retryFailedDownloads: () => ipcRenderer.invoke('retry-failed-downloads'),
|
||||||
|
|
||||||
// Download
|
// Download
|
||||||
startDownload: () => ipcRenderer.invoke('start-download'),
|
startDownload: () => ipcRenderer.invoke('start-download'),
|
||||||
@ -91,6 +92,8 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
downloadUpdate: () => ipcRenderer.invoke('download-update'),
|
downloadUpdate: () => ipcRenderer.invoke('download-update'),
|
||||||
installUpdate: () => ipcRenderer.invoke('install-update'),
|
installUpdate: () => ipcRenderer.invoke('install-update'),
|
||||||
openExternal: (url: string) => ipcRenderer.invoke('open-external', url),
|
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
|
// Events
|
||||||
onDownloadProgress: (callback: (progress: DownloadProgress) => void) => {
|
onDownloadProgress: (callback: (progress: DownloadProgress) => void) => {
|
||||||
|
|||||||
19
typescript-version/src/renderer-globals.d.ts
vendored
19
typescript-version/src/renderer-globals.d.ts
vendored
@ -87,6 +87,22 @@ interface UpdateDownloadProgress {
|
|||||||
total: number;
|
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 {
|
interface ApiBridge {
|
||||||
getConfig(): Promise<AppConfig>;
|
getConfig(): Promise<AppConfig>;
|
||||||
saveConfig(config: Partial<AppConfig>): Promise<AppConfig>;
|
saveConfig(config: Partial<AppConfig>): Promise<AppConfig>;
|
||||||
@ -97,6 +113,7 @@ interface ApiBridge {
|
|||||||
addToQueue(item: Omit<QueueItem, 'id' | 'status' | 'progress'>): Promise<QueueItem[]>;
|
addToQueue(item: Omit<QueueItem, 'id' | 'status' | 'progress'>): Promise<QueueItem[]>;
|
||||||
removeFromQueue(id: string): Promise<QueueItem[]>;
|
removeFromQueue(id: string): Promise<QueueItem[]>;
|
||||||
clearCompleted(): Promise<QueueItem[]>;
|
clearCompleted(): Promise<QueueItem[]>;
|
||||||
|
retryFailedDownloads(): Promise<QueueItem[]>;
|
||||||
startDownload(): Promise<boolean>;
|
startDownload(): Promise<boolean>;
|
||||||
cancelDownload(): Promise<boolean>;
|
cancelDownload(): Promise<boolean>;
|
||||||
isDownloading(): Promise<boolean>;
|
isDownloading(): Promise<boolean>;
|
||||||
@ -115,6 +132,8 @@ interface ApiBridge {
|
|||||||
downloadUpdate(): Promise<{ downloading?: boolean; error?: boolean }>;
|
downloadUpdate(): Promise<{ downloading?: boolean; error?: boolean }>;
|
||||||
installUpdate(): Promise<void>;
|
installUpdate(): Promise<void>;
|
||||||
openExternal(url: string): Promise<void>;
|
openExternal(url: string): Promise<void>;
|
||||||
|
runPreflight(autoFix: boolean): Promise<PreflightResult>;
|
||||||
|
getDebugLog(lines: number): Promise<string>;
|
||||||
onDownloadProgress(callback: (progress: DownloadProgress) => void): void;
|
onDownloadProgress(callback: (progress: DownloadProgress) => void): void;
|
||||||
onQueueUpdated(callback: (queue: QueueItem[]) => void): void;
|
onQueueUpdated(callback: (queue: QueueItem[]) => void): void;
|
||||||
onDownloadStarted(callback: () => void): void;
|
onDownloadStarted(callback: () => void): void;
|
||||||
|
|||||||
@ -7,6 +7,7 @@ const UI_TEXT_DE = {
|
|||||||
navMerge: 'Videos zusammenfugen',
|
navMerge: 'Videos zusammenfugen',
|
||||||
navSettings: 'Einstellungen',
|
navSettings: 'Einstellungen',
|
||||||
queueTitle: 'Warteschlange',
|
queueTitle: 'Warteschlange',
|
||||||
|
retryFailed: 'Fehler neu',
|
||||||
clearQueue: 'Leeren',
|
clearQueue: 'Leeren',
|
||||||
refresh: 'Aktualisieren',
|
refresh: 'Aktualisieren',
|
||||||
streamerPlaceholder: 'Streamer hinzufugen...',
|
streamerPlaceholder: 'Streamer hinzufugen...',
|
||||||
@ -36,6 +37,13 @@ const UI_TEXT_DE = {
|
|||||||
partMinutesLabel: 'Teil-Lange (Minuten)',
|
partMinutesLabel: 'Teil-Lange (Minuten)',
|
||||||
updateTitle: 'Updates',
|
updateTitle: 'Updates',
|
||||||
checkUpdates: 'Nach Updates suchen',
|
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'
|
notConnected: 'Nicht verbunden'
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
|
|||||||
@ -7,6 +7,7 @@ const UI_TEXT_EN = {
|
|||||||
navMerge: 'Merge Videos',
|
navMerge: 'Merge Videos',
|
||||||
navSettings: 'Settings',
|
navSettings: 'Settings',
|
||||||
queueTitle: 'Queue',
|
queueTitle: 'Queue',
|
||||||
|
retryFailed: 'Retry failed',
|
||||||
clearQueue: 'Clear',
|
clearQueue: 'Clear',
|
||||||
refresh: 'Refresh',
|
refresh: 'Refresh',
|
||||||
streamerPlaceholder: 'Add streamer...',
|
streamerPlaceholder: 'Add streamer...',
|
||||||
@ -36,6 +37,13 @@ const UI_TEXT_EN = {
|
|||||||
partMinutesLabel: 'Part Length (Minutes)',
|
partMinutesLabel: 'Part Length (Minutes)',
|
||||||
updateTitle: 'Updates',
|
updateTitle: 'Updates',
|
||||||
checkUpdates: 'Check for 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'
|
notConnected: 'Not connected'
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
|
|||||||
@ -19,6 +19,11 @@ async function clearCompleted(): Promise<void> {
|
|||||||
renderQueue();
|
renderQueue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function retryFailedDownloads(): Promise<void> {
|
||||||
|
queue = await window.api.retryFailedDownloads();
|
||||||
|
renderQueue();
|
||||||
|
}
|
||||||
|
|
||||||
function getQueueStatusLabel(item: QueueItem): string {
|
function getQueueStatusLabel(item: QueueItem): string {
|
||||||
if (item.status === 'completed') return UI_TEXT.queue.statusDone;
|
if (item.status === 'completed') return UI_TEXT.queue.statusDone;
|
||||||
if (item.status === 'error') return UI_TEXT.queue.statusFailed;
|
if (item.status === 'error') return UI_TEXT.queue.statusFailed;
|
||||||
|
|||||||
@ -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<void> {
|
||||||
|
const btn = byId<HTMLButtonElement>(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<void> {
|
||||||
|
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<void> {
|
async function saveSettings(): Promise<void> {
|
||||||
const clientId = byId<HTMLInputElement>('clientId').value.trim();
|
const clientId = byId<HTMLInputElement>('clientId').value.trim();
|
||||||
const clientSecret = byId<HTMLInputElement>('clientSecret').value.trim();
|
const clientSecret = byId<HTMLInputElement>('clientSecret').value.trim();
|
||||||
|
|||||||
@ -38,3 +38,4 @@ let clipDialogData: ClipDialogData | null = null;
|
|||||||
let clipTotalSeconds = 0;
|
let clipTotalSeconds = 0;
|
||||||
|
|
||||||
let updateReady = false;
|
let updateReady = false;
|
||||||
|
let debugLogAutoRefreshTimer: number | null = null;
|
||||||
|
|||||||
@ -46,6 +46,7 @@ function applyLanguageToStaticUI(): void {
|
|||||||
setText('navMergeText', UI_TEXT.static.navMerge);
|
setText('navMergeText', UI_TEXT.static.navMerge);
|
||||||
setText('navSettingsText', UI_TEXT.static.navSettings);
|
setText('navSettingsText', UI_TEXT.static.navSettings);
|
||||||
setText('queueTitleText', UI_TEXT.static.queueTitle);
|
setText('queueTitleText', UI_TEXT.static.queueTitle);
|
||||||
|
setText('btnRetryFailed', UI_TEXT.static.retryFailed);
|
||||||
setText('btnClear', UI_TEXT.static.clearQueue);
|
setText('btnClear', UI_TEXT.static.clearQueue);
|
||||||
setText('refreshText', UI_TEXT.static.refresh);
|
setText('refreshText', UI_TEXT.static.refresh);
|
||||||
setText('clipsHeading', UI_TEXT.static.clipsHeading);
|
setText('clipsHeading', UI_TEXT.static.clipsHeading);
|
||||||
@ -74,6 +75,13 @@ function applyLanguageToStaticUI(): void {
|
|||||||
setText('partMinutesLabel', UI_TEXT.static.partMinutesLabel);
|
setText('partMinutesLabel', UI_TEXT.static.partMinutesLabel);
|
||||||
setText('updateTitle', UI_TEXT.static.updateTitle);
|
setText('updateTitle', UI_TEXT.static.updateTitle);
|
||||||
setText('checkUpdateBtn', UI_TEXT.static.checkUpdates);
|
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('updateText', UI_TEXT.updates.bannerDefault);
|
||||||
setText('updateButton', UI_TEXT.updates.downloadNow);
|
setText('updateButton', UI_TEXT.updates.downloadNow);
|
||||||
setPlaceholder('newStreamer', UI_TEXT.static.streamerPlaceholder);
|
setPlaceholder('newStreamer', UI_TEXT.static.streamerPlaceholder);
|
||||||
|
|||||||
@ -80,6 +80,9 @@ async function init(): Promise<void> {
|
|||||||
void checkUpdateSilent();
|
void checkUpdateSilent();
|
||||||
}, 3000);
|
}, 3000);
|
||||||
|
|
||||||
|
void runPreflight(false);
|
||||||
|
void refreshDebugLog();
|
||||||
|
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
void syncQueueAndDownloadState();
|
void syncQueueAndDownloadState();
|
||||||
}, 2000);
|
}, 2000);
|
||||||
|
|||||||
@ -318,6 +318,15 @@ body {
|
|||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-retry {
|
||||||
|
background: #2a3344;
|
||||||
|
color: #d9e4f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-retry:hover {
|
||||||
|
background: #33405a;
|
||||||
|
}
|
||||||
|
|
||||||
.btn-start {
|
.btn-start {
|
||||||
background: var(--success);
|
background: var(--success);
|
||||||
color: white;
|
color: white;
|
||||||
@ -574,6 +583,19 @@ body {
|
|||||||
gap: 10px;
|
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 {
|
.form-row input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user