diff --git a/typescript-version/package-lock.json b/typescript-version/package-lock.json index aefe4d6..1230c26 100644 --- a/typescript-version/package-lock.json +++ b/typescript-version/package-lock.json @@ -1,12 +1,12 @@ { "name": "twitch-vod-manager", - "version": "4.1.3", + "version": "4.1.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "twitch-vod-manager", - "version": "4.1.3", + "version": "4.1.4", "license": "MIT", "dependencies": { "axios": "^1.6.0", diff --git a/typescript-version/package.json b/typescript-version/package.json index b048992..e05339c 100644 --- a/typescript-version/package.json +++ b/typescript-version/package.json @@ -1,6 +1,6 @@ { "name": "twitch-vod-manager", - "version": "4.1.3", + "version": "4.1.4", "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 1ba4aeb..cfb7ae9 100644 --- a/typescript-version/src/index.html +++ b/typescript-version/src/index.html @@ -457,7 +457,7 @@

Updates

-

Version: v4.1.3

+

Version: v4.1.4

@@ -502,7 +502,7 @@
Nicht verbunden - v4.1.3 + v4.1.4 diff --git a/typescript-version/src/main.ts b/typescript-version/src/main.ts index e8d074e..b4a5409 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 = '4.1.3'; +const APP_VERSION = '4.1.4'; const UPDATE_CHECK_URL = 'http://24-music.de/version.json'; // Paths @@ -27,6 +27,7 @@ const DEFAULT_METADATA_CACHE_MINUTES = 10; const DEFAULT_PERFORMANCE_MODE: PerformanceMode = 'balanced'; const QUEUE_SAVE_DEBOUNCE_MS = 250; const MIN_FREE_DISK_BYTES = 128 * 1024 * 1024; +const TOOL_PATH_REFRESH_TTL_MS = 10 * 1000; const CACHE_CLEANUP_INTERVAL_MS = 60 * 1000; const MAX_LOGIN_TO_USER_ID_CACHE_ENTRIES = 4096; 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 bundledFFmpegPath: 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 // ========================================== function getStreamlinkPath(): string { + if (streamlinkPathCache) { + if (streamlinkPathCache === 'streamlink' || fs.existsSync(streamlinkPathCache)) { + return streamlinkPathCache; + } + streamlinkPathCache = null; + } + if (bundledStreamlinkPath && fs.existsSync(bundledStreamlinkPath)) { - return bundledStreamlinkPath; + streamlinkPathCache = bundledStreamlinkPath; + return streamlinkPathCache; } try { if (process.platform === 'win32') { const result = execSync('where streamlink', { encoding: 'utf-8' }); 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 { const result = execSync('which streamlink', { encoding: 'utf-8' }); - return result.trim(); + streamlinkPathCache = result.trim(); + return streamlinkPathCache; } } catch { } @@ -400,10 +418,14 @@ function getStreamlinkPath(): string { ]; 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 { @@ -450,7 +472,7 @@ async function runPreflight(autoFix = false): Promise { if (autoFix) { await ensureStreamlinkInstalled(); await ensureFfmpegInstalled(); - refreshBundledToolPaths(); + refreshBundledToolPaths(true); } const streamlinkCmd = getStreamlinkCommand(); @@ -531,10 +553,44 @@ function findFileRecursive(rootDir: string, fileName: string): string | null { return null; } -function refreshBundledToolPaths(): void { - bundledStreamlinkPath = findFileRecursive(TOOLS_STREAMLINK_DIR, process.platform === 'win32' ? 'streamlink.exe' : 'streamlink'); - bundledFFmpegPath = findFileRecursive(TOOLS_FFMPEG_DIR, process.platform === 'win32' ? 'ffmpeg.exe' : 'ffmpeg'); - bundledFFprobePath = findFileRecursive(TOOLS_FFMPEG_DIR, process.platform === 'win32' ? 'ffprobe.exe' : 'ffprobe'); +function getDirectoryMtimeMs(directoryPath: string): number { + try { + return fs.statSync(directoryPath).mtimeMs; + } 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 { @@ -634,7 +690,7 @@ async function ensureStreamlinkInstalled(): Promise { try { fs.unlinkSync(zipPath); } catch { } if (!extractOk) return false; - refreshBundledToolPaths(); + refreshBundledToolPaths(true); streamlinkCommandCache = null; const cmd = getStreamlinkCommand(); @@ -675,7 +731,7 @@ async function ensureFfmpegInstalled(): Promise { try { fs.unlinkSync(zipPath); } catch { } if (!extractOk) return false; - refreshBundledToolPaths(); + refreshBundledToolPaths(true); const newFfmpegPath = getFFmpegPath(); const newFfprobePath = getFFprobePath(); @@ -726,18 +782,30 @@ function getStreamlinkCommand(): { command: string; prefixArgs: string[] } { } function getFFmpegPath(): string { + if (ffmpegPathCache) { + if (ffmpegPathCache === 'ffmpeg' || fs.existsSync(ffmpegPathCache)) { + return ffmpegPathCache; + } + ffmpegPathCache = null; + } + if (bundledFFmpegPath && fs.existsSync(bundledFFmpegPath)) { - return bundledFFmpegPath; + ffmpegPathCache = bundledFFmpegPath; + return ffmpegPathCache; } try { if (process.platform === 'win32') { const result = execSync('where ffmpeg', { encoding: 'utf-8' }); 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 { const result = execSync('which ffmpeg', { encoding: 'utf-8' }); - return result.trim(); + ffmpegPathCache = result.trim(); + return ffmpegPathCache; } } catch { } @@ -748,20 +816,45 @@ function getFFmpegPath(): string { ]; 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 { + if (ffprobePathCache) { + if (ffprobePathCache === 'ffprobe' || ffprobePathCache === 'ffprobe.exe' || fs.existsSync(ffprobePathCache)) { + return ffprobePathCache; + } + ffprobePathCache = null; + } + if (bundledFFprobePath && fs.existsSync(bundledFFprobePath)) { - return bundledFFprobePath; + ffprobePathCache = bundledFFprobePath; + return ffprobePathCache; } const ffmpegPath = getFFmpegPath(); 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 { @@ -3038,7 +3131,7 @@ ipcMain.handle('save-video-dialog', async (_, defaultName: string) => { // APP LIFECYCLE // ========================================== app.whenReady().then(() => { - refreshBundledToolPaths(); + refreshBundledToolPaths(true); startMetadataCacheCleanup(); createWindow(); appendDebugLog('startup-tools-check-skipped', 'Deferred to first use'); diff --git a/typescript-version/src/renderer-streamers.ts b/typescript-version/src/renderer-streamers.ts index 9c1b5ea..05203f5 100644 --- a/typescript-version/src/renderer-streamers.ts +++ b/typescript-version/src/renderer-streamers.ts @@ -1,3 +1,5 @@ +let selectStreamerRequestId = 0; + function renderStreamers(): void { const list = byId('streamerList'); list.innerHTML = ''; @@ -50,12 +52,18 @@ async function removeStreamer(name: string): Promise { } async function selectStreamer(name: string, forceRefresh = false): Promise { + const requestId = ++selectStreamerRequestId; + const isStaleRequest = () => requestId !== selectStreamerRequestId || currentStreamer !== name; + currentStreamer = name; renderStreamers(); byId('pageTitle').textContent = name; if (!isConnected) { await connect(); + if (isStaleRequest()) { + return; + } } if (!isConnected) { @@ -65,12 +73,20 @@ async function selectStreamer(name: string, forceRefresh = false): Promise byId('vodGrid').innerHTML = `

${UI_TEXT.vods.loading}

`; const userId = await window.api.getUserId(name); + if (isStaleRequest()) { + return; + } + if (!userId) { byId('vodGrid').innerHTML = `

${UI_TEXT.vods.notFound}

`; return; } const vods = await window.api.getVODs(userId, forceRefresh); + if (isStaleRequest()) { + return; + } + renderVODs(vods, name); }