add ffmpeg

This commit is contained in:
Julian Freeman
2025-12-08 17:16:46 -04:00
parent 14b0e96c7d
commit ac00c54ca3
6 changed files with 242 additions and 2 deletions

View File

@@ -15,6 +15,9 @@ use crate::storage::{self};
const YT_DLP_REPO_URL: &str = "https://github.com/yt-dlp/yt-dlp/releases/latest/download";
// Bellard's QuickJS binary releases URL
const QJS_REPO_URL: &str = "https://bellard.org/quickjs/binary_releases";
// FFmpeg builds
const FFMPEG_GITHUB_API: &str = "https://api.github.com/repos/BtbN/FFmpeg-Builds/releases/latest";
const FFMPEG_EVERMEET_BASE: &str = "https://evermeet.cx/ffmpeg";
pub fn get_ytdlp_binary_name() -> &'static str {
if cfg!(target_os = "windows") {
@@ -62,10 +65,23 @@ pub fn get_qjs_path(app: &AppHandle) -> Result<PathBuf> {
Ok(get_bin_dir(app)?.join(get_qjs_binary_name()))
}
pub fn get_ffmpeg_binary_name() -> &'static str {
if cfg!(target_os = "windows") {
"ffmpeg.exe"
} else {
"ffmpeg"
}
}
pub fn get_ffmpeg_path(app: &AppHandle) -> Result<PathBuf> {
Ok(get_bin_dir(app)?.join(get_ffmpeg_binary_name()))
}
pub fn check_binaries(app: &AppHandle) -> bool {
let ytdlp = get_ytdlp_path(app).map(|p| p.exists()).unwrap_or(false);
let qjs = get_qjs_path(app).map(|p| p.exists()).unwrap_or(false);
ytdlp && qjs
let ffmpeg = get_ffmpeg_path(app).map(|p| p.exists()).unwrap_or(false);
ytdlp && qjs && ffmpeg
}
// --- yt-dlp Logic ---
@@ -272,6 +288,175 @@ pub async fn update_qjs(app: &AppHandle) -> Result<String> {
Ok("QuickJS 已更新/安装".to_string())
}
// --- FFmpeg Logic ---
pub async fn download_ffmpeg(app: &AppHandle) -> Result<PathBuf> {
let bin_dir = get_bin_dir(app)?;
if cfg!(target_os = "windows") {
// Query GitHub releases API to find a suitable win64 zip asset
let client = reqwest::Client::new();
let resp = client.get(FFMPEG_GITHUB_API)
.header(reqwest::header::USER_AGENT, "stream-capture")
.send()
.await?;
if !resp.status().is_success() {
return Err(anyhow!("无法获取 FFmpeg releases 信息"));
}
let json: serde_json::Value = resp.json().await?;
let mut download_url: Option<String> = None;
if let Some(assets) = json.get("assets").and_then(|a| a.as_array()) {
for asset in assets {
if let (Some(name), Some(url)) = (asset.get("name").and_then(|n| n.as_str()), asset.get("browser_download_url").and_then(|u| u.as_str())) {
let lname = name.to_lowercase();
if lname.contains("win64") && lname.ends_with(".zip") {
download_url = Some(url.to_string());
break;
}
}
}
if download_url.is_none() {
// fallback: choose first zip asset
for asset in assets {
if let (Some(url), Some(name)) = (asset.get("browser_download_url").and_then(|u| u.as_str()), asset.get("name").and_then(|n| n.as_str())) {
if name.to_lowercase().ends_with(".zip") {
download_url = Some(url.to_string());
break;
}
}
}
}
}
let url = download_url.ok_or(anyhow!("未找到适合 Windows 的 FFmpeg 发行包"))?;
let resp = client.get(&url).header(reqwest::header::USER_AGENT, "stream-capture").send().await?;
if !resp.status().is_success() {
return Err(anyhow!("下载 FFmpeg 失败"));
}
let bytes = resp.bytes().await?;
let mut archive = ZipArchive::new(Cursor::new(bytes))?;
let mut found = false;
for i in 0..archive.len() {
let mut file = archive.by_index(i)?;
if file.is_dir() { continue; }
let name = file.name().to_string();
let filename_only = name.split('/').last().unwrap_or("");
if filename_only.eq_ignore_ascii_case("ffmpeg.exe") || filename_only.ends_with(".dll") {
let mut out_file = fs::File::create(bin_dir.join(filename_only))?;
std::io::copy(&mut file, &mut out_file)?;
found = true;
}
}
if !found {
return Err(anyhow!("在 FFmpeg 压缩包中未找到 ffmpeg 可执行文件"));
}
let final_path = get_ffmpeg_path(app)?;
Ok(final_path)
} else if cfg!(target_os = "macos") {
// Fetch listing page and find latest ffmpeg-*.zip
let resp = reqwest::get(FFMPEG_EVERMEET_BASE).await?;
if !resp.status().is_success() {
return Err(anyhow!("无法获取 evermeet.ffmpeg 列表"));
}
let body = resp.text().await?;
let re = regex::Regex::new(r#"href="(ffmpeg-[0-9][^"]*\.zip)"#)?;
let mut candidates: Vec<&str> = Vec::new();
for cap in re.captures_iter(&body) {
if let Some(m) = cap.get(1) {
candidates.push(m.as_str());
}
}
let filename = candidates.last().ok_or(anyhow!("未在 evermeet 找到 ffmpeg zip 文件"))?;
let url = format!("{}/{}", FFMPEG_EVERMEET_BASE, filename);
let resp = reqwest::get(&url).await?;
if !resp.status().is_success() {
return Err(anyhow!("下载 FFmpeg macOS 版本失败"));
}
let bytes = resp.bytes().await?;
let mut archive = ZipArchive::new(Cursor::new(bytes))?;
let mut found = false;
for i in 0..archive.len() {
let mut file = archive.by_index(i)?;
if file.is_dir() { continue; }
let name = file.name().to_string();
let filename_only = name.split('/').last().unwrap_or("");
if filename_only == "ffmpeg" {
let mut out_file = fs::File::create(bin_dir.join("ffmpeg"))?;
std::io::copy(&mut file, &mut out_file)?;
found = true;
}
}
if !found {
return Err(anyhow!("在 FFmpeg 压缩包中未找到 ffmpeg 可执行文件"));
}
let final_path = get_ffmpeg_path(app)?;
#[cfg(target_family = "unix")]
{
let mut perms = fs::metadata(&final_path)?.permissions();
perms.set_mode(0o755);
fs::set_permissions(&final_path, perms)?;
}
#[cfg(target_os = "macos")]
{
std::process::Command::new("xattr").arg("-d").arg("com.apple.quarantine").arg(&final_path).output().ok();
}
Ok(final_path)
} else {
Err(anyhow!("当前操作系统不支持自动下载 FFmpeg"))
}
}
pub async fn update_ffmpeg(app: &AppHandle) -> Result<String> {
let path = get_ffmpeg_path(app)?;
if !path.exists() {
download_ffmpeg(app).await?;
return Ok("FFmpeg 已安装".to_string());
}
// Re-download to update
download_ffmpeg(app).await?;
// Update settings timestamp
let mut settings = storage::load_settings(app)?;
settings.last_updated = Some(chrono::Utc::now());
storage::save_settings(app, &settings)?;
Ok("FFmpeg 已更新".to_string())
}
pub fn get_ffmpeg_version(app: &AppHandle) -> Result<String> {
let path = get_ffmpeg_path(app)?;
if !path.exists() {
return Ok("未安装".to_string());
}
let mut cmd = std::process::Command::new(&path);
cmd.arg("-version");
#[cfg(target_os = "windows")]
cmd.creation_flags(0x08000000);
let output = cmd.output()?;
if output.status.success() {
// Prefer stdout, fallback to stderr if stdout empty
let out = if !output.stdout.is_empty() {
String::from_utf8_lossy(&output.stdout).to_string()
} else {
String::from_utf8_lossy(&output.stderr).to_string()
};
if let Some(first_line) = out.lines().next() {
let v = first_line.trim().to_string();
if !v.is_empty() {
return Ok(v);
}
}
}
// If we couldn't obtain a usable version string, treat as not installed
Ok("未安装".to_string())
}
pub fn get_qjs_version(app: &AppHandle) -> Result<String> {
let path = get_qjs_path(app)?;
if !path.exists() {

View File

@@ -29,6 +29,11 @@ pub async fn update_quickjs(app: AppHandle) -> Result<String, String> {
binary_manager::update_qjs(&app).await.map_err(|e| e.to_string())
}
#[tauri::command]
pub async fn update_ffmpeg(app: AppHandle) -> Result<String, String> {
binary_manager::update_ffmpeg(&app).await.map_err(|e| e.to_string())
}
#[tauri::command]
pub fn get_ytdlp_version(app: AppHandle) -> Result<String, String> {
binary_manager::get_ytdlp_version(&app).map_err(|e| e.to_string())
@@ -39,6 +44,11 @@ pub fn get_quickjs_version(app: AppHandle) -> Result<String, String> {
binary_manager::get_qjs_version(&app).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn get_ffmpeg_version(app: AppHandle) -> Result<String, String> {
binary_manager::get_ffmpeg_version(&app).map_err(|e| e.to_string())
}
#[tauri::command]
pub async fn fetch_metadata(app: AppHandle, url: String, parse_mix_playlist: bool) -> Result<downloader::MetadataResult, String> {
downloader::fetch_metadata(&app, &url, parse_mix_playlist).await.map_err(|e| e.to_string())

View File

@@ -166,6 +166,7 @@ pub async fn download_video(
) -> Result<String> {
let ytdlp_path = binary_manager::get_ytdlp_path(&app)?;
let qjs_path = binary_manager::get_qjs_path(&app)?; // Get absolute path to quickjs
let ffmpeg_path = binary_manager::get_ffmpeg_path(&app)?; // Get absolute path to ffmpeg
let mut args = Vec::new();
@@ -173,6 +174,9 @@ pub async fn download_video(
args.push("--js-runtimes".to_string());
// Rust's Command automatically handles spaces in arguments, so we should NOT quote the path here.
args.push(format!("quickjs:{}", qjs_path.to_string_lossy()));
// Pass ffmpeg location so yt-dlp can find our managed ffmpeg
args.push("--ffmpeg-location".to_string());
args.push(ffmpeg_path.to_string_lossy().to_string());
args.push(url);

View File

@@ -13,8 +13,10 @@ pub fn run() {
commands::init_ytdlp,
commands::update_ytdlp,
commands::update_quickjs,
commands::update_ffmpeg,
commands::get_ytdlp_version,
commands::get_quickjs_version,
commands::get_ffmpeg_version,
commands::fetch_metadata,
commands::start_download,
commands::get_settings,