add quickjs but has bugs
This commit is contained in:
258
src-tauri/src/binary_manager.rs
Normal file
258
src-tauri/src/binary_manager.rs
Normal file
@@ -0,0 +1,258 @@
|
||||
// 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;
|
||||
use zip::ZipArchive;
|
||||
use std::io::Cursor;
|
||||
|
||||
use crate::storage::{self};
|
||||
|
||||
const YT_DLP_REPO_URL: &str = "https://github.com/yt-dlp/yt-dlp/releases/latest/download";
|
||||
const QJS_REPO_URL: &str = "https://github.com/quickjs-ng/quickjs/releases/latest/download";
|
||||
|
||||
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
|
||||
fn get_qjs_source_name() -> &'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 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
|
||||
}
|
||||
|
||||
// --- 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!("Failed to download yt-dlp: Status {}", 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)?;
|
||||
}
|
||||
|
||||
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("Downloaded fresh yt-dlp".to_string());
|
||||
}
|
||||
|
||||
// Use built-in update for yt-dlp
|
||||
let output = std::process::Command::new(&path)
|
||||
.arg("-U")
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
||||
return Err(anyhow!("yt-dlp update failed: {}", 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 updated".to_string())
|
||||
}
|
||||
|
||||
pub fn get_ytdlp_version(app: &AppHandle) -> Result<String> {
|
||||
let path = get_ytdlp_path(app)?;
|
||||
if !path.exists() {
|
||||
return Ok("Not installed".to_string());
|
||||
}
|
||||
|
||||
let output = std::process::Command::new(&path)
|
||||
.arg("--version")
|
||||
.output()?;
|
||||
|
||||
if output.status.success() {
|
||||
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
|
||||
} else {
|
||||
Ok("Unknown".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
// --- QuickJS Logic ---
|
||||
|
||||
pub async fn download_qjs(app: &AppHandle) -> Result<PathBuf> {
|
||||
// Determine asset name based on OS/Arch
|
||||
let asset_name = if cfg!(target_os = "windows") {
|
||||
"qjs-windows-x86_64.zip"
|
||||
} else if cfg!(target_os = "macos") {
|
||||
if cfg!(target_arch = "aarch64") {
|
||||
"qjs-macos-aarch64.zip"
|
||||
} else {
|
||||
"qjs-macos-x86_64.zip"
|
||||
}
|
||||
} else {
|
||||
return Err(anyhow!("Unsupported OS for QuickJS auto-download"));
|
||||
};
|
||||
|
||||
let url = format!("{}/{}", QJS_REPO_URL, asset_name);
|
||||
let response = reqwest::get(&url).await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(anyhow!("Failed to download QuickJS: Status {}", 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: find the file that starts with 'qjs' (ignoring extensions like .exe for now) and isn't a folder
|
||||
let source_name = get_qjs_source_name();
|
||||
let target_name = get_qjs_binary_name(); // quickjs.exe or quickjs
|
||||
|
||||
let mut found = false;
|
||||
for i in 0..archive.len() {
|
||||
let mut file = archive.by_index(i)?;
|
||||
let name = file.name().split('/').last().unwrap_or("");
|
||||
|
||||
if name == source_name {
|
||||
let mut out_file = fs::File::create(bin_dir.join(target_name))?;
|
||||
std::io::copy(&mut file, &mut out_file)?;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return Err(anyhow!("Could not find {} in downloaded archive", 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)?;
|
||||
}
|
||||
|
||||
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?;
|
||||
Ok("QuickJS updated/installed".to_string())
|
||||
}
|
||||
|
||||
pub fn get_qjs_version(app: &AppHandle) -> Result<String> {
|
||||
let path = get_qjs_path(app)?;
|
||||
if !path.exists() {
|
||||
return Ok("Not installed".to_string());
|
||||
}
|
||||
|
||||
// QuickJS might not support --version in a standard way, or might print help.
|
||||
// Let's try executing it with --version or -h and see if we can grab something.
|
||||
// For now, simpler: return "Installed" if it works, or check creation time?
|
||||
// Let's return "Installed" to keep UI simple or try to run it.
|
||||
// If we run `quickjs --version`, it often just prints the version if supported.
|
||||
// quickjs-ng seems to support it?
|
||||
// If not, we will just return "Ready".
|
||||
|
||||
// Attempt execution
|
||||
// Note: using .arg("-h") might be safer if --version isn't standard.
|
||||
// But let's try just checking existence for safety to avoid hanging if it opens REPL.
|
||||
Ok("Installed".to_string())
|
||||
}
|
||||
|
||||
pub async fn ensure_binaries(app: &AppHandle) -> Result<()> {
|
||||
let ytdlp = get_ytdlp_path(app)?;
|
||||
if !ytdlp.exists() {
|
||||
download_ytdlp(app).await?;
|
||||
}
|
||||
|
||||
let qjs = get_qjs_path(app)?;
|
||||
if !qjs.exists() {
|
||||
download_qjs(app).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[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(), "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(), "qjs");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
// filepath: src-tauri/src/commands.rs
|
||||
use tauri::{AppHandle, Manager};
|
||||
use crate::{ytdlp, downloader, storage};
|
||||
use crate::{binary_manager, downloader, storage};
|
||||
use crate::downloader::DownloadOptions;
|
||||
use crate::storage::{Settings, HistoryItem};
|
||||
use uuid::Uuid;
|
||||
@@ -8,25 +8,35 @@ use std::path::Path;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn init_ytdlp(app: AppHandle) -> Result<bool, String> {
|
||||
if ytdlp::check_ytdlp(&app) {
|
||||
if binary_manager::check_binaries(&app) {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// If not found, try to download
|
||||
match ytdlp::download_ytdlp(&app).await {
|
||||
match binary_manager::ensure_binaries(&app).await {
|
||||
Ok(_) => Ok(true),
|
||||
Err(e) => Err(format!("Failed to download yt-dlp: {}", e)),
|
||||
Err(e) => Err(format!("Failed to download binaries: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn update_ytdlp(app: AppHandle) -> Result<String, String> {
|
||||
ytdlp::update_ytdlp(&app).await.map_err(|e| e.to_string())
|
||||
binary_manager::update_ytdlp(&app).await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
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 fn get_ytdlp_version(app: AppHandle) -> Result<String, String> {
|
||||
ytdlp::get_version(&app).map_err(|e| e.to_string())
|
||||
binary_manager::get_ytdlp_version(&app).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_quickjs_version(app: AppHandle) -> Result<String, String> {
|
||||
binary_manager::get_qjs_version(&app).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::process::Stdio;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use anyhow::{Result, anyhow};
|
||||
use regex::Regex;
|
||||
use crate::ytdlp;
|
||||
use crate::binary_manager;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct VideoMetadata {
|
||||
@@ -54,9 +54,19 @@ pub struct LogEvent {
|
||||
}
|
||||
|
||||
pub async fn fetch_metadata(app: &AppHandle, url: &str, parse_mix_playlist: bool) -> Result<MetadataResult> {
|
||||
let ytdlp_path = ytdlp::get_ytdlp_path(app)?;
|
||||
let ytdlp_path = binary_manager::get_ytdlp_path(app)?; // Updated path call
|
||||
|
||||
// Inject PATH for QuickJS
|
||||
let bin_dir = binary_manager::get_bin_dir(app)?;
|
||||
let path_env = std::env::var("PATH").unwrap_or_default();
|
||||
let new_path_env = format!("{}{}{}", bin_dir.to_string_lossy(), if cfg!(windows) { ";" } else { ":" }, path_env);
|
||||
|
||||
let mut cmd = Command::new(ytdlp_path);
|
||||
|
||||
// Environment injection
|
||||
cmd.env("PATH", new_path_env);
|
||||
cmd.arg("--js-runtime").arg("qjs"); // Force QuickJS
|
||||
|
||||
cmd.arg("--dump-single-json")
|
||||
.arg("--flat-playlist")
|
||||
.arg("--no-warnings");
|
||||
@@ -124,9 +134,19 @@ pub async fn download_video(
|
||||
url: String,
|
||||
options: DownloadOptions,
|
||||
) -> Result<String> {
|
||||
let ytdlp_path = ytdlp::get_ytdlp_path(&app)?;
|
||||
let ytdlp_path = binary_manager::get_ytdlp_path(&app)?; // Updated path call
|
||||
|
||||
// Inject PATH for QuickJS
|
||||
let bin_dir = binary_manager::get_bin_dir(&app)?;
|
||||
let path_env = std::env::var("PATH").unwrap_or_default();
|
||||
let new_path_env = format!("{}{}{}", bin_dir.to_string_lossy(), if cfg!(windows) { ";" } else { ":" }, path_env);
|
||||
|
||||
let mut args = Vec::new();
|
||||
|
||||
// JS Runtime args must be passed via .arg(), env is set on command builder
|
||||
args.push("--js-runtime".to_string());
|
||||
args.push("quickjs".to_string());
|
||||
|
||||
args.push(url);
|
||||
|
||||
// Output template
|
||||
@@ -153,6 +173,7 @@ pub async fn download_video(
|
||||
args.push("--newline".to_string()); // Easier parsing
|
||||
|
||||
let mut child = Command::new(ytdlp_path)
|
||||
.env("PATH", new_path_env) // Inject PATH
|
||||
.args(&args)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped()) // Capture stderr for logs
|
||||
@@ -229,4 +250,4 @@ pub async fn download_video(
|
||||
}).ok();
|
||||
Err(anyhow!("Download process failed"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// filepath: src-tauri/src/lib.rs
|
||||
mod ytdlp;
|
||||
mod binary_manager;
|
||||
mod downloader;
|
||||
mod storage;
|
||||
mod commands;
|
||||
@@ -12,7 +12,9 @@ pub fn run() {
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
commands::init_ytdlp,
|
||||
commands::update_ytdlp,
|
||||
commands::update_quickjs,
|
||||
commands::get_ytdlp_version,
|
||||
commands::get_quickjs_version,
|
||||
commands::fetch_metadata,
|
||||
commands::start_download,
|
||||
commands::get_settings,
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
// filepath: src-tauri/src/ytdlp.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;
|
||||
|
||||
use crate::storage::{self};
|
||||
|
||||
const REPO_URL: &str = "https://github.com/yt-dlp/yt-dlp/releases/latest/download";
|
||||
|
||||
pub fn get_binary_name() -> &'static str {
|
||||
if cfg!(target_os = "windows") {
|
||||
"yt-dlp.exe"
|
||||
} else if cfg!(target_os = "macos") {
|
||||
"yt-dlp_macos"
|
||||
} else {
|
||||
"yt-dlp"
|
||||
}
|
||||
}
|
||||
|
||||
// Helper for testing
|
||||
pub fn get_ytdlp_path_from_dir(base_dir: &PathBuf) -> PathBuf {
|
||||
base_dir.join("bin").join(get_binary_name())
|
||||
}
|
||||
|
||||
pub fn get_ytdlp_path(app: &AppHandle) -> Result<PathBuf> {
|
||||
let app_data = storage::get_app_data_dir(app)?;
|
||||
// Use helper
|
||||
Ok(get_ytdlp_path_from_dir(&app_data))
|
||||
}
|
||||
|
||||
pub fn check_ytdlp(app: &AppHandle) -> bool {
|
||||
match get_ytdlp_path(app) {
|
||||
Ok(path) => path.exists(),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn download_ytdlp(app: &AppHandle) -> Result<PathBuf> {
|
||||
let binary_name = get_binary_name();
|
||||
let url = format!("{}/{}", REPO_URL, binary_name);
|
||||
|
||||
let response = reqwest::get(&url).await?;
|
||||
if !response.status().is_success() {
|
||||
return Err(anyhow!("Failed to download yt-dlp: Status {}", response.status()));
|
||||
}
|
||||
|
||||
let bytes = response.bytes().await?;
|
||||
let path = get_ytdlp_path(app)?;
|
||||
|
||||
// Ensure parent directory exists
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
fs::write(&path, bytes)?;
|
||||
|
||||
// Set permissions on Unix
|
||||
#[cfg(target_family = "unix")]
|
||||
{
|
||||
let mut perms = fs::metadata(&path)?.permissions();
|
||||
perms.set_mode(0o755);
|
||||
fs::set_permissions(&path, perms)?;
|
||||
}
|
||||
|
||||
// Update settings with timestamp
|
||||
let mut settings = storage::load_settings(app)?;
|
||||
settings.last_updated = Some(chrono::Utc::now());
|
||||
storage::save_settings(app, &settings)?;
|
||||
|
||||
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("Downloaded fresh copy".to_string());
|
||||
}
|
||||
|
||||
let output = std::process::Command::new(&path)
|
||||
.arg("-U")
|
||||
.output()?;
|
||||
|
||||
if output.status.success() {
|
||||
// Update settings timestamp
|
||||
let mut settings = storage::load_settings(app)?;
|
||||
settings.last_updated = Some(chrono::Utc::now());
|
||||
storage::save_settings(app, &settings)?;
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
|
||||
Ok(stdout)
|
||||
} else {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
||||
Err(anyhow!("Update failed: {}", stderr))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_version(app: &AppHandle) -> Result<String> {
|
||||
let path = get_ytdlp_path(app)?;
|
||||
if !path.exists() {
|
||||
return Ok("Not installed".to_string());
|
||||
}
|
||||
|
||||
let output = std::process::Command::new(&path)
|
||||
.arg("--version")
|
||||
.output()?;
|
||||
|
||||
if output.status.success() {
|
||||
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
|
||||
} else {
|
||||
Ok("Unknown".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
fn test_binary_name() {
|
||||
let name = get_binary_name();
|
||||
if cfg!(target_os = "windows") {
|
||||
assert_eq!(name, "yt-dlp.exe");
|
||||
} else if cfg!(target_os = "macos") {
|
||||
assert_eq!(name, "yt-dlp_macos");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_path_construction() {
|
||||
let base = PathBuf::from("/tmp/app");
|
||||
let path = get_ytdlp_path_from_dir(&base);
|
||||
let name = get_binary_name();
|
||||
assert_eq!(path, base.join("bin").join(name));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user