add ffmpeg
This commit is contained in:
@@ -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() {
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user