Optimize tool path caching and streamer load race handling (v4.1.4)
This commit is contained in:
parent
014c8ecf74
commit
7412e7aa16
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": "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",
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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');
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user