use std::path::PathBuf; use std::process::Command; #[cfg(target_os = "windows")] use std::os::windows::process::CommandExt; /// Extract audio from a video file to a WAV file using ffmpeg. /// Returns the path to the extracted audio file. #[tauri::command] pub fn extract_audio(file_path: String, output_path: Option) -> Result { let input = PathBuf::from(&file_path); if !input.exists() { return Err(format!("File not found: {}", file_path)); } // Use provided output path, or fall back to a temp WAV file let stem = input.file_stem().unwrap_or_default().to_string_lossy(); let output = match output_path { Some(ref p) => PathBuf::from(p), None => std::env::temp_dir().join(format!("{stem}_audio.wav")), }; eprintln!( "[media] Extracting audio: {} -> {}", input.display(), output.display() ); // Find ffmpeg — check sidecar extract dir first, then system PATH let ffmpeg = find_ffmpeg().ok_or("ffmpeg not found. Install ffmpeg or ensure it's in PATH.")?; let mut cmd = Command::new(&ffmpeg); cmd.args([ "-y", // Overwrite output "-i", &file_path, "-vn", // No video "-acodec", "pcm_s16le", // WAV PCM 16-bit "-ar", "22050", // 22kHz mono for better playback quality "-ac", "1", // Mono ]) .arg(output.to_str().unwrap()) .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::piped()); // Hide the console window on Windows (CREATE_NO_WINDOW = 0x08000000) #[cfg(target_os = "windows")] cmd.creation_flags(0x08000000); let status = match cmd.status() { Ok(s) => s, Err(e) if e.raw_os_error() == Some(13) => { // Permission denied — fix permissions and retry eprintln!("[media] Permission denied on ffmpeg, fixing permissions and retrying..."); #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; if let Ok(meta) = std::fs::metadata(&ffmpeg) { let mut perms = meta.permissions(); perms.set_mode(0o755); let _ = std::fs::set_permissions(&ffmpeg, perms); } // Also fix ffprobe if it exists let ffprobe = ffmpeg.replace("ffmpeg", "ffprobe"); if let Ok(meta) = std::fs::metadata(&ffprobe) { let mut perms = meta.permissions(); perms.set_mode(0o755); let _ = std::fs::set_permissions(&ffprobe, perms); } } Command::new(&ffmpeg) .args(["-y", "-i", &file_path, "-vn", "-acodec", "pcm_s16le", "-ar", "22050", "-ac", "1"]) .arg(output.to_str().unwrap()) .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::piped()) .status() .map_err(|e| format!("Failed to run ffmpeg after chmod: {e}"))? } Err(e) => return Err(format!("Failed to run ffmpeg: {e}")), }; if !status.success() { return Err(format!("ffmpeg exited with status {status}")); } if !output.exists() { return Err("ffmpeg completed but output file not found".to_string()); } eprintln!("[media] Audio extracted successfully"); Ok(output.to_string_lossy().to_string()) } #[tauri::command] pub fn check_file_exists(path: String) -> bool { std::path::Path::new(&path).exists() } #[tauri::command] pub fn copy_file(src: String, dst: String) -> Result<(), String> { std::fs::copy(&src, &dst).map_err(|e| format!("Failed to copy file: {e}"))?; Ok(()) } #[tauri::command] pub fn create_dir(path: String) -> Result<(), String> { std::fs::create_dir_all(&path).map_err(|e| format!("Failed to create directory: {e}"))?; Ok(()) } /// Find ffmpeg binary — check sidecar directory first, then system PATH. fn find_ffmpeg() -> Option { // Check sidecar extract dir (ffmpeg is bundled with the sidecar) if let Some(data_dir) = crate::sidecar::DATA_DIR.get() { // Read sidecar version to find the right directory let version_file = data_dir.join("sidecar-version.txt"); if let Ok(version) = std::fs::read_to_string(&version_file) { let version = version.trim(); let sidecar_dir = data_dir.join(format!("sidecar-{version}")); let ffmpeg_name = if cfg!(target_os = "windows") { "ffmpeg.exe" } else { "ffmpeg" }; let ffmpeg_path = sidecar_dir.join(ffmpeg_name); if ffmpeg_path.exists() { return Some(ffmpeg_path.to_string_lossy().to_string()); } } } // Fall back to system PATH let ffmpeg_name = if cfg!(target_os = "windows") { "ffmpeg.exe" } else { "ffmpeg" }; if Command::new(ffmpeg_name) .arg("-version") .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) .status() .is_ok() { return Some(ffmpeg_name.to_string()); } None }