681 lines
22 KiB
Rust
681 lines
22 KiB
Rust
// filepath: src-tauri/src/binary_manager.rs
|
|
use std::fs;
|
|
use std::path::PathBuf;
|
|
use tauri::AppHandle;
|
|
use anyhow::{Result, anyhow};
|
|
#[cfg(target_family = "unix")]
|
|
use std::os::unix::fs::PermissionsExt;
|
|
#[cfg(target_os = "windows")]
|
|
use std::os::windows::process::CommandExt;
|
|
use zip::ZipArchive;
|
|
use std::io::Cursor;
|
|
|
|
use crate::process_utils::first_non_empty_line;
|
|
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";
|
|
|
|
#[derive(serde::Serialize, Clone, Debug)]
|
|
pub struct RuntimeStatus {
|
|
pub ffmpeg_source: String,
|
|
pub ffmpeg_version: String,
|
|
pub js_runtime_name: String,
|
|
pub js_runtime_source: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub enum FfmpegLocation {
|
|
System,
|
|
Managed(PathBuf),
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub enum JsRuntime {
|
|
Deno,
|
|
Node,
|
|
ManagedQuickJs(PathBuf),
|
|
}
|
|
|
|
pub fn get_ytdlp_binary_name() -> &'static str {
|
|
if cfg!(target_os = "windows") {
|
|
"yt-dlp.exe"
|
|
} else if cfg!(target_os = "macos") {
|
|
"yt-dlp_macos"
|
|
} else {
|
|
"yt-dlp"
|
|
}
|
|
}
|
|
|
|
// Target name on disk (for yt-dlp usage)
|
|
pub fn get_qjs_binary_name() -> &'static str {
|
|
if cfg!(target_os = "windows") {
|
|
"quickjs.exe"
|
|
} else {
|
|
"quickjs"
|
|
}
|
|
}
|
|
|
|
// Source name inside the zip archive (standard QuickJS naming)
|
|
fn get_qjs_source_name_in_zip() -> &'static str {
|
|
if cfg!(target_os = "windows") {
|
|
"qjs.exe"
|
|
} else {
|
|
"qjs"
|
|
}
|
|
}
|
|
|
|
// Get base directory for all binaries
|
|
pub fn get_bin_dir(app: &AppHandle) -> Result<PathBuf> {
|
|
let app_data = storage::get_app_data_dir(app)?;
|
|
let bin_dir = app_data.join("bin");
|
|
if !bin_dir.exists() {
|
|
fs::create_dir_all(&bin_dir)?;
|
|
}
|
|
Ok(bin_dir)
|
|
}
|
|
|
|
pub fn get_ytdlp_path(app: &AppHandle) -> Result<PathBuf> {
|
|
Ok(get_bin_dir(app)?.join(get_ytdlp_binary_name()))
|
|
}
|
|
|
|
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 {
|
|
get_ytdlp_path(app).map(|p| p.exists()).unwrap_or(false)
|
|
}
|
|
|
|
// --- yt-dlp Logic ---
|
|
|
|
pub async fn download_ytdlp(app: &AppHandle) -> Result<PathBuf> {
|
|
let binary_name = get_ytdlp_binary_name();
|
|
let url = format!("{}/{}", YT_DLP_REPO_URL, binary_name);
|
|
|
|
let response = reqwest::get(&url).await?;
|
|
if !response.status().is_success() {
|
|
return Err(anyhow!("下载 yt-dlp 失败:状态 {}", response.status()));
|
|
}
|
|
|
|
let bytes = response.bytes().await?;
|
|
let path = get_ytdlp_path(app)?;
|
|
|
|
if let Some(parent) = path.parent() {
|
|
fs::create_dir_all(parent)?;
|
|
}
|
|
|
|
fs::write(&path, bytes)?;
|
|
|
|
#[cfg(target_family = "unix")]
|
|
{
|
|
let mut perms = fs::metadata(&path)?.permissions();
|
|
perms.set_mode(0o755);
|
|
fs::set_permissions(&path, perms)?;
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
// Remove quarantine attribute to allow execution on macOS
|
|
std::process::Command::new("xattr")
|
|
.arg("-d")
|
|
.arg("com.apple.quarantine")
|
|
.arg(&path)
|
|
.output()
|
|
.ok();
|
|
}
|
|
|
|
Ok(path)
|
|
}
|
|
|
|
pub async fn update_ytdlp(app: &AppHandle) -> Result<String> {
|
|
let path = get_ytdlp_path(app)?;
|
|
if !path.exists() {
|
|
download_ytdlp(app).await?;
|
|
return Ok("yt-dlp 已全新下载".to_string());
|
|
}
|
|
|
|
// Use built-in update for yt-dlp
|
|
let mut cmd = std::process::Command::new(&path);
|
|
cmd.arg("-U");
|
|
|
|
#[cfg(target_os = "windows")]
|
|
cmd.creation_flags(0x08000000);
|
|
|
|
let output = cmd.output()?;
|
|
|
|
if !output.status.success() {
|
|
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
|
return Err(anyhow!("yt-dlp 更新失败:{}", stderr));
|
|
}
|
|
|
|
// Update settings timestamp
|
|
let mut settings = storage::load_settings(app)?;
|
|
settings.last_updated = Some(chrono::Utc::now());
|
|
storage::save_settings(app, &settings)?;
|
|
|
|
Ok("yt-dlp 已更新".to_string())
|
|
}
|
|
|
|
pub fn get_ytdlp_version(app: &AppHandle) -> Result<String> {
|
|
let path = get_ytdlp_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() {
|
|
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
|
|
} else {
|
|
Ok("未知".to_string())
|
|
}
|
|
}
|
|
|
|
// --- QuickJS Logic ---
|
|
|
|
#[derive(serde::Deserialize)]
|
|
struct LatestInfo {
|
|
version: String,
|
|
}
|
|
|
|
pub async fn download_qjs(app: &AppHandle) -> Result<PathBuf> {
|
|
// 1. Fetch LATEST.json to get version info (though filenames seem predictable-ish, version helps)
|
|
// Actually, looking at the file list, Bellard uses date-based versions.
|
|
// Format: quickjs-win-x86_64-YYYY-MM-DD.zip
|
|
// We need to find the correct filename dynamically or parse LATEST.json if it gave filenames.
|
|
// The LATEST.json content was {"version":"2024-01-13"} (example from prompt context).
|
|
// So we can construct the filename: quickjs-{platform}-{version}.zip
|
|
|
|
let latest_url = format!("{}/LATEST.json", QJS_REPO_URL);
|
|
let latest_resp = reqwest::get(&latest_url).await?;
|
|
if !latest_resp.status().is_success() {
|
|
return Err(anyhow!("获取 QuickJS 版本信息失败"));
|
|
}
|
|
let latest_info: LatestInfo = latest_resp.json().await?;
|
|
let version = latest_info.version;
|
|
|
|
// Construct filename based on OS/Arch
|
|
let filename = if cfg!(target_os = "windows") {
|
|
format!("quickjs-win-x86_64-{}.zip", version)
|
|
} else if cfg!(target_os = "macos") {
|
|
// NOTE: Cosmo builds are universal/portable for Linux/Mac usually?
|
|
// Based on prompt instruction: "macos download quickjs-cosmo marked file"
|
|
// Bellard lists: quickjs-cosmo-YYYY-MM-DD.zip
|
|
format!("quickjs-cosmo-{}.zip", version)
|
|
} else {
|
|
return Err(anyhow!("不支持当前操作系统的 QuickJS 自动下载"));
|
|
};
|
|
|
|
let download_url = format!("{}/{}", QJS_REPO_URL, filename);
|
|
let response = reqwest::get(&download_url).await?;
|
|
|
|
if !response.status().is_success() {
|
|
return Err(anyhow!("下载 QuickJS 失败:状态 {}", response.status()));
|
|
}
|
|
|
|
let bytes = response.bytes().await?;
|
|
let cursor = Cursor::new(bytes);
|
|
let mut archive = ZipArchive::new(cursor)?;
|
|
|
|
let bin_dir = get_bin_dir(app)?;
|
|
|
|
// Extract logic: The zip from Bellard usually contains a folder or just binaries.
|
|
// We need to find the `qjs` binary.
|
|
// Windows zip usually has `qjs.exe`
|
|
// Cosmo zip usually has `qjs`? Let's search for it.
|
|
|
|
let source_name = get_qjs_source_name_in_zip();
|
|
let target_name = get_qjs_binary_name(); // quickjs.exe or quickjs
|
|
|
|
let mut found_exe = false;
|
|
for i in 0..archive.len() {
|
|
let mut file = archive.by_index(i)?;
|
|
// Filenames in zip might be like "quickjs-win-x86_64-2024-01-13/qjs.exe"
|
|
let name = file.name().to_string();
|
|
|
|
// Skip directories
|
|
if file.is_dir() {
|
|
continue;
|
|
}
|
|
|
|
let filename_only = name.split('/').last().unwrap_or("");
|
|
|
|
if filename_only == source_name {
|
|
let mut out_file = fs::File::create(bin_dir.join(target_name))?;
|
|
std::io::copy(&mut file, &mut out_file)?;
|
|
found_exe = true;
|
|
} else if filename_only.ends_with(".dll") {
|
|
// Extract DLLs (needed for Windows MinGW builds, e.g. libwinpthread-1.dll)
|
|
let mut out_file = fs::File::create(bin_dir.join(filename_only))?;
|
|
std::io::copy(&mut file, &mut out_file)?;
|
|
}
|
|
}
|
|
|
|
if !found_exe {
|
|
return Err(anyhow!("在下载的压缩包中找不到 {}", source_name));
|
|
}
|
|
|
|
let final_path = get_qjs_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")]
|
|
{
|
|
// Remove quarantine attribute to allow execution on macOS
|
|
std::process::Command::new("xattr")
|
|
.arg("-d")
|
|
.arg("com.apple.quarantine")
|
|
.arg(&final_path)
|
|
.output()
|
|
.ok();
|
|
}
|
|
|
|
Ok(final_path)
|
|
}
|
|
|
|
pub async fn update_qjs(app: &AppHandle) -> Result<String> {
|
|
// QuickJS doesn't have self-update, so we just re-download
|
|
download_qjs(app).await?;
|
|
|
|
let mut settings = storage::load_settings(app)?;
|
|
settings.last_updated = Some(chrono::Utc::now());
|
|
storage::save_settings(app, &settings)?;
|
|
|
|
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();
|
|
// Prefer GPL static build, avoid shared to get a single exe
|
|
if lname.contains("win64") && lname.contains("gpl") && !lname.contains("shared") && lname.ends_with(".zip") {
|
|
download_url = Some(url.to_string());
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if download_url.is_none() {
|
|
// fallback: choose first zip asset that is NOT shared if possible
|
|
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())) {
|
|
let lname = name.to_lowercase();
|
|
if lname.ends_with(".zip") && !lname.contains("shared") {
|
|
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("");
|
|
// Only extract the executable, ignore DLLs
|
|
if filename_only.eq_ignore_ascii_case("ffmpeg.exe") {
|
|
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> {
|
|
if let Some(version) = run_version_command("ffmpeg", "-version") {
|
|
return Ok(version);
|
|
}
|
|
|
|
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() {
|
|
if let Some(line) = first_non_empty_line(&output) {
|
|
return Ok(line);
|
|
}
|
|
}
|
|
|
|
// 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() {
|
|
return Ok("未安装".to_string());
|
|
}
|
|
|
|
let mut version_cmd = std::process::Command::new(&path);
|
|
version_cmd.arg("--version");
|
|
#[cfg(target_os = "windows")]
|
|
version_cmd.creation_flags(0x08000000);
|
|
|
|
if let Ok(output) = version_cmd.output() {
|
|
if output.status.success() {
|
|
if let Some(line) = first_non_empty_line(&output) {
|
|
return Ok(line);
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut help_cmd = std::process::Command::new(&path);
|
|
help_cmd.arg("-h");
|
|
#[cfg(target_os = "windows")]
|
|
help_cmd.creation_flags(0x08000000);
|
|
|
|
if let Ok(output) = help_cmd.output() {
|
|
if let Some(line) = first_non_empty_line(&output) {
|
|
return Ok(line);
|
|
}
|
|
}
|
|
|
|
Ok("已安装".to_string())
|
|
}
|
|
|
|
pub async fn ensure_binaries(app: &AppHandle) -> Result<()> {
|
|
let ytdlp = get_ytdlp_path(app)?;
|
|
if !ytdlp.exists() {
|
|
download_ytdlp(app).await?;
|
|
} else {
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
std::process::Command::new("xattr")
|
|
.arg("-d")
|
|
.arg("com.apple.quarantine")
|
|
.arg(&ytdlp)
|
|
.output()
|
|
.ok();
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn run_version_command(command: &str, arg: &str) -> Option<String> {
|
|
let mut cmd = std::process::Command::new(command);
|
|
cmd.arg(arg);
|
|
#[cfg(target_os = "windows")]
|
|
cmd.creation_flags(0x08000000);
|
|
|
|
cmd.output()
|
|
.ok()
|
|
.filter(|output| output.status.success())
|
|
.and_then(|output| first_non_empty_line(&output))
|
|
}
|
|
|
|
pub fn resolve_ffmpeg(app: &AppHandle, allow_download: bool) -> Result<Option<FfmpegLocation>> {
|
|
if run_version_command("ffmpeg", "-version").is_some() {
|
|
return Ok(Some(FfmpegLocation::System));
|
|
}
|
|
|
|
let managed = get_ffmpeg_path(app)?;
|
|
if managed.exists() {
|
|
return Ok(Some(FfmpegLocation::Managed(managed)));
|
|
}
|
|
|
|
if allow_download {
|
|
return Ok(Some(FfmpegLocation::Managed(managed)));
|
|
}
|
|
|
|
Ok(None)
|
|
}
|
|
|
|
pub async fn ensure_ffmpeg_available(app: &AppHandle) -> Result<Option<FfmpegLocation>> {
|
|
if let Some(location) = resolve_ffmpeg(app, false)? {
|
|
return Ok(Some(location));
|
|
}
|
|
|
|
let path = download_ffmpeg(app).await?;
|
|
Ok(Some(FfmpegLocation::Managed(path)))
|
|
}
|
|
|
|
pub fn resolve_js_runtime(app: &AppHandle, allow_download: bool) -> Result<Option<JsRuntime>> {
|
|
if run_version_command("deno", "--version").is_some() {
|
|
return Ok(Some(JsRuntime::Deno));
|
|
}
|
|
|
|
if run_version_command("node", "--version").is_some() {
|
|
return Ok(Some(JsRuntime::Node));
|
|
}
|
|
|
|
let managed = get_qjs_path(app)?;
|
|
if managed.exists() {
|
|
return Ok(Some(JsRuntime::ManagedQuickJs(managed)));
|
|
}
|
|
|
|
if allow_download {
|
|
return Ok(Some(JsRuntime::ManagedQuickJs(managed)));
|
|
}
|
|
|
|
Ok(None)
|
|
}
|
|
|
|
pub async fn ensure_js_runtime_available(app: &AppHandle) -> Result<Option<JsRuntime>> {
|
|
if let Some(runtime) = resolve_js_runtime(app, false)? {
|
|
return Ok(Some(runtime));
|
|
}
|
|
|
|
let path = download_qjs(app).await?;
|
|
Ok(Some(JsRuntime::ManagedQuickJs(path)))
|
|
}
|
|
|
|
impl FfmpegLocation {
|
|
pub fn source_label(&self) -> &'static str {
|
|
match self {
|
|
FfmpegLocation::System => "system",
|
|
FfmpegLocation::Managed(_) => "managed",
|
|
}
|
|
}
|
|
|
|
pub fn version(&self, app: &AppHandle) -> Result<String> {
|
|
match self {
|
|
FfmpegLocation::System => Ok(run_version_command("ffmpeg", "-version").unwrap_or_else(|| "未知".to_string())),
|
|
FfmpegLocation::Managed(_) => get_ffmpeg_version(app),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl JsRuntime {
|
|
pub fn source_label(&self) -> &'static str {
|
|
match self {
|
|
JsRuntime::Deno | JsRuntime::Node => "system",
|
|
JsRuntime::ManagedQuickJs(_) => "managed",
|
|
}
|
|
}
|
|
|
|
pub fn display_name(&self, app: &AppHandle) -> Result<String> {
|
|
match self {
|
|
JsRuntime::Deno => Ok(run_version_command("deno", "--version").unwrap_or_else(|| "deno".to_string())),
|
|
JsRuntime::Node => Ok(run_version_command("node", "--version").unwrap_or_else(|| "node".to_string())),
|
|
JsRuntime::ManagedQuickJs(_) => get_qjs_version(app),
|
|
}
|
|
}
|
|
|
|
pub fn yt_dlp_argument(&self) -> String {
|
|
match self {
|
|
JsRuntime::Deno => "deno".to_string(),
|
|
JsRuntime::Node => "node".to_string(),
|
|
JsRuntime::ManagedQuickJs(path) => format!("quickjs:{}", path.to_string_lossy()),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn get_runtime_status(app: &AppHandle) -> Result<RuntimeStatus> {
|
|
let ffmpeg = resolve_ffmpeg(app, false)?;
|
|
let js_runtime = resolve_js_runtime(app, false)?;
|
|
|
|
let (ffmpeg_source, ffmpeg_version) = match ffmpeg {
|
|
Some(location) => (location.source_label().to_string(), location.version(app)?),
|
|
None => ("unavailable".to_string(), "未安装".to_string()),
|
|
};
|
|
|
|
let (js_runtime_name, js_runtime_source) = match js_runtime {
|
|
Some(runtime) => (runtime.display_name(app)?, runtime.source_label().to_string()),
|
|
None => ("未安装".to_string(), "unavailable".to_string()),
|
|
};
|
|
|
|
Ok(RuntimeStatus {
|
|
ffmpeg_source,
|
|
ffmpeg_version,
|
|
js_runtime_name,
|
|
js_runtime_source,
|
|
})
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_binary_names() {
|
|
if cfg!(target_os = "windows") {
|
|
assert_eq!(get_ytdlp_binary_name(), "yt-dlp.exe");
|
|
assert_eq!(get_qjs_binary_name(), "quickjs.exe");
|
|
assert_eq!(get_qjs_source_name_in_zip(), "qjs.exe");
|
|
} else if cfg!(target_os = "macos") {
|
|
assert_eq!(get_ytdlp_binary_name(), "yt-dlp_macos");
|
|
assert_eq!(get_qjs_binary_name(), "quickjs");
|
|
assert_eq!(get_qjs_source_name_in_zip(), "qjs");
|
|
}
|
|
}
|
|
}
|