Fix startup stall and queue status flicker (v3.8.1)

Defer heavy tool auto-install checks from startup to first-use paths to avoid long launch stalls, and merge backend queue snapshots with in-flight renderer progress state so download status text no longer jumps between placeholder states.
This commit is contained in:
xRangerDE 2026-02-13 12:30:07 +01:00
parent 3d404d75e1
commit 159f442d43
5 changed files with 67 additions and 24 deletions

View File

@ -1,12 +1,12 @@
{ {
"name": "twitch-vod-manager", "name": "twitch-vod-manager",
"version": "3.8.0", "version": "3.8.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "twitch-vod-manager", "name": "twitch-vod-manager",
"version": "3.8.0", "version": "3.8.1",
"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": "3.8.0", "version": "3.8.1",
"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

@ -335,7 +335,7 @@
<div class="settings-card"> <div class="settings-card">
<h3>Updates</h3> <h3>Updates</h3>
<p id="versionInfo" style="margin-bottom: 10px; color: var(--text-secondary);">Version: v3.8.0</p> <p id="versionInfo" style="margin-bottom: 10px; color: var(--text-secondary);">Version: v3.8.1</p>
<button class="btn-secondary" onclick="checkUpdate()">Nach Updates suchen</button> <button class="btn-secondary" onclick="checkUpdate()">Nach Updates suchen</button>
</div> </div>
</div> </div>
@ -346,7 +346,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.0</span> <span id="versionText">v3.8.1</span>
</div> </div>
</main> </main>
</div> </div>

View File

@ -1,14 +1,14 @@
import { app, BrowserWindow, ipcMain, dialog, shell } from 'electron'; import { app, BrowserWindow, ipcMain, dialog, shell } 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, execFileSync, spawnSync } from 'child_process'; import { spawn, ChildProcess, execSync, exec, spawnSync } from 'child_process';
import axios from 'axios'; import axios from 'axios';
import { autoUpdater } from 'electron-updater'; import { autoUpdater } from 'electron-updater';
// ========================================== // ==========================================
// CONFIG & CONSTANTS // CONFIG & CONSTANTS
// ========================================== // ==========================================
const APP_VERSION = '3.8.0'; const APP_VERSION = '3.8.1';
const UPDATE_CHECK_URL = 'http://24-music.de/version.json'; const UPDATE_CHECK_URL = 'http://24-music.de/version.json';
// Paths // Paths
@ -273,15 +273,36 @@ async function downloadFile(url: string, destinationPath: string): Promise<boole
} }
} }
function extractZip(zipPath: string, destinationDir: string): boolean { async function extractZip(zipPath: string, destinationDir: string): Promise<boolean> {
try { try {
fs.mkdirSync(destinationDir, { recursive: true }); fs.mkdirSync(destinationDir, { recursive: true });
execFileSync('powershell', [
const command = `Expand-Archive -Path '${zipPath.replace(/'/g, "''")}' -DestinationPath '${destinationDir.replace(/'/g, "''")}' -Force`;
await new Promise<void>((resolve, reject) => {
const proc = spawn('powershell', [
'-NoProfile', '-NoProfile',
'-ExecutionPolicy', 'Bypass', '-ExecutionPolicy', 'Bypass',
'-Command', '-Command',
`Expand-Archive -Path '${zipPath.replace(/'/g, "''")}' -DestinationPath '${destinationDir.replace(/'/g, "''")}' -Force` command
], { windowsHide: true, stdio: 'ignore' }); ], { windowsHide: true });
let stderr = '';
proc.stderr?.on('data', (data) => {
stderr += data.toString();
});
proc.on('close', (code) => {
if (code === 0) {
resolve();
} else {
reject(new Error(`Expand-Archive exit code ${code}: ${stderr.trim()}`));
}
});
proc.on('error', (err) => reject(err));
});
return true; return true;
} catch (e) { } catch (e) {
appendDebugLog('extract-zip-failed', { zipPath, destinationDir, error: String(e) }); appendDebugLog('extract-zip-failed', { zipPath, destinationDir, error: String(e) });
@ -327,7 +348,7 @@ async function ensureStreamlinkInstalled(): Promise<boolean> {
fs.rmSync(TOOLS_STREAMLINK_DIR, { recursive: true, force: true }); fs.rmSync(TOOLS_STREAMLINK_DIR, { recursive: true, force: true });
fs.mkdirSync(TOOLS_STREAMLINK_DIR, { recursive: true }); fs.mkdirSync(TOOLS_STREAMLINK_DIR, { recursive: true });
const extractOk = extractZip(zipPath, TOOLS_STREAMLINK_DIR); const extractOk = await extractZip(zipPath, TOOLS_STREAMLINK_DIR);
try { fs.unlinkSync(zipPath); } catch { } try { fs.unlinkSync(zipPath); } catch { }
if (!extractOk) return false; if (!extractOk) return false;
@ -368,7 +389,7 @@ async function ensureFfmpegInstalled(): Promise<boolean> {
fs.rmSync(TOOLS_FFMPEG_DIR, { recursive: true, force: true }); fs.rmSync(TOOLS_FFMPEG_DIR, { recursive: true, force: true });
fs.mkdirSync(TOOLS_FFMPEG_DIR, { recursive: true }); fs.mkdirSync(TOOLS_FFMPEG_DIR, { recursive: true });
const extractOk = extractZip(zipPath, TOOLS_FFMPEG_DIR); const extractOk = await extractZip(zipPath, TOOLS_FFMPEG_DIR);
try { fs.unlinkSync(zipPath); } catch { } try { fs.unlinkSync(zipPath); } catch { }
if (!extractOk) return false; if (!extractOk) return false;
@ -1653,12 +1674,7 @@ ipcMain.handle('save-video-dialog', async (_, defaultName: string) => {
app.whenReady().then(() => { app.whenReady().then(() => {
refreshBundledToolPaths(); refreshBundledToolPaths();
createWindow(); createWindow();
appendDebugLog('startup-tools-check-skipped', 'Deferred to first use');
void (async () => {
const streamlinkOk = await ensureStreamlinkInstalled();
const ffmpegOk = await ensureFfmpegInstalled();
appendDebugLog('startup-tools-check', { streamlinkOk, ffmpegOk });
})();
app.on('activate', () => { app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) { if (BrowserWindow.getAllWindows().length === 0) {

View File

@ -21,7 +21,7 @@ async function init(): Promise<void> {
updateDownloadButtonState(); updateDownloadButtonState();
window.api.onQueueUpdated((q: QueueItem[]) => { window.api.onQueueUpdated((q: QueueItem[]) => {
queue = Array.isArray(q) ? q : []; queue = mergeQueueState(Array.isArray(q) ? q : []);
renderQueue(); renderQueue();
}); });
@ -82,6 +82,33 @@ async function init(): Promise<void> {
}, 2000); }, 2000);
} }
function mergeQueueState(nextQueue: QueueItem[]): QueueItem[] {
const prevById = new Map(queue.map((item) => [item.id, item]));
return nextQueue.map((item) => {
const prev = prevById.get(item.id);
if (!prev) {
return item;
}
if (item.status !== 'downloading') {
return item;
}
return {
...item,
progress: item.progress > 0 ? item.progress : prev.progress,
speed: item.speed || prev.speed,
eta: item.eta || prev.eta,
currentPart: item.currentPart || prev.currentPart,
totalParts: item.totalParts || prev.totalParts,
downloadedBytes: item.downloadedBytes || prev.downloadedBytes,
totalBytes: item.totalBytes || prev.totalBytes,
progressStatus: item.progressStatus || prev.progressStatus
};
});
}
function updateDownloadButtonState(): void { function updateDownloadButtonState(): void {
const btn = byId('btnStart'); const btn = byId('btnStart');
btn.textContent = downloading ? 'Stoppen' : 'Start'; btn.textContent = downloading ? 'Stoppen' : 'Start';
@ -90,7 +117,7 @@ function updateDownloadButtonState(): void {
async function syncQueueAndDownloadState(): Promise<void> { async function syncQueueAndDownloadState(): Promise<void> {
const latestQueue = await window.api.getQueue(); const latestQueue = await window.api.getQueue();
queue = Array.isArray(latestQueue) ? latestQueue : []; queue = mergeQueueState(Array.isArray(latestQueue) ? latestQueue : []);
renderQueue(); renderQueue();
const backendDownloading = await window.api.isDownloading(); const backendDownloading = await window.api.isDownloading();