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 = ``;
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);
}