Optimize tool path caching and streamer load race handling (v4.1.4)

This commit is contained in:
xRangerDE 2026-02-17 23:53:37 +01:00
parent 014c8ecf74
commit 7412e7aa16
5 changed files with 135 additions and 26 deletions

View File

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

View File

@ -457,7 +457,7 @@
<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: v4.1.3</p> <p id="versionInfo" style="margin-bottom: 10px; color: var(--text-secondary);">Version: v4.1.4</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>
@ -502,7 +502,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">v4.1.3</span> <span id="versionText">v4.1.4</span>
</div> </div>
</main> </main>
</div> </div>

View File

@ -8,7 +8,7 @@ import { autoUpdater } from 'electron-updater';
// ========================================== // ==========================================
// CONFIG & CONSTANTS // CONFIG & CONSTANTS
// ========================================== // ==========================================
const APP_VERSION = '4.1.3'; const APP_VERSION = '4.1.4';
const UPDATE_CHECK_URL = 'http://24-music.de/version.json'; const UPDATE_CHECK_URL = 'http://24-music.de/version.json';
// Paths // Paths
@ -27,6 +27,7 @@ const DEFAULT_METADATA_CACHE_MINUTES = 10;
const DEFAULT_PERFORMANCE_MODE: PerformanceMode = 'balanced'; const DEFAULT_PERFORMANCE_MODE: PerformanceMode = 'balanced';
const QUEUE_SAVE_DEBOUNCE_MS = 250; const QUEUE_SAVE_DEBOUNCE_MS = 250;
const MIN_FREE_DISK_BYTES = 128 * 1024 * 1024; const MIN_FREE_DISK_BYTES = 128 * 1024 * 1024;
const TOOL_PATH_REFRESH_TTL_MS = 10 * 1000;
const CACHE_CLEANUP_INTERVAL_MS = 60 * 1000; const CACHE_CLEANUP_INTERVAL_MS = 60 * 1000;
const MAX_LOGIN_TO_USER_ID_CACHE_ENTRIES = 4096; const MAX_LOGIN_TO_USER_ID_CACHE_ENTRIES = 4096;
const MAX_VOD_LIST_CACHE_ENTRIES = 512; const MAX_VOD_LIST_CACHE_ENTRIES = 512;
@ -373,23 +374,40 @@ let streamlinkCommandCache: { command: string; prefixArgs: string[] } | null = n
let bundledStreamlinkPath: string | null = null; let bundledStreamlinkPath: string | null = null;
let bundledFFmpegPath: string | null = null; let bundledFFmpegPath: string | null = null;
let bundledFFprobePath: string | null = null; let bundledFFprobePath: string | null = null;
let streamlinkPathCache: string | null = null;
let ffmpegPathCache: string | null = null;
let ffprobePathCache: string | null = null;
let bundledToolPathSignature = '';
let bundledToolPathRefreshedAt = 0;
// ========================================== // ==========================================
// TOOL PATHS // TOOL PATHS
// ========================================== // ==========================================
function getStreamlinkPath(): string { function getStreamlinkPath(): string {
if (streamlinkPathCache) {
if (streamlinkPathCache === 'streamlink' || fs.existsSync(streamlinkPathCache)) {
return streamlinkPathCache;
}
streamlinkPathCache = null;
}
if (bundledStreamlinkPath && fs.existsSync(bundledStreamlinkPath)) { if (bundledStreamlinkPath && fs.existsSync(bundledStreamlinkPath)) {
return bundledStreamlinkPath; streamlinkPathCache = bundledStreamlinkPath;
return streamlinkPathCache;
} }
try { try {
if (process.platform === 'win32') { if (process.platform === 'win32') {
const result = execSync('where streamlink', { encoding: 'utf-8' }); const result = execSync('where streamlink', { encoding: 'utf-8' });
const paths = result.trim().split('\n'); const paths = result.trim().split('\n');
if (paths.length > 0) return paths[0].trim(); if (paths.length > 0) {
streamlinkPathCache = paths[0].trim();
return streamlinkPathCache;
}
} else { } else {
const result = execSync('which streamlink', { encoding: 'utf-8' }); const result = execSync('which streamlink', { encoding: 'utf-8' });
return result.trim(); streamlinkPathCache = result.trim();
return streamlinkPathCache;
} }
} catch { } } catch { }
@ -400,10 +418,14 @@ function getStreamlinkPath(): string {
]; ];
for (const p of commonPaths) { for (const p of commonPaths) {
if (fs.existsSync(p)) return p; if (fs.existsSync(p)) {
streamlinkPathCache = p;
return streamlinkPathCache;
}
} }
return 'streamlink'; streamlinkPathCache = 'streamlink';
return streamlinkPathCache;
} }
function sleep(ms: number): Promise<void> { function sleep(ms: number): Promise<void> {
@ -450,7 +472,7 @@ async function runPreflight(autoFix = false): Promise<PreflightResult> {
if (autoFix) { if (autoFix) {
await ensureStreamlinkInstalled(); await ensureStreamlinkInstalled();
await ensureFfmpegInstalled(); await ensureFfmpegInstalled();
refreshBundledToolPaths(); refreshBundledToolPaths(true);
} }
const streamlinkCmd = getStreamlinkCommand(); const streamlinkCmd = getStreamlinkCommand();
@ -531,10 +553,44 @@ function findFileRecursive(rootDir: string, fileName: string): string | null {
return null; return null;
} }
function refreshBundledToolPaths(): void { function getDirectoryMtimeMs(directoryPath: string): number {
bundledStreamlinkPath = findFileRecursive(TOOLS_STREAMLINK_DIR, process.platform === 'win32' ? 'streamlink.exe' : 'streamlink'); try {
bundledFFmpegPath = findFileRecursive(TOOLS_FFMPEG_DIR, process.platform === 'win32' ? 'ffmpeg.exe' : 'ffmpeg'); return fs.statSync(directoryPath).mtimeMs;
bundledFFprobePath = findFileRecursive(TOOLS_FFMPEG_DIR, process.platform === 'win32' ? 'ffprobe.exe' : 'ffprobe'); } catch {
return 0;
}
}
function refreshBundledToolPaths(force = false): void {
const now = Date.now();
const signature = `${getDirectoryMtimeMs(TOOLS_STREAMLINK_DIR)}|${getDirectoryMtimeMs(TOOLS_FFMPEG_DIR)}`;
if (!force && signature === bundledToolPathSignature && (now - bundledToolPathRefreshedAt) < TOOL_PATH_REFRESH_TTL_MS) {
return;
}
bundledToolPathSignature = signature;
bundledToolPathRefreshedAt = now;
const nextBundledStreamlinkPath = findFileRecursive(TOOLS_STREAMLINK_DIR, process.platform === 'win32' ? 'streamlink.exe' : 'streamlink');
const nextBundledFFmpegPath = findFileRecursive(TOOLS_FFMPEG_DIR, process.platform === 'win32' ? 'ffmpeg.exe' : 'ffmpeg');
const nextBundledFFprobePath = findFileRecursive(TOOLS_FFMPEG_DIR, process.platform === 'win32' ? 'ffprobe.exe' : 'ffprobe');
const changed =
nextBundledStreamlinkPath !== bundledStreamlinkPath ||
nextBundledFFmpegPath !== bundledFFmpegPath ||
nextBundledFFprobePath !== bundledFFprobePath;
bundledStreamlinkPath = nextBundledStreamlinkPath;
bundledFFmpegPath = nextBundledFFmpegPath;
bundledFFprobePath = nextBundledFFprobePath;
if (changed) {
streamlinkPathCache = null;
ffmpegPathCache = null;
ffprobePathCache = null;
streamlinkCommandCache = null;
}
} }
async function downloadFile(url: string, destinationPath: string): Promise<boolean> { async function downloadFile(url: string, destinationPath: string): Promise<boolean> {
@ -634,7 +690,7 @@ async function ensureStreamlinkInstalled(): Promise<boolean> {
try { fs.unlinkSync(zipPath); } catch { } try { fs.unlinkSync(zipPath); } catch { }
if (!extractOk) return false; if (!extractOk) return false;
refreshBundledToolPaths(); refreshBundledToolPaths(true);
streamlinkCommandCache = null; streamlinkCommandCache = null;
const cmd = getStreamlinkCommand(); const cmd = getStreamlinkCommand();
@ -675,7 +731,7 @@ async function ensureFfmpegInstalled(): Promise<boolean> {
try { fs.unlinkSync(zipPath); } catch { } try { fs.unlinkSync(zipPath); } catch { }
if (!extractOk) return false; if (!extractOk) return false;
refreshBundledToolPaths(); refreshBundledToolPaths(true);
const newFfmpegPath = getFFmpegPath(); const newFfmpegPath = getFFmpegPath();
const newFfprobePath = getFFprobePath(); const newFfprobePath = getFFprobePath();
@ -726,18 +782,30 @@ function getStreamlinkCommand(): { command: string; prefixArgs: string[] } {
} }
function getFFmpegPath(): string { function getFFmpegPath(): string {
if (ffmpegPathCache) {
if (ffmpegPathCache === 'ffmpeg' || fs.existsSync(ffmpegPathCache)) {
return ffmpegPathCache;
}
ffmpegPathCache = null;
}
if (bundledFFmpegPath && fs.existsSync(bundledFFmpegPath)) { if (bundledFFmpegPath && fs.existsSync(bundledFFmpegPath)) {
return bundledFFmpegPath; ffmpegPathCache = bundledFFmpegPath;
return ffmpegPathCache;
} }
try { try {
if (process.platform === 'win32') { if (process.platform === 'win32') {
const result = execSync('where ffmpeg', { encoding: 'utf-8' }); const result = execSync('where ffmpeg', { encoding: 'utf-8' });
const paths = result.trim().split('\n'); const paths = result.trim().split('\n');
if (paths.length > 0) return paths[0].trim(); if (paths.length > 0) {
ffmpegPathCache = paths[0].trim();
return ffmpegPathCache;
}
} else { } else {
const result = execSync('which ffmpeg', { encoding: 'utf-8' }); const result = execSync('which ffmpeg', { encoding: 'utf-8' });
return result.trim(); ffmpegPathCache = result.trim();
return ffmpegPathCache;
} }
} catch { } } catch { }
@ -748,20 +816,45 @@ function getFFmpegPath(): string {
]; ];
for (const p of commonPaths) { for (const p of commonPaths) {
if (fs.existsSync(p)) return p; if (fs.existsSync(p)) {
ffmpegPathCache = p;
return ffmpegPathCache;
}
} }
return 'ffmpeg'; ffmpegPathCache = 'ffmpeg';
return ffmpegPathCache;
} }
function getFFprobePath(): string { function getFFprobePath(): string {
if (ffprobePathCache) {
if (ffprobePathCache === 'ffprobe' || ffprobePathCache === 'ffprobe.exe' || fs.existsSync(ffprobePathCache)) {
return ffprobePathCache;
}
ffprobePathCache = null;
}
if (bundledFFprobePath && fs.existsSync(bundledFFprobePath)) { if (bundledFFprobePath && fs.existsSync(bundledFFprobePath)) {
return bundledFFprobePath; ffprobePathCache = bundledFFprobePath;
return ffprobePathCache;
} }
const ffmpegPath = getFFmpegPath(); const ffmpegPath = getFFmpegPath();
const ffprobeExe = process.platform === 'win32' ? 'ffprobe.exe' : 'ffprobe'; const ffprobeExe = process.platform === 'win32' ? 'ffprobe.exe' : 'ffprobe';
return path.join(path.dirname(ffmpegPath), ffprobeExe);
if (ffmpegPath === 'ffmpeg') {
ffprobePathCache = ffprobeExe;
return ffprobePathCache;
}
const derivedFfprobePath = path.join(path.dirname(ffmpegPath), ffprobeExe);
if (fs.existsSync(derivedFfprobePath)) {
ffprobePathCache = derivedFfprobePath;
return ffprobePathCache;
}
ffprobePathCache = ffprobeExe;
return ffprobePathCache;
} }
function appendDebugLog(message: string, details?: unknown): void { function appendDebugLog(message: string, details?: unknown): void {
@ -3038,7 +3131,7 @@ ipcMain.handle('save-video-dialog', async (_, defaultName: string) => {
// APP LIFECYCLE // APP LIFECYCLE
// ========================================== // ==========================================
app.whenReady().then(() => { app.whenReady().then(() => {
refreshBundledToolPaths(); refreshBundledToolPaths(true);
startMetadataCacheCleanup(); startMetadataCacheCleanup();
createWindow(); createWindow();
appendDebugLog('startup-tools-check-skipped', 'Deferred to first use'); appendDebugLog('startup-tools-check-skipped', 'Deferred to first use');

View File

@ -1,3 +1,5 @@
let selectStreamerRequestId = 0;
function renderStreamers(): void { function renderStreamers(): void {
const list = byId('streamerList'); const list = byId('streamerList');
list.innerHTML = ''; list.innerHTML = '';
@ -50,12 +52,18 @@ async function removeStreamer(name: string): Promise<void> {
} }
async function selectStreamer(name: string, forceRefresh = false): Promise<void> { async function selectStreamer(name: string, forceRefresh = false): Promise<void> {
const requestId = ++selectStreamerRequestId;
const isStaleRequest = () => requestId !== selectStreamerRequestId || currentStreamer !== name;
currentStreamer = name; currentStreamer = name;
renderStreamers(); renderStreamers();
byId('pageTitle').textContent = name; byId('pageTitle').textContent = name;
if (!isConnected) { if (!isConnected) {
await connect(); await connect();
if (isStaleRequest()) {
return;
}
} }
if (!isConnected) { if (!isConnected) {
@ -65,12 +73,20 @@ async function selectStreamer(name: string, forceRefresh = false): Promise<void>
byId('vodGrid').innerHTML = `<div class="empty-state"><p>${UI_TEXT.vods.loading}</p></div>`; byId('vodGrid').innerHTML = `<div class="empty-state"><p>${UI_TEXT.vods.loading}</p></div>`;
const userId = await window.api.getUserId(name); const userId = await window.api.getUserId(name);
if (isStaleRequest()) {
return;
}
if (!userId) { if (!userId) {
byId('vodGrid').innerHTML = `<div class="empty-state"><h3>${UI_TEXT.vods.notFound}</h3></div>`; byId('vodGrid').innerHTML = `<div class="empty-state"><h3>${UI_TEXT.vods.notFound}</h3></div>`;
return; return;
} }
const vods = await window.api.getVODs(userId, forceRefresh); const vods = await window.api.getVODs(userId, forceRefresh);
if (isStaleRequest()) {
return;
}
renderVODs(vods, name); renderVODs(vods, name);
} }