use std::fs; use std::io::Write; use std::path::{Path, PathBuf}; use tokio::process::Command; use tokio_stream::StreamExt; const FFMPEG_VERSION: &str = "7.1"; #[derive(Debug, Clone)] pub struct FFmpegInstaller; impl FFmpegInstaller { pub fn new() -> Self { Self } /// 获取 FFmpeg 存储路径 pub fn get_ffmpeg_path() -> PathBuf { let app_dir = Self::get_app_data_dir(); #[cfg(target_os = "windows")] let ffmpeg_name = "ffmpeg.exe"; #[cfg(not(target_os = "windows"))] let ffmpeg_name = "ffmpeg"; app_dir.join(ffmpeg_name) } /// 获取应用数据目录 fn get_app_data_dir() -> PathBuf { let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from(".")); #[cfg(target_os = "macos")] let app_dir = home.join("Library/Application Support/FormatConverter"); #[cfg(target_os = "windows")] let app_dir = home.join("AppData/Local/FormatConverter"); #[cfg(target_os = "linux")] let app_dir = home.join(".local/share/format-converter"); fs::create_dir_all(&app_dir).ok(); app_dir } /// 检查 FFmpeg 是否已安装 pub fn is_installed() -> bool { // 1. 检查系统 PATH 中的 FFmpeg if let Ok(output) = std::process::Command::new("which") .arg("ffmpeg") .output() { if output.status.success() && !output.stdout.is_empty() { return true; } } // 2. 检查应用目录 let app_ffmpeg = Self::get_ffmpeg_path(); if app_ffmpeg.exists() { return true; } // 3. 检查系统常见路径 let common_paths = Self::get_system_ffmpeg_paths(); for path in common_paths { if path.exists() { return true; } } false } /// 获取可能的系统 FFmpeg 路径 fn get_system_ffmpeg_paths() -> Vec { let mut paths = Vec::new(); #[cfg(target_os = "macos")] { // Homebrew 常见安装路径 paths.push(PathBuf::from("/opt/homebrew/bin/ffmpeg")); // Apple Silicon paths.push(PathBuf::from("/usr/local/bin/ffmpeg")); // Intel // 其他可能路径 paths.push(PathBuf::from("/usr/bin/ffmpeg")); } #[cfg(target_os = "linux")] { paths.push(PathBuf::from("/usr/bin/ffmpeg")); paths.push(PathBuf::from("/usr/local/bin/ffmpeg")); } #[cfg(target_os = "windows")] { paths.push(PathBuf::from("C:\\ffmpeg\\bin\\ffmpeg.exe")); if let Ok(program_files) = std::env::var("ProgramFiles") { paths.push(PathBuf::from(&format!("{}\\ffmpeg\\bin\\ffmpeg.exe", program_files))); } } paths } /// 获取 FFmpeg 版本 pub async fn get_version() -> Option { // 1. 尝试系统 PATH 中的 ffmpeg if let Ok(output) = tokio::time::timeout( std::time::Duration::from_secs(5), Command::new("ffmpeg").args(["-version"]).output() ).await { if let Ok(output) = output { if output.status.success() { let stdout = String::from_utf8_lossy(&output.stdout); if let Some(first_line) = stdout.lines().next() { return Some(first_line.to_string()); } } } } // 2. 尝试应用目录中的 ffmpeg let ffmpeg_path = Self::get_ffmpeg_path(); if ffmpeg_path.exists() { if let Some(cmd_str) = ffmpeg_path.to_str() { if let Ok(Ok(output)) = tokio::time::timeout( std::time::Duration::from_secs(5), Command::new(cmd_str).args(["-version"]).output() ).await { if output.status.success() { let stdout = String::from_utf8_lossy(&output.stdout); if let Some(first_line) = stdout.lines().next() { return Some(first_line.to_string()); } } } } } // 3. 尝试系统常见路径 let system_paths = Self::get_system_ffmpeg_paths(); for path in system_paths { if path.exists() { if let Some(cmd_str) = path.to_str() { if let Ok(Ok(output)) = tokio::time::timeout( std::time::Duration::from_secs(5), Command::new(cmd_str).args(["-version"]).output() ).await { if output.status.success() { let stdout = String::from_utf8_lossy(&output.stdout); if let Some(first_line) = stdout.lines().next() { return Some(first_line.to_string()); } } } } } } None } /// 下载并安装 FFmpeg pub async fn install(progress_callback: F) -> Result<(), String> where F: Fn(f64, String) + Send + 'static, { let platform = Self::detect_platform()?; let download_url = Self::get_download_url(&platform); progress_callback(0.0, "准备下载 FFmpeg...".to_string()); // 创建临时目录 let temp_dir = std::env::temp_dir().join("format-converter-ffmpeg"); fs::create_dir_all(&temp_dir).map_err(|e: std::io::Error| format!("创建临时目录失败: {}", e))?; // 下载文件 progress_callback(5.0, "正在下载 FFmpeg...".to_string()); let archive_path = temp_dir.join(format!("ffmpeg.{}", platform.extension())); Self::download_file(&download_url, &archive_path, |downloaded, total| { let percent = if total > 0 { (downloaded as f64 / total as f64) * 40.0 + 5.0 } else { 5.0 }; progress_callback(percent, format!("正在下载 FFmpeg... {:.1}%", percent)); }) .await .map_err(|e| format!("下载失败: {}", e))?; // 解压 progress_callback(50.0, "正在解压...".to_string()); Self::extract_archive(&archive_path, &temp_dir, &platform) .await .map_err(|e| format!("解压失败: {}", e))?; // 移动到目标位置 progress_callback(90.0, "正在安装...".to_string()); let ffmpeg_path = Self::get_ffmpeg_path(); let extracted_ffmpeg = Self::find_ffmpeg_binary(&temp_dir, &platform) .ok_or("在解压文件中未找到 FFmpeg 可执行文件")?; fs::copy(&extracted_ffmpeg, &ffmpeg_path) .map_err(|e| format!("复制文件失败: {}", e))?; // 设置可执行权限(Unix) #[cfg(not(target_os = "windows"))] { use std::os::unix::fs::PermissionsExt; let mut perms = fs::metadata(&ffmpeg_path).unwrap().permissions(); perms.set_mode(0o755); fs::set_permissions(&ffmpeg_path, perms).unwrap(); } // 清理临时文件 progress_callback(95.0, "正在清理...".to_string()); fs::remove_dir_all(&temp_dir).ok(); progress_callback(100.0, "FFmpeg 安装完成".to_string()); Ok(()) } /// 检测平台 fn detect_platform() -> Result { #[cfg(target_os = "macos")] { // 检测架构 let arch = std::env::consts::ARCH; match arch { "aarch64" => Ok(Platform::MacOSArm64), "x86_64" => Ok(Platform::MacOSX64), _ => Err(format!("不支持的架构: {}", arch)), } } #[cfg(target_os = "windows")] { let arch = std::env::consts::ARCH; match arch { "x86_64" => Ok(Platform::WindowsX64), "aarch64" => Ok(Platform::WindowsArm64), _ => Err(format!("不支持的架构: {}", arch)), } } #[cfg(target_os = "linux")] { let arch = std::env::consts::ARCH; match arch { "x86_64" => Ok(Platform::LinuxX64), "aarch64" => Ok(Platform::LinuxArm64), _ => Err(format!("不支持的架构: {}", arch)), } } #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] { Err("不支持的操作系统".to_string()) } } /// 获取下载 URL fn get_download_url(platform: &Platform) -> String { match platform { Platform::MacOSArm64 => format!( "https://evermeet.cx/pub/ffmpeg/ffmpeg-{}-macos-arm64.zip", FFMPEG_VERSION ), Platform::MacOSX64 => format!( "https://evermeet.cx/pub/ffmpeg/ffmpeg-{}-macos-x86_64.zip", FFMPEG_VERSION ), Platform::WindowsX64 => format!( "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip" ), Platform::WindowsArm64 => format!( "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip" ), Platform::LinuxX64 => format!( "https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz" ), Platform::LinuxArm64 => format!( "https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-arm64-static.tar.xz" ), } } /// 下载文件 async fn download_file(url: &str, path: &Path, progress: F) -> Result<(), String> where F: Fn(u64, u64), { let client = reqwest::Client::new(); let response = client .get(url) .timeout(std::time::Duration::from_secs(300)) .send() .await .map_err(|e| e.to_string())?; let total_size = response.content_length().unwrap_or(0); let mut file = fs::File::create(path).map_err(|e| e.to_string())?; let mut downloaded: u64 = 0; let mut stream = response.bytes_stream(); while let Some(chunk) = stream.next().await { let chunk = chunk.map_err(|e: reqwest::Error| e.to_string())?; file.write_all(&chunk).map_err(|e: std::io::Error| e.to_string())?; downloaded += chunk.len() as u64; progress(downloaded, total_size); } Ok(()) } /// 解压归档文件 async fn extract_archive( archive_path: &Path, output_dir: &Path, platform: &Platform, ) -> Result<(), String> { match platform.extension().as_str() { "zip" => { let file = fs::File::open(archive_path).map_err(|e| e.to_string())?; let mut archive = zip::ZipArchive::new(file).map_err(|e| e.to_string())?; archive.extract(output_dir).map_err(|e| e.to_string())?; } "tar.xz" => { let file = fs::File::open(archive_path).map_err(|e| e.to_string())?; let tar = xz2::read::XzDecoder::new(file); let mut archive = tar::Archive::new(tar); archive.unpack(output_dir).map_err(|e| e.to_string())?; } _ => return Err("不支持的压缩格式".to_string()), } Ok(()) } /// 查找 FFmpeg 可执行文件 fn find_ffmpeg_binary(dir: &Path, _platform: &Platform) -> Option { #[cfg(target_os = "windows")] let binary_name = "ffmpeg.exe"; #[cfg(not(target_os = "windows"))] let binary_name = "ffmpeg"; for entry in walkdir::WalkDir::new(dir) { if let Ok(entry) = entry { if entry.file_name() == binary_name { return Some(entry.path().to_path_buf()); } } } None } } #[derive(Debug)] enum Platform { MacOSArm64, MacOSX64, WindowsX64, WindowsArm64, LinuxX64, LinuxArm64, } impl Platform { fn extension(&self) -> String { match self { Platform::MacOSArm64 | Platform::MacOSX64 | Platform::WindowsX64 | Platform::WindowsArm64 => "zip".to_string(), Platform::LinuxX64 | Platform::LinuxArm64 => "tar.xz".to_string(), } } }