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.
This commit is contained in:
vincent
2026-08-29 11:52:33 +02:00
parent 3296b5436a
commit 50ad908f1e
2 changed files with 59 additions and 6 deletions
+50 -6
View File
@@ -37,8 +37,17 @@ pub struct AppState {
pub children: Arc<Mutex<HashMap<String, tokio::process::Child>>>,
pub download_slots: Arc<Semaphore>,
pub playlists: PlaylistServer,
/// yt-dlp's PyInstaller bundle costs several seconds to start, so its
/// version is read once and kept.
pub prereqs: Arc<Mutex<Option<Prereqs>>>,
/// 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<Mutex<HashMap<(String, Option<u32>), (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<Prereqs, String> {
// 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<Prereqs, String
label(&head, "ffmpeg")
}),
library_path: library.to_string_lossy().to_string(),
})
};
*state.prereqs.lock().await = Some(fresh.clone());
Ok(fresh)
}
async fn read_channels(path: &str) -> Result<Vec<Channel>, String> {
@@ -227,6 +246,15 @@ pub async fn resolve_stream(
max_height: Option<u32>,
state: State<'_, AppState>,
) -> Result<Stream, String> {
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<u32>),
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<AppState, String> {
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())),
})
}