diff --git a/spec/quickjs_update.md b/spec/quickjs_update.md new file mode 100644 index 0000000..fcdd01d --- /dev/null +++ b/spec/quickjs_update.md @@ -0,0 +1,26 @@ +# QuickJS Binary Source Update + +## 1. Context +The current `quickjs-ng` runtime is proving too slow for our needs. We need to switch to the original QuickJS runtime provided by Bellard. + +## 2. Source Details +* **Base URL:** `https://bellard.org/quickjs/binary_releases/` +* **Versioning:** Use `LATEST.json` at the base URL to find the latest version and filename. + * Format: `{"version":"2024-01-13","files":{"quickjs-win-x86_64.zip":"quickjs-win-x86_64-2024-01-13.zip", ...}}` +* **Target Files:** + * **Windows:** Look for key `quickjs-win-x86_64.zip`. + * **macOS:** Look for key `quickjs-cosmo-x86_64.zip` (Cosmo builds are generally portable). *Wait, need to confirm if macos specific builds exist or if cosmo is the intended one for unix-like.* + * *Correction*: Bellard's page lists `quickjs-macos-x86_64.zip`? Let's check LATEST.json first. Assuming `quickjs-cosmo` might be Linux/Universal. Let's fetch `LATEST.json` to be sure. + +## 3. Implementation Changes +* **`src-tauri/src/binary_manager.rs`**: + * Update `QJS_REPO_URL` to `https://bellard.org/quickjs/binary_releases`. + * Add logic to fetch `LATEST.json` first. + * Parse the JSON to get the actual filename for the current OS. + * Download the ZIP file. + * Extract the binary (`qjs.exe` or `qjs`) from the ZIP. + * Rename it to our internal standard (`quickjs.exe` / `quickjs`). + +## 4. Verification +* Re-run tests. +* Ensure `update_quickjs` flows correctly with the new logic. diff --git a/src-tauri/src/binary_manager.rs b/src-tauri/src/binary_manager.rs index 199fabb..cd5b738 100644 --- a/src-tauri/src/binary_manager.rs +++ b/src-tauri/src/binary_manager.rs @@ -5,11 +5,14 @@ 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"; +// Bellard's QuickJS binary releases URL +const QJS_REPO_URL: &str = "https://bellard.org/quickjs/binary_releases"; pub fn get_ytdlp_binary_name() -> &'static str { if cfg!(target_os = "windows") { @@ -30,16 +33,10 @@ pub fn get_qjs_binary_name() -> &'static str { } } -// Source name inside the zip archive or direct download -// Updated based on actual release assets: -// Windows: qjs-windows-x86_64.exe (direct executable) -// macOS: qjs-darwin (direct executable) -// Note: The logic now needs to support direct download, not just zip. -fn get_qjs_download_filename() -> &'static str { +// Source name inside the zip archive (standard QuickJS naming) +fn get_qjs_source_name_in_zip() -> &'static str { if cfg!(target_os = "windows") { - "qjs-windows-x86_64.exe" - } else if cfg!(target_os = "macos") { - "qjs-darwin" + "qjs.exe" } else { "qjs" } @@ -143,28 +140,80 @@ pub fn get_ytdlp_version(app: &AppHandle) -> Result { // --- QuickJS Logic --- -pub async fn download_qjs(app: &AppHandle) -> Result { - // Determine asset name based on OS/Arch - // Updated to match actual GitHub Release assets (direct executables, not zips) - let asset_name = get_qjs_download_filename(); +#[derive(serde::Deserialize)] +struct LatestInfo { + version: String, +} - let url = format!("{}/{}", QJS_REPO_URL, asset_name); - let response = reqwest::get(&url).await?; +pub async fn download_qjs(app: &AppHandle) -> Result { + // 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!("Failed to fetch QuickJS version info")); + } + 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!("Unsupported OS for QuickJS auto-download")); + }; + + let download_url = format!("{}/{}", QJS_REPO_URL, filename); + let response = reqwest::get(&download_url).await?; if !response.status().is_success() { return Err(anyhow!("Failed to download QuickJS: Status {}", response.status())); } let bytes = response.bytes().await?; - // Direct write, no zip extraction needed + let cursor = Cursor::new(bytes); + let mut archive = ZipArchive::new(cursor)?; + + let bin_dir = get_bin_dir(app)?; - let final_path = get_qjs_path(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. - if let Some(parent) = final_path.parent() { - fs::create_dir_all(parent)?; + let source_name = get_qjs_source_name_in_zip(); + 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)?; + // Filenames in zip might be like "quickjs-win-x86_64-2024-01-13/qjs.exe" + let name = file.name(); + 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 = true; + break; + } } - fs::write(&final_path, bytes)?; + if !found { + return Err(anyhow!("Could not find {} in downloaded archive", source_name)); + } + + let final_path = get_qjs_path(app)?; #[cfg(target_family = "unix")] { @@ -187,11 +236,6 @@ pub fn get_qjs_version(app: &AppHandle) -> Result { if !path.exists() { return Ok("Not installed".to_string()); } - - // Try to get version if possible, otherwise just say Installed - // Running with -h usually works and prints version in first line often - // But let's stick to simple existence check for stability unless we need exact version. - // Or we can check file creation time. Ok("Installed".to_string()) } @@ -218,12 +262,11 @@ mod tests { if cfg!(target_os = "windows") { assert_eq!(get_ytdlp_binary_name(), "yt-dlp.exe"); assert_eq!(get_qjs_binary_name(), "quickjs.exe"); - // Check download filename - assert_eq!(get_qjs_download_filename(), "qjs-windows-x86_64.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_download_filename(), "qjs-darwin"); + assert_eq!(get_qjs_source_name_in_zip(), "qjs"); } } -} \ No newline at end of file +} diff --git a/src-tauri/src/downloader.rs b/src-tauri/src/downloader.rs index 9184300..92d32ef 100644 --- a/src-tauri/src/downloader.rs +++ b/src-tauri/src/downloader.rs @@ -53,19 +53,16 @@ pub struct LogEvent { pub level: String, // "info", "error" } -pub async fn fetch_metadata(app: &AppHandle, url: &str, parse_mix_playlist: bool) -> Result { - 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); + +pub async fn fetch_metadata(app: &AppHandle, url: &str, parse_mix_playlist: bool) -> Result { + let ytdlp_path = binary_manager::get_ytdlp_path(app)?; + let qjs_path = binary_manager::get_qjs_path(app)?; // Get absolute path to quickjs + let mut cmd = Command::new(ytdlp_path); - // Environment injection - cmd.env("PATH", new_path_env); - cmd.arg("--js-runtime").arg("quickjs"); // Force QuickJS + // Pass the runtime and its absolute path to --js-runtimes + cmd.arg("--js-runtimes").arg(format!("quickjs:{}", qjs_path.to_string_lossy())); cmd.arg("--dump-single-json") .arg("--flat-playlist") @@ -134,18 +131,14 @@ pub async fn download_video( url: String, options: DownloadOptions, ) -> Result { - let ytdlp_path = binary_manager::get_ytdlp_path(&app)?; // Updated path call + let ytdlp_path = binary_manager::get_ytdlp_path(&app)?; + let qjs_path = binary_manager::get_qjs_path(&app)?; // Get absolute path to quickjs - // 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()); + // Pass the runtime and its absolute path to --js-runtimes + args.push("--js-runtimes".to_string()); + args.push(format!("quickjs:{}", qjs_path.to_string_lossy())); args.push(url); @@ -158,7 +151,7 @@ pub async fn download_video( if options.is_audio_only { args.push("-x".to_string()); args.push("--audio-format".to_string()); - args.push("mp3".to_string()); // Defaulting to mp3 for simplicity + args.push("mp3".to_string()); } else { let format_arg = if options.quality == "best" { "bestvideo+bestaudio/best".to_string() @@ -170,13 +163,14 @@ pub async fn download_video( } // Progress output - args.push("--newline".to_string()); // Easier parsing + args.push("--newline".to_string()); - let mut child = Command::new(ytdlp_path) - .env("PATH", new_path_env) // Inject PATH + let mut cmd = Command::new(ytdlp_path); + + let mut child = cmd .args(&args) .stdout(Stdio::piped()) - .stderr(Stdio::piped()) // Capture stderr for logs + .stderr(Stdio::piped()) .spawn()?; let stdout = child.stdout.take().ok_or(anyhow!("Failed to open stdout"))?; @@ -250,4 +244,4 @@ pub async fn download_video( }).ok(); Err(anyhow!("Download process failed")) } -} \ No newline at end of file +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index d37c55d..3943834 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -14,7 +14,7 @@ { "title": "stream-capture", "width": 1300, - "height": 800 + "height": 850 } ], "security": {