diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 783fae5..59b3869 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1435,27 +1435,10 @@ async fn test_ytdlp(app_handle: tauri::AppHandle) -> Result { #[tauri::command] async fn test_ffmpeg(app_handle: tauri::AppHandle) -> Result { let (version, error, _) = run_sidecar_version(&app_handle, "ffmpeg", &["-version"]).await; - if let Some(text) = version { - let first_line = text.lines().next().unwrap_or(""); - let re = regex::Regex::new(r"(?i)version\s+([\d\.]+)").unwrap(); - let clean = re - .captures(first_line) - .and_then(|c| c.get(1)) - .map(|m| m.as_str().to_string()) - .unwrap_or_else(|| { - let parts: Vec<&str> = first_line.split_whitespace().collect(); - parts - .get(2) - .unwrap_or(&first_line) - .split('-') - .next() - .unwrap_or("") - .to_string() - }); - Ok(clean) - } else { - Err(error.unwrap_or_else(|| "ffmpeg returned no version output".to_string())) - } + version + .as_deref() + .and_then(parse_ffmpeg_version) + .ok_or_else(|| error.unwrap_or_else(|| "ffmpeg returned no version output".to_string())) } #[tauri::command] @@ -2327,19 +2310,9 @@ async fn check_ffmpeg(app_handle: &tauri::AppHandle) -> EngineStatusItem { let (version_raw, run_error, stderr_tail) = run_sidecar_version(app_handle, sidecar_name, &["-version"]).await; - let version = version_raw.as_ref().and_then(|text| { - text.lines().next().and_then(|first| { - let re = regex::Regex::new(r"(?i)version\s+([\d\.]+)").unwrap(); - if let Some(caps) = re.captures(first) { - caps.get(1).map(|m| m.as_str().to_string()) - } else { - let parts: Vec<&str> = first.split_whitespace().collect(); - parts - .get(2) - .map(|v| v.split('-').next().unwrap_or(v).to_string()) - } - }) - }); + let version = version_raw + .as_ref() + .and_then(|text| parse_ffmpeg_version(text)); let error = run_error; let remediation_hint = error @@ -2366,6 +2339,35 @@ async fn check_ffmpeg(app_handle: &tauri::AppHandle) -> EngineStatusItem { } } +fn parse_ffmpeg_version(output: &str) -> Option { + let first = output.lines().next()?.trim(); + let token = first + .split_whitespace() + .collect::>() + .windows(2) + .find_map(|window| { + window[0] + .eq_ignore_ascii_case("version") + .then_some(window[1]) + })?; + + let without_url = token + .split_once("-https://") + .or_else(|| token.split_once("-http://")) + .map(|(prefix, _)| prefix) + .unwrap_or(token); + + if without_url.starts_with("N-") { + return Some(without_url.to_string()); + } + + without_url + .split('-') + .next() + .filter(|version| !version.trim().is_empty()) + .map(str::to_string) +} + async fn check_deno(app_handle: &tauri::AppHandle) -> EngineStatusItem { let sidecar_name = "deno"; let expected_sidecar = crate::platform::engine_binary_name(sidecar_name); @@ -4303,7 +4305,7 @@ mod tests { filename_from_content_disposition, filename_from_url_disposition_query, filename_from_url_path, is_excluded_yt_dlp_format, json_lower, media_output_template, media_progress_speed, normalize_speed_limit_for_aria2, parse_firelink_deep_link, - parse_media_progress_line, redact_log_line, FirelinkDeepLink, MediaProgress, + parse_ffmpeg_version, parse_media_progress_line, redact_log_line, FirelinkDeepLink, MediaProgress, MEDIA_PROGRESS_PREFIX, }; use serde_json::json; @@ -4616,6 +4618,23 @@ mod tests { ); } + #[test] + fn parses_ffmpeg_snapshot_version_without_collapsing_to_n() { + let output = "ffmpeg version N-125385-ge2e889d9da-https://www.martin-riedl.de Copyright (c) 2000-2026 the FFmpeg developers"; + + assert_eq!( + parse_ffmpeg_version(output), + Some("N-125385-ge2e889d9da".to_string()) + ); + } + + #[test] + fn parses_ffmpeg_release_version_without_build_suffix() { + let output = "ffmpeg version 8.1.2-static https://example.invalid Copyright (c) 2000-2026 the FFmpeg developers"; + + assert_eq!(parse_ffmpeg_version(output), Some("8.1.2".to_string())); + } + #[test] fn uses_fragment_progress_instead_of_temporary_hls_size_estimates() { let line = format!(