From 320c95041ab52c3124696169bfb19484ad54dd5f Mon Sep 17 00:00:00 2001 From: Julian Freeman Date: Tue, 2 Dec 2025 11:59:30 -0400 Subject: [PATCH] add quickjs but has bugs --- spec/quickjs_runtime.md | 67 +++++++++ src-tauri/Cargo.lock | 249 ++++++++++++++++++++++++++++++ src-tauri/Cargo.toml | 1 + src-tauri/src/binary_manager.rs | 258 ++++++++++++++++++++++++++++++++ src-tauri/src/commands.rs | 22 ++- src-tauri/src/downloader.rs | 29 +++- src-tauri/src/lib.rs | 4 +- src-tauri/src/ytdlp.rs | 140 ----------------- src/stores/settings.ts | 13 +- src/views/Settings.vue | 95 +++++++++--- 10 files changed, 700 insertions(+), 178 deletions(-) create mode 100644 spec/quickjs_runtime.md create mode 100644 src-tauri/src/binary_manager.rs delete mode 100644 src-tauri/src/ytdlp.rs diff --git a/spec/quickjs_runtime.md b/spec/quickjs_runtime.md new file mode 100644 index 0000000..13cc972 --- /dev/null +++ b/spec/quickjs_runtime.md @@ -0,0 +1,67 @@ +# Feature Specification: QuickJS Runtime Integration for yt-dlp + +## 1. Context & Objective +`yt-dlp` increasingly relies on a JavaScript runtime to execute complex decryption scripts (e.g., signature extraction) required by YouTube and other platforms. Without a JS runtime, downloads may fail or be throttled. +The objective is to automatically manage the **QuickJS** runtime binary (download, update, store) alongside the existing `yt-dlp` binary and configure the download process to utilize it via environment variable injection. + +## 2. Technical Requirements + +### 2.1. Binary Management +The application must manage the QuickJS binary lifecycle identically to how it currently handles `yt-dlp`. + +* **Source Repository:** [https://github.com/quickjs-ng/quickjs](https://github.com/quickjs-ng/quickjs) +* **Storage Location:** The user's AppData `bin` directory (same as `yt-dlp`). + * Windows: `%APPDATA%\StreamCapture\bin\` + * macOS: `~/Library/Application Support/StreamCapture/bin/` +* **Unified Naming on Disk:** + Regardless of the source filename, the binary must be renamed upon saving to ensure consistent invocation: + * **Windows:** `qjs.exe` + * **macOS:** `qjs` + +### 2.2. Download & Update Logic +* **Check:** On app launch (or `init_ytdlp`), check if `qjs` exists. +* **Download:** If missing, fetch the appropriate asset from the latest GitHub release of `quickjs-ng/quickjs`. + * *Note:* Logic must detect OS/Arch to pick the correct asset (e.g., `windows-x86_64`, `macos-x86_64/arm64`). +* **Permissions:** On macOS/Linux, ensure `chmod +x` is applied. +* **Update:** When `update_ytdlp` is triggered, also check/redownload the latest `qjs` binary. + +### 2.3. Execution & Environment Injection +To enable `yt-dlp` to find the `qjs` binary without hardcoding absolute paths in the flags (which can be fragile), we will modify the **subprocess environment**. + +* **Command Flag:** Always pass `--js-runtime qjs` to the `yt-dlp` command. +* **Environment Modification:** + Before spawning the `yt-dlp` child process in Rust, dynamically modify its `PATH` environment variable. + * **Action:** Prepend the absolute path of the application's `bin` directory to the system `PATH`. + * **Result:** When `yt-dlp` looks for `qjs`, it will find it in the injected `PATH`. + +## 3. Implementation Plan + +### Step 1: Refactor `src-tauri/src/ytdlp.rs` +* Rename file/module to `src-tauri/src/binary_manager.rs` (Optional, or just expand `ytdlp.rs`). +* Add constants/functions for QuickJS: + * `get_qjs_binary_name()` + * `download_qjs()`: Similar logic to `download_ytdlp` but parsing QuickJS release assets. + +### Step 2: Update `src-tauri/src/commands.rs` +* Update `init_ytdlp` to initialize **both** binaries. +* Update `update_ytdlp` to update **both** binaries. + +### Step 3: Update `src-tauri/src/downloader.rs` +* In `download_video` and `fetch_metadata`: + 1. Resolve the absolute path to the `bin` directory. + 2. Read the current system `PATH`. + 3. Construct a new `PATH` string: `bin_dir + delimiter + system_path`. + 4. Configure the `Command`: + ```rust + Command::new(...) + .env("PATH", new_path) + .arg("--js-runtime") + .arg("qjs") + ... + ``` + +## 4. Acceptance Criteria +1. **File Existence:** `qjs.exe` (Win) or `qjs` (Mac) appears in the `bin` folder after app startup. +2. **Process Execution:** The logs show `yt-dlp` running successfully. +3. **Verification:** If a specific video requires JS interpretation (often indicated by slow downloads or errors without JS), it proceeds smoothly. +4. **Clean Logs:** No "PhantomJS not found" or "JS engine not found" warnings in the logs. diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 0883156..23178cb 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -8,6 +8,17 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -47,6 +58,15 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "ashpd" version = "0.11.0" @@ -349,6 +369,15 @@ dependencies = [ "serde", ] +[[package]] +name = "bzip2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a53fac24f34a81bc9954b5d6cfce0c21e18ec6959f44f56e8e90e4bb7c346c" +dependencies = [ + "libbz2-rs-sys", +] + [[package]] name = "cairo-rs" version = "0.18.5" @@ -423,6 +452,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -479,6 +510,16 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "combine" version = "4.6.7" @@ -498,6 +539,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + [[package]] name = "convert_case" version = "0.4.0" @@ -573,6 +620,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc32fast" version = "1.5.0" @@ -679,6 +741,12 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "deflate64" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26bf8fc351c5ed29b5c2f0cbbac1b209b74f60ecd62e675a998df72c49af5204" + [[package]] name = "deranged" version = "0.5.5" @@ -689,6 +757,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "derive_more" version = "0.99.20" @@ -710,6 +789,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -978,6 +1058,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" dependencies = [ "crc32fast", + "libz-rs-sys", "miniz_oxide", ] @@ -1492,6 +1573,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "html5ever" version = "0.29.1" @@ -1798,6 +1888,15 @@ dependencies = [ "cfb", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -1884,6 +1983,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + [[package]] name = "js-sys" version = "0.3.83" @@ -1969,6 +2078,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "libbz2-rs-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7" + [[package]] name = "libc" version = "0.2.177" @@ -1995,6 +2110,15 @@ dependencies = [ "libc", ] +[[package]] +name = "libz-rs-sys" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "840db8cf39d9ec4dd794376f38acc40d0fc65eec2a8f484f7fd375b84602becd" +dependencies = [ + "zlib-rs", +] + [[package]] name = "linux-raw-sys" version = "0.11.0" @@ -2028,6 +2152,16 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "lzma-rust2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c60a23ffb90d527e23192f1246b14746e2f7f071cb84476dd879071696c18a4a" +dependencies = [ + "crc", + "sha2", +] + [[package]] name = "mac" version = "0.1.1" @@ -2639,6 +2773,16 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -2863,6 +3007,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "ppmd-rust" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d558c559f0450f16f2a27a1f017ef38468c1090c9ce63c8e51366232d53717b4" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -3682,6 +3832,17 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha2" version = "0.10.9" @@ -3825,6 +3986,7 @@ dependencies = [ "tauri-plugin-opener", "tokio", "uuid", + "zip", ] [[package]] @@ -5814,6 +5976,20 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] [[package]] name = "zerotrie" @@ -5848,6 +6024,79 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "zip" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2a05c7c36fde6c09b08576c9f7fb4cda705990f73b58fe011abf7dfb24168b" +dependencies = [ + "aes", + "arbitrary", + "bzip2", + "constant_time_eq", + "crc32fast", + "deflate64", + "flate2", + "getrandom 0.3.4", + "hmac", + "indexmap 2.12.1", + "lzma-rust2", + "memchr", + "pbkdf2", + "ppmd-rust", + "sha1", + "time", + "zeroize", + "zopfli", + "zstd", +] + +[[package]] +name = "zlib-rs" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f06ae92f42f5e5c42443fd094f245eb656abf56dd7cce9b8b263236565e00f2" + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "zvariant" version = "5.8.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 6c445c5..e8a09b9 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -25,3 +25,4 @@ chrono = { version = "0.4", features = ["serde"] } futures-util = "0.3" regex = "1.10" uuid = { version = "1.0", features = ["v4", "serde"] } +zip = "6.0.0" diff --git a/src-tauri/src/binary_manager.rs b/src-tauri/src/binary_manager.rs new file mode 100644 index 0000000..1137e85 --- /dev/null +++ b/src-tauri/src/binary_manager.rs @@ -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 { + 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 { + Ok(get_bin_dir(app)?.join(get_ytdlp_binary_name())) +} + +pub fn get_qjs_path(app: &AppHandle) -> Result { + 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 { + 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 { + 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 { + 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 { + // 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 { + // 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 { + 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"); + } + } +} diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index e20bcde..1add16d 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -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 { - 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 { - 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 { + binary_manager::update_qjs(&app).await.map_err(|e| e.to_string()) } #[tauri::command] pub fn get_ytdlp_version(app: AppHandle) -> Result { - 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 { + binary_manager::get_qjs_version(&app).map_err(|e| e.to_string()) } #[tauri::command] diff --git a/src-tauri/src/downloader.rs b/src-tauri/src/downloader.rs index 22a27ba..c444e3a 100644 --- a/src-tauri/src/downloader.rs +++ b/src-tauri/src/downloader.rs @@ -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 { - 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 { - 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")) } -} +} \ No newline at end of file diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 1f0a1ea..602fb80 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -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, diff --git a/src-tauri/src/ytdlp.rs b/src-tauri/src/ytdlp.rs deleted file mode 100644 index 3a5c8cc..0000000 --- a/src-tauri/src/ytdlp.rs +++ /dev/null @@ -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 { - 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 { - 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 { - 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 { - 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)); - } -} \ No newline at end of file diff --git a/src/stores/settings.ts b/src/stores/settings.ts index ba536ef..6f039ee 100644 --- a/src/stores/settings.ts +++ b/src/stores/settings.ts @@ -17,6 +17,7 @@ export const useSettingsStore = defineStore('settings', () => { }) const ytdlpVersion = ref('Checking...') + const quickjsVersion = ref('Checking...') const isInitializing = ref(true) async function loadSettings() { @@ -43,15 +44,21 @@ export const useSettingsStore = defineStore('settings', () => { isInitializing.value = true // check/download await invoke('init_ytdlp') - ytdlpVersion.value = await invoke('get_ytdlp_version') + await refreshVersions() } catch (e) { console.error(e) ytdlpVersion.value = 'Error' + quickjsVersion.value = 'Error' } finally { isInitializing.value = false } } + async function refreshVersions() { + ytdlpVersion.value = await invoke('get_ytdlp_version') + quickjsVersion.value = await invoke('get_quickjs_version') + } + function applyTheme(theme: string) { const root = document.documentElement const isDark = theme === 'dark' || (theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches) @@ -69,5 +76,5 @@ export const useSettingsStore = defineStore('settings', () => { } }) - return { settings, loadSettings, save, initYtdlp, ytdlpVersion, isInitializing } -}) + return { settings, loadSettings, save, initYtdlp, refreshVersions, ytdlpVersion, quickjsVersion, isInitializing } +}) \ No newline at end of file diff --git a/src/views/Settings.vue b/src/views/Settings.vue index 9a16814..41a08eb 100644 --- a/src/views/Settings.vue +++ b/src/views/Settings.vue @@ -4,10 +4,11 @@ import { ref } from 'vue' import { useSettingsStore } from '../stores/settings' import { invoke } from '@tauri-apps/api/core' import { open } from '@tauri-apps/plugin-dialog' -import { Folder, RefreshCw, Monitor, Sun, Moon } from 'lucide-vue-next' +import { Folder, RefreshCw, Monitor, Sun, Moon, Terminal } from 'lucide-vue-next' const settingsStore = useSettingsStore() -const updating = ref(false) +const updatingYtdlp = ref(false) +const updatingQuickjs = ref(false) const updateStatus = ref('') async function browsePath() { @@ -24,16 +25,30 @@ async function browsePath() { } async function updateYtdlp() { - updating.value = true - updateStatus.value = 'Checking...' + updatingYtdlp.value = true + updateStatus.value = 'Updating yt-dlp...' try { const res = await invoke('update_ytdlp') updateStatus.value = res - await settingsStore.initYtdlp() + await settingsStore.refreshVersions() } catch (e: any) { - updateStatus.value = 'Error: ' + e.toString() + updateStatus.value = 'yt-dlp Error: ' + e.toString() } finally { - updating.value = false + updatingYtdlp.value = false + } +} + +async function updateQuickjs() { + updatingQuickjs.value = true + updateStatus.value = 'Updating QuickJS...' + try { + const res = await invoke('update_quickjs') + updateStatus.value = res + await settingsStore.refreshVersions() + } catch (e: any) { + updateStatus.value = 'QuickJS Error: ' + e.toString() + } finally { + updatingQuickjs.value = false } } @@ -99,27 +114,59 @@ function setTheme(theme: 'light' | 'dark' | 'system') { - +
-
-
-

Binary Management

-

Current Version: {{ settingsStore.ytdlpVersion }}

-

Last Updated: {{ settingsStore.settings.last_updated ? new Date(settingsStore.settings.last_updated).toLocaleDateString() : 'Never' }}

-
- +

External Binaries

+ +
+ +
+
+
+ +
+
+
yt-dlp
+
v{{ settingsStore.ytdlpVersion }}
+
+
+ +
+ + +
+
+
+ +
+
+
QuickJS
+
{{ settingsStore.quickjsVersion }}
+
+
+ +
-
+ +
{{ updateStatus }}
+

Last Checked: {{ settingsStore.settings.last_updated ? new Date(settingsStore.settings.last_updated).toLocaleString() : 'Never' }}

- + \ No newline at end of file