From 50ad908f1ef5c58b45cceda2998ab5d5d5b5ec12 Mon Sep 17 00:00:00 2001 From: vincent Date: Sat, 29 Aug 2026 11:52:33 +0200 Subject: [PATCH] perf: cache prereq versions and resolved stream URLs The bundled yt-dlp is a PyInstaller one-file binary that unpacks ~37MB on every invocation, costing about eight seconds a call on this machine regardless of signing, xattrs or thinning the universal binary. Homebrew's copy is a Python script and starts instantly, so bundling traded startup speed for self-containment. Softens it where possible: tool versions are read once at startup in the background rather than on every Settings open, and resolved stream URLs are cached for three hours (well inside YouTube's signed-URL lifetime) so replaying a video costs nothing. A first play still pays the startup. --- src-tauri/src/commands.rs | 56 ++++++++++++++++++++++++++++++++++----- src-tauri/src/lib.rs | 9 +++++++ 2 files changed, 59 insertions(+), 6 deletions(-) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 570c5d3..681d843 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -37,8 +37,17 @@ pub struct AppState { pub children: Arc>>, pub download_slots: Arc, pub playlists: PlaylistServer, + /// yt-dlp's PyInstaller bundle costs several seconds to start, so its + /// version is read once and kept. + pub prereqs: Arc>>, + /// Resolved stream URLs, keyed by video and quality cap. YouTube's signed + /// URLs last hours, so replaying a video should not pay for yt-dlp again. + pub streams: Arc), (String, std::time::Instant)>>>, } +/// Comfortably inside the ~6h lifetime of YouTube's signed URLs. +const STREAM_CACHE_TTL: std::time::Duration = std::time::Duration::from_secs(3 * 3600); + #[derive(Clone, Serialize)] struct RefreshProgress { done: usize, @@ -140,8 +149,16 @@ fn label(version: &str, name: &str) -> String { #[tauri::command] pub async fn check_prereqs(state: State<'_, AppState>) -> Result { + // The library path can change, so only the tool versions are cached. let library = state.library.lock().await.clone(); - Ok(Prereqs { + if let Some(cached) = state.prereqs.lock().await.clone() { + return Ok(Prereqs { + library_path: library.to_string_lossy().to_string(), + ..cached + }); + } + + let fresh = Prereqs { yt_dlp: version_of("yt-dlp", "--version").await.map(|v| label(&v, "yt-dlp")), ffmpeg: version_of("ffmpeg", "-version").await.map(|v| { // ffmpeg's first line is long; keep the useful head of it. @@ -149,7 +166,9 @@ pub async fn check_prereqs(state: State<'_, AppState>) -> Result Result, String> { @@ -227,6 +246,15 @@ pub async fn resolve_stream( max_height: Option, state: State<'_, AppState>, ) -> Result { + let key = (video_id.clone(), max_height); + { + let mut cache = state.streams.lock().await; + cache.retain(|_, (_, at)| at.elapsed() < STREAM_CACHE_TTL); + if let Some((cached, _)) = cache.get(&key) { + return Ok(Stream { url: Some(cached.clone()), playlist: None }); + } + } + let url = format!("https://www.youtube.com/watch?v={video_id}"); // The HLS master playlist. Every m3u8 format shares the same manifest_url, @@ -237,6 +265,7 @@ pub async fn resolve_stream( .await { let Some(cap) = max_height else { + remember(&state, key, &master).await; return Ok(Stream { url: Some(master), playlist: None }); }; // Fetch and cut it down. If anything about that fails, the adaptive @@ -244,10 +273,11 @@ pub async fn resolve_stream( return match state.http.get(&master).send().await { Ok(resp) => match resp.text().await { Ok(body) => Ok(match filter_master_playlist(&body, cap) { - Some(filtered) => Stream { - url: Some(state.playlists.publish(filtered).await), - playlist: None, - }, + Some(filtered) => { + let served = state.playlists.publish(filtered).await; + remember(&state, key, &served).await; + Stream { url: Some(served), playlist: None } + } None => Stream { url: Some(master), playlist: None }, }), Err(_) => Ok(Stream { url: Some(master), playlist: None }), @@ -272,6 +302,18 @@ pub async fn resolve_stream( Err("Could not find a playable stream for this video.".into()) } +async fn remember( + state: &State<'_, AppState>, + key: (String, Option), + url: &str, +) { + state + .streams + .lock() + .await + .insert(key, (url.to_string(), std::time::Instant::now())); +} + /// Keeps only the highest video variant at or below `max_height`, along with /// every `EXT-X-MEDIA` line (the audio and subtitle groups it references). /// @@ -766,6 +808,8 @@ pub fn build_state(app: &AppHandle) -> Result { children: Arc::new(Mutex::new(HashMap::new())), download_slots: Arc::new(Semaphore::new(DOWNLOAD_CONCURRENCY)), playlists: PlaylistServer::start()?, + prereqs: Arc::new(Mutex::new(None)), + streams: Arc::new(Mutex::new(HashMap::new())), }) } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 59e4d1d..a60deee 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -46,6 +46,15 @@ pub fn run() { enable_element_fullscreen(&window); } + // The bundled yt-dlp takes seconds to start, so pay that once in + // the background rather than the first time Settings is opened. + let handle = app.handle().clone(); + tauri::async_runtime::spawn(async move { + use tauri::Manager; + let state = handle.state::(); + let _ = commands::check_prereqs(state).await; + }); + Ok(()) }) .invoke_handler(tauri::generate_handler![