mirror of
https://github.com/nimbold/Firelink.git
synced 2026-07-26 12:08:27 +00:00
refactor(backend): migrate to tauri v2 sidecars and resolve pipe deadlocks
- Replace tokio::process with tauri_plugin_shell sidecar API for native cross-compilation bundle execution. - Implement non-blocking stdout/stderr multiplexing in yt-dlp spawn to prevent OS pipe buffer deadlocks. - Refactor capabilities to restrict execution explicitly to sidecars instead of wildcards. - Rename local binaries to strictly adhere to target-triple architecture suffixes. - Drop manual get_binary_name path resolutions in favor of Tauri's externalBin bundler.
This commit is contained in:
@@ -0,0 +1,57 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const os = require('os');
|
||||||
|
|
||||||
|
// Maps Node's os.arch() to Rust's target_arch
|
||||||
|
const archMap = {
|
||||||
|
'x64': 'x86_64',
|
||||||
|
'arm64': 'aarch64'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Maps Node's os.platform() to Rust's target_os/target_env
|
||||||
|
const platformMap = {
|
||||||
|
'darwin': 'apple-darwin',
|
||||||
|
'win32': 'pc-windows-msvc',
|
||||||
|
'linux': 'unknown-linux-gnu'
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentArch = archMap[os.arch()];
|
||||||
|
const currentPlatform = platformMap[os.platform()];
|
||||||
|
|
||||||
|
if (!currentArch || !currentPlatform) {
|
||||||
|
console.error(`Unsupported architecture or platform: ${os.arch()} / ${os.platform()}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetTriple = `${currentArch}-${currentPlatform}`;
|
||||||
|
const isWindows = os.platform() === 'win32';
|
||||||
|
const ext = isWindows ? '.exe' : '';
|
||||||
|
const suffix = `-${targetTriple}${ext}`;
|
||||||
|
|
||||||
|
const binariesDir = path.join(__dirname, '..', 'src-tauri', 'binaries');
|
||||||
|
const requiredBinaries = ['yt-dlp', 'aria2c', 'ffmpeg', 'deno'];
|
||||||
|
|
||||||
|
console.log(`Verifying target sidecars for: ${targetTriple}`);
|
||||||
|
|
||||||
|
let missing = false;
|
||||||
|
|
||||||
|
for (const bin of requiredBinaries) {
|
||||||
|
const expectedName = `${bin}${suffix}`;
|
||||||
|
const binPath = path.join(binariesDir, expectedName);
|
||||||
|
|
||||||
|
if (!fs.existsSync(binPath)) {
|
||||||
|
console.error(`[ERROR] Missing strictly required sidecar: ${expectedName} in src-tauri/binaries/`);
|
||||||
|
missing = true;
|
||||||
|
} else {
|
||||||
|
console.log(`[OK] Found sidecar: ${expectedName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (missing) {
|
||||||
|
console.error('\nPlease download or build the missing target triple binaries and place them in the src-tauri/binaries directory.');
|
||||||
|
console.error('Build blocked due to missing architecture-aware sidecars.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('All required sidecars are present.');
|
||||||
|
process.exit(0);
|
||||||
Generated
+64
@@ -1256,6 +1256,7 @@ dependencies = [
|
|||||||
"tauri-plugin-dialog",
|
"tauri-plugin-dialog",
|
||||||
"tauri-plugin-notification",
|
"tauri-plugin-notification",
|
||||||
"tauri-plugin-opener",
|
"tauri-plugin-opener",
|
||||||
|
"tauri-plugin-shell",
|
||||||
"tauri-plugin-single-instance",
|
"tauri-plugin-single-instance",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
@@ -3032,6 +3033,16 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "os_pipe"
|
||||||
|
version = "1.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pango"
|
name = "pango"
|
||||||
version = "0.18.3"
|
version = "0.18.3"
|
||||||
@@ -4065,12 +4076,44 @@ dependencies = [
|
|||||||
"digest",
|
"digest",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shared_child"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"sigchld",
|
||||||
|
"windows-sys 0.60.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shlex"
|
name = "shlex"
|
||||||
version = "2.0.1"
|
version = "2.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sigchld"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"os_pipe",
|
||||||
|
"signal-hook",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook"
|
||||||
|
version = "0.3.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"signal-hook-registry",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signal-hook-registry"
|
name = "signal-hook-registry"
|
||||||
version = "1.4.8"
|
version = "1.4.8"
|
||||||
@@ -4631,6 +4674,27 @@ dependencies = [
|
|||||||
"zbus",
|
"zbus",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-shell"
|
||||||
|
version = "2.3.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8457dbf9e2bab1edd8df22bb2c20857a59a9868e79cb3eac5ed639eec4d0c73b"
|
||||||
|
dependencies = [
|
||||||
|
"encoding_rs",
|
||||||
|
"log 0.4.32",
|
||||||
|
"open",
|
||||||
|
"os_pipe",
|
||||||
|
"regex 1.12.4",
|
||||||
|
"schemars 0.8.22",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"shared_child",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-single-instance"
|
name = "tauri-plugin-single-instance"
|
||||||
version = "2.4.2"
|
version = "2.4.2"
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ tauri-build = { version = "2", features = [] }
|
|||||||
tauri = { version = "2", features = ["macos-private-api", "tray-icon", "image-png"] }
|
tauri = { version = "2", features = ["macos-private-api", "tray-icon", "image-png"] }
|
||||||
tauri-plugin-opener = "2"
|
tauri-plugin-opener = "2"
|
||||||
tauri-plugin-dialog = "2"
|
tauri-plugin-dialog = "2"
|
||||||
|
tauri-plugin-shell = "2"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
tokio = { version = "1", features = ["fs", "process", "io-util", "rt", "rt-multi-thread", "macros", "sync", "time"] }
|
tokio = { version = "1", features = ["fs", "process", "io-util", "rt", "rt-multi-thread", "macros", "sync", "time"] }
|
||||||
|
|||||||
@@ -9,6 +9,28 @@
|
|||||||
"opener:default",
|
"opener:default",
|
||||||
"dialog:default",
|
"dialog:default",
|
||||||
"notification:default",
|
"notification:default",
|
||||||
"notification:allow-is-permission-granted"
|
"notification:allow-is-permission-granted",
|
||||||
|
"shell:allow-execute",
|
||||||
|
{
|
||||||
|
"identifier": "shell:allow-execute",
|
||||||
|
"allow": [
|
||||||
|
{
|
||||||
|
"cmd": "yt-dlp",
|
||||||
|
"args": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cmd": "aria2c",
|
||||||
|
"args": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cmd": "ffmpeg",
|
||||||
|
"args": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cmd": "deno",
|
||||||
|
"args": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+107
-149
@@ -2,9 +2,6 @@
|
|||||||
|
|
||||||
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
||||||
use tauri::{Manager, Emitter};
|
use tauri::{Manager, Emitter};
|
||||||
use tokio::process::Command as AsyncCommand;
|
|
||||||
use std::process::Stdio;
|
|
||||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
@@ -20,13 +17,7 @@ pub struct MetadataResponse {
|
|||||||
size_bytes: u64,
|
size_bytes: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_binary_name(base: &str) -> String {
|
|
||||||
if cfg!(target_os = "windows") {
|
|
||||||
format!("{}.exe", base)
|
|
||||||
} else {
|
|
||||||
base.to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -177,24 +168,20 @@ async fn fetch_metadata(url: String, user_agent: Option<String>, username: Optio
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn fetch_media_metadata(app_handle: tauri::AppHandle, url: String, cookie_browser: Option<String>, username: Option<String>, password: Option<String>) -> Result<String, String> {
|
async fn fetch_media_metadata(app_handle: tauri::AppHandle, url: String, cookie_browser: Option<String>, username: Option<String>, password: Option<String>) -> Result<String, String> {
|
||||||
println!("fetch_media_metadata called for: {}", url);
|
println!("fetch_media_metadata called for: {}", url);
|
||||||
let resource_dir = app_handle.path().resource_dir().map_err(|e| e.to_string())?;
|
use tauri_plugin_shell::ShellExt;
|
||||||
let ytdlp_path = resource_dir.join("binaries").join(get_binary_name("yt-dlp"));
|
let mut cmd = app_handle.shell().sidecar("yt-dlp").map_err(|e| format!("Failed to create sidecar yt-dlp: {}", e))?;
|
||||||
|
cmd = cmd.arg("-J")
|
||||||
let deno_path = resource_dir.join("binaries").join(get_binary_name("deno"));
|
|
||||||
let mut cmd = AsyncCommand::new(&ytdlp_path);
|
|
||||||
cmd.arg("-J")
|
|
||||||
.arg("--no-warnings")
|
.arg("--no-warnings")
|
||||||
.arg("--no-playlist")
|
.arg("--no-playlist")
|
||||||
.arg("--no-check-formats")
|
.arg("--no-check-formats")
|
||||||
.arg("--socket-timeout").arg("20")
|
.arg("--socket-timeout").arg("20")
|
||||||
.arg("--retries").arg("3")
|
.arg("--retries").arg("3")
|
||||||
.arg("--extractor-retries").arg("3")
|
.arg("--extractor-retries").arg("3")
|
||||||
.arg("--compat-options").arg("no-youtube-unavailable-videos")
|
.arg("--compat-options").arg("no-youtube-unavailable-videos");
|
||||||
.arg("--js-runtimes").arg(format!("deno:{}", deno_path.display()));
|
|
||||||
|
|
||||||
if let Some(browser) = cookie_browser {
|
if let Some(browser) = cookie_browser {
|
||||||
if !browser.is_empty() {
|
if !browser.is_empty() {
|
||||||
cmd.arg("--cookies-from-browser").arg(&browser);
|
cmd = cmd.arg("--cookies-from-browser").arg(&browser);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,10 +201,10 @@ async fn fetch_media_metadata(app_handle: tauri::AppHandle, url: String, cookie_
|
|||||||
config_file.write_all(config_content.as_bytes()).map_err(|e| e.to_string())?;
|
config_file.write_all(config_content.as_bytes()).map_err(|e| e.to_string())?;
|
||||||
let config_path = config_file.into_temp_path();
|
let config_path = config_file.into_temp_path();
|
||||||
if !config_content.is_empty() {
|
if !config_content.is_empty() {
|
||||||
cmd.arg("--config-location").arg(&config_path);
|
cmd = cmd.arg("--config-location").arg(&config_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.arg("--").arg(&url);
|
cmd = cmd.arg("--").arg(&url);
|
||||||
|
|
||||||
// We use tokio AsyncCommand so it doesn't block the async thread
|
// We use tokio AsyncCommand so it doesn't block the async thread
|
||||||
let output = cmd.output()
|
let output = cmd.output()
|
||||||
@@ -236,11 +223,10 @@ async fn fetch_media_metadata(app_handle: tauri::AppHandle, url: String, cookie_
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn test_ytdlp(app_handle: tauri::AppHandle) -> Result<String, String> {
|
async fn test_ytdlp(app_handle: tauri::AppHandle) -> Result<String, String> {
|
||||||
println!("test_ytdlp called!");
|
println!("test_ytdlp called!");
|
||||||
let resource_dir = app_handle.path().resource_dir().map_err(|e| e.to_string())?;
|
use tauri_plugin_shell::ShellExt;
|
||||||
let ytdlp_path = resource_dir.join("binaries").join(get_binary_name("yt-dlp"));
|
|
||||||
println!("Resolved yt-dlp path: {:?}", ytdlp_path);
|
let output = app_handle.shell().sidecar("yt-dlp")
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
let output = AsyncCommand::new(&ytdlp_path)
|
|
||||||
.arg("--version")
|
.arg("--version")
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
@@ -249,7 +235,7 @@ async fn test_ytdlp(app_handle: tauri::AppHandle) -> Result<String, String> {
|
|||||||
format!("Failed to execute yt-dlp: {}", e)
|
format!("Failed to execute yt-dlp: {}", e)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
println!("yt-dlp execution finished with status: {}", output.status);
|
println!("yt-dlp execution finished with status: {:?}", output.status);
|
||||||
if output.status.success() {
|
if output.status.success() {
|
||||||
let text = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
let text = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||||
println!("yt-dlp output: {}", text);
|
println!("yt-dlp output: {}", text);
|
||||||
@@ -264,11 +250,10 @@ async fn test_ytdlp(app_handle: tauri::AppHandle) -> Result<String, String> {
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn test_ffmpeg(app_handle: tauri::AppHandle) -> Result<String, String> {
|
async fn test_ffmpeg(app_handle: tauri::AppHandle) -> Result<String, String> {
|
||||||
println!("test_ffmpeg called!");
|
println!("test_ffmpeg called!");
|
||||||
let resource_dir = app_handle.path().resource_dir().map_err(|e| e.to_string())?;
|
use tauri_plugin_shell::ShellExt;
|
||||||
let ffmpeg_path = resource_dir.join("binaries").join(get_binary_name("ffmpeg"));
|
|
||||||
println!("Resolved ffmpeg path: {:?}", ffmpeg_path);
|
|
||||||
|
|
||||||
let output = AsyncCommand::new(&ffmpeg_path)
|
let output = app_handle.shell().sidecar("ffmpeg")
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
.arg("-version")
|
.arg("-version")
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
@@ -277,7 +262,7 @@ async fn test_ffmpeg(app_handle: tauri::AppHandle) -> Result<String, String> {
|
|||||||
format!("Failed to execute ffmpeg: {}", e)
|
format!("Failed to execute ffmpeg: {}", e)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
println!("ffmpeg execution finished with status: {}", output.status);
|
println!("ffmpeg execution finished with status: {:?}", output.status);
|
||||||
if output.status.success() {
|
if output.status.success() {
|
||||||
let text = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
let text = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||||
let first_line = text.lines().next().unwrap_or("").to_string();
|
let first_line = text.lines().next().unwrap_or("").to_string();
|
||||||
@@ -295,11 +280,10 @@ async fn test_ffmpeg(app_handle: tauri::AppHandle) -> Result<String, String> {
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn test_deno(app_handle: tauri::AppHandle) -> Result<String, String> {
|
async fn test_deno(app_handle: tauri::AppHandle) -> Result<String, String> {
|
||||||
println!("test_deno called!");
|
println!("test_deno called!");
|
||||||
let resource_dir = app_handle.path().resource_dir().map_err(|e| e.to_string())?;
|
use tauri_plugin_shell::ShellExt;
|
||||||
let deno_path = resource_dir.join("binaries").join(get_binary_name("deno"));
|
|
||||||
println!("Resolved deno path: {:?}", deno_path);
|
|
||||||
|
|
||||||
let output = AsyncCommand::new(&deno_path)
|
let output = app_handle.shell().sidecar("deno")
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
.arg("--version")
|
.arg("--version")
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
@@ -308,7 +292,7 @@ async fn test_deno(app_handle: tauri::AppHandle) -> Result<String, String> {
|
|||||||
format!("Failed to execute deno: {}", e)
|
format!("Failed to execute deno: {}", e)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
println!("deno execution finished with status: {}", output.status);
|
println!("deno execution finished with status: {:?}", output.status);
|
||||||
if output.status.success() {
|
if output.status.success() {
|
||||||
let text = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
let text = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||||
let re = regex::Regex::new(r"deno\s+(\d+\.\d+\.\d+)").unwrap();
|
let re = regex::Regex::new(r"deno\s+(\d+\.\d+\.\d+)").unwrap();
|
||||||
@@ -383,49 +367,19 @@ async fn show_in_folder(app: tauri::AppHandle, path: String) -> Result<(), Strin
|
|||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::{Arc, Mutex, RwLock};
|
use std::sync::{Arc, Mutex, RwLock};
|
||||||
|
|
||||||
struct Aria2DaemonGuard(std::sync::Mutex<Option<std::process::Child>>);
|
struct Aria2DaemonGuard(std::sync::Mutex<Option<tauri_plugin_shell::process::CommandChild>>);
|
||||||
|
|
||||||
impl Drop for Aria2DaemonGuard {
|
impl Drop for Aria2DaemonGuard {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
if let Ok(mut lock) = self.0.lock() {
|
if let Ok(mut lock) = self.0.lock() {
|
||||||
if let Some(mut child) = lock.take() {
|
if let Some(child) = lock.take() {
|
||||||
let _ = child.kill();
|
let _ = child.kill();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
fn resign_aria2_debug_bundle(aria2c_path: &std::path::Path) -> Result<(), String> {
|
|
||||||
let lib_dir = aria2c_path
|
|
||||||
.parent()
|
|
||||||
.ok_or_else(|| "aria2 resource directory is missing".to_string())?
|
|
||||||
.join("aria2-libs");
|
|
||||||
let mut libraries = std::fs::read_dir(&lib_dir)
|
|
||||||
.map_err(|error| format!("Failed to read aria2 libraries: {error}"))?
|
|
||||||
.filter_map(Result::ok)
|
|
||||||
.map(|entry| entry.path())
|
|
||||||
.filter(|path| path.extension().and_then(|extension| extension.to_str()) == Some("dylib"))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
libraries.sort();
|
|
||||||
|
|
||||||
for path in libraries.into_iter().chain(std::iter::once(aria2c_path.to_path_buf())) {
|
|
||||||
let output = std::process::Command::new("/usr/bin/codesign")
|
|
||||||
.args(["--force", "--sign", "-"])
|
|
||||||
.arg(&path)
|
|
||||||
.output()
|
|
||||||
.map_err(|error| format!("Failed to run codesign for {}: {error}", path.display()))?;
|
|
||||||
if !output.status.success() {
|
|
||||||
return Err(format!(
|
|
||||||
"Failed to sign {}: {}",
|
|
||||||
path.display(),
|
|
||||||
String::from_utf8_lossy(&output.stderr).trim()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod download;
|
pub mod download;
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
@@ -800,9 +754,6 @@ pub(crate) async fn start_media_download_internal(
|
|||||||
.unwrap_or("download")
|
.unwrap_or("download")
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
let resource_dir = app_handle.path().resource_dir().map_err(|e| e.to_string())?;
|
|
||||||
let ytdlp_path = resource_dir.join("binaries").join(get_binary_name("yt-dlp"));
|
|
||||||
let ffmpeg_path = resource_dir.join("binaries").join(get_binary_name("ffmpeg"));
|
|
||||||
|
|
||||||
let resolved_dest = resolve_path(&destination, &app_handle);
|
let resolved_dest = resolve_path(&destination, &app_handle);
|
||||||
|
|
||||||
@@ -822,11 +773,9 @@ pub(crate) async fn start_media_download_internal(
|
|||||||
1.0
|
1.0
|
||||||
};
|
};
|
||||||
|
|
||||||
let deno_path = resource_dir.join("binaries").join(get_binary_name("deno"));
|
use tauri_plugin_shell::ShellExt;
|
||||||
let mut cmd = AsyncCommand::new(&ytdlp_path);
|
let mut cmd = app_handle.shell().sidecar("yt-dlp").map_err(|e| e.to_string())?
|
||||||
cmd.arg("--newline")
|
.arg("--newline")
|
||||||
.arg("--ffmpeg-location")
|
|
||||||
.arg(&ffmpeg_path)
|
|
||||||
.arg("--no-check-formats")
|
.arg("--no-check-formats")
|
||||||
.arg("--socket-timeout").arg("20")
|
.arg("--socket-timeout").arg("20")
|
||||||
.arg("--retries").arg("3")
|
.arg("--retries").arg("3")
|
||||||
@@ -836,37 +785,35 @@ pub(crate) async fn start_media_download_internal(
|
|||||||
.arg("--concurrent-fragments").arg("4")
|
.arg("--concurrent-fragments").arg("4")
|
||||||
.arg("--no-warnings")
|
.arg("--no-warnings")
|
||||||
.arg("--compat-options").arg("no-youtube-unavailable-videos")
|
.arg("--compat-options").arg("no-youtube-unavailable-videos")
|
||||||
.arg("--js-runtimes").arg(format!("deno:{},node", deno_path.display()))
|
|
||||||
.arg("--progress-template").arg("download:[%(progress.downloaded_bytes)s/%(progress.total_bytes)s]")
|
|
||||||
.arg("-o").arg(out_path.to_string_lossy().to_string());
|
.arg("-o").arg(out_path.to_string_lossy().to_string());
|
||||||
|
|
||||||
if let Some(limit) = speed_limit {
|
if let Some(limit) = speed_limit {
|
||||||
if !limit.is_empty() {
|
if !limit.is_empty() {
|
||||||
cmd.arg("--limit-rate").arg(limit);
|
cmd = cmd.arg("--limit-rate").arg(limit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(p) = proxy {
|
if let Some(p) = proxy {
|
||||||
if !p.is_empty() {
|
if !p.is_empty() {
|
||||||
cmd.arg("--proxy").arg(p);
|
cmd = cmd.arg("--proxy").arg(p);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(mut cs) = cookie_source {
|
if let Some(mut cs) = cookie_source {
|
||||||
if !cs.is_empty() && cs != "none" {
|
if !cs.is_empty() && cs != "none" {
|
||||||
if cs == "safari" { cs = "safari:".to_string() }
|
if cs == "safari" { cs = "safari:".to_string() }
|
||||||
cmd.arg("--cookies-from-browser").arg(cs);
|
cmd = cmd.arg("--cookies-from-browser").arg(cs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(ua) = user_agent {
|
if let Some(ua) = user_agent {
|
||||||
if !ua.is_empty() {
|
if !ua.is_empty() {
|
||||||
cmd.arg("--user-agent").arg(ua);
|
cmd = cmd.arg("--user-agent").arg(ua);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(tries) = max_tries {
|
if let Some(tries) = max_tries {
|
||||||
cmd.arg("--retries").arg(tries.to_string());
|
cmd = cmd.arg("--retries").arg(tries.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut config_file = tempfile::Builder::new().prefix("ytdlp-").suffix(".conf").tempfile().map_err(|e| e.to_string())?;
|
let mut config_file = tempfile::Builder::new().prefix("ytdlp-").suffix(".conf").tempfile().map_err(|e| e.to_string())?;
|
||||||
@@ -890,36 +837,33 @@ pub(crate) async fn start_media_download_internal(
|
|||||||
config_file.write_all(config_content.as_bytes()).map_err(|e| e.to_string())?;
|
config_file.write_all(config_content.as_bytes()).map_err(|e| e.to_string())?;
|
||||||
let config_path = config_file.into_temp_path();
|
let config_path = config_file.into_temp_path();
|
||||||
if !config_content.is_empty() {
|
if !config_content.is_empty() {
|
||||||
cmd.arg("--config-location").arg(&config_path);
|
cmd = cmd.arg("--config-location").arg(config_path.to_string_lossy().to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(format) = format_selector {
|
if let Some(format) = format_selector {
|
||||||
cmd.arg("-f").arg(format);
|
cmd = cmd.arg("-f").arg(format);
|
||||||
// If the filename implies an audio format, use it as audio output
|
// If the filename implies an audio format, use it as audio output
|
||||||
if safe_filename.ends_with(".mp3") {
|
if safe_filename.ends_with(".mp3") {
|
||||||
cmd.arg("-x").arg("--audio-format").arg("mp3");
|
cmd = cmd.arg("-x").arg("--audio-format").arg("mp3");
|
||||||
} else if safe_filename.ends_with(".m4a") {
|
} else if safe_filename.ends_with(".m4a") {
|
||||||
cmd.arg("-x").arg("--audio-format").arg("m4a");
|
cmd = cmd.arg("-x").arg("--audio-format").arg("m4a");
|
||||||
} else if safe_filename.ends_with(".opus") {
|
} else if safe_filename.ends_with(".opus") {
|
||||||
cmd.arg("-x").arg("--audio-format").arg("opus");
|
cmd = cmd.arg("-x").arg("--audio-format").arg("opus");
|
||||||
} else {
|
} else {
|
||||||
// Otherwise attempt to merge into mp4 or mkv based on filename
|
// Otherwise attempt to merge into mp4 or mkv based on filename
|
||||||
if safe_filename.ends_with(".mp4") {
|
if safe_filename.ends_with(".mp4") {
|
||||||
cmd.arg("--merge-output-format").arg("mp4");
|
cmd = cmd.arg("--merge-output-format").arg("mp4");
|
||||||
} else if safe_filename.ends_with(".webm") {
|
} else if safe_filename.ends_with(".webm") {
|
||||||
cmd.arg("--merge-output-format").arg("webm");
|
cmd = cmd.arg("--merge-output-format").arg("webm");
|
||||||
} else {
|
} else {
|
||||||
cmd.arg("--merge-output-format").arg("mkv");
|
cmd = cmd.arg("--merge-output-format").arg("mkv");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.arg("--").arg(&url);
|
cmd = cmd.arg("--").arg(&url);
|
||||||
cmd.stdout(Stdio::piped());
|
|
||||||
cmd.stderr(Stdio::piped()); // Also pipe stderr for better error reporting
|
|
||||||
|
|
||||||
let mut child = cmd.spawn().map_err(|e| format!("Failed to spawn yt-dlp: {}", e))?;
|
let (mut rx, child) = cmd.spawn().map_err(|e| format!("Failed to spawn yt-dlp: {}", e))?;
|
||||||
let stdout = child.stdout.take().ok_or("Failed to capture stdout")?;
|
|
||||||
|
|
||||||
// yt-dlp parsing regex
|
// yt-dlp parsing regex
|
||||||
static PCT_RE: std::sync::OnceLock<Regex> = std::sync::OnceLock::new();
|
static PCT_RE: std::sync::OnceLock<Regex> = std::sync::OnceLock::new();
|
||||||
@@ -931,27 +875,28 @@ pub(crate) async fn start_media_download_internal(
|
|||||||
let eta_re = ETA_RE.get_or_init(|| Regex::new(r"ETA\s+([^\s]+)").unwrap());
|
let eta_re = ETA_RE.get_or_init(|| Regex::new(r"ETA\s+([^\s]+)").unwrap());
|
||||||
|
|
||||||
let _keep_alive = config_path;
|
let _keep_alive = config_path;
|
||||||
let mut reader = BufReader::new(stdout).lines();
|
|
||||||
let mut current_track: f64 = 0.0;
|
let mut current_track: f64 = 0.0;
|
||||||
let mut last_fraction: f64 = 0.0;
|
let mut last_fraction: f64 = 0.0;
|
||||||
let mut last_progress_at = std::time::Instant::now()
|
let mut last_progress_at = std::time::Instant::now()
|
||||||
.checked_sub(std::time::Duration::from_millis(150))
|
.checked_sub(std::time::Duration::from_millis(200))
|
||||||
.unwrap_or_else(std::time::Instant::now);
|
.unwrap_or_else(std::time::Instant::now);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
_ = cancel_rx.changed() => {
|
_ = cancel_rx.changed() => {
|
||||||
let _ = child.kill().await;
|
let _ = child.kill();
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
line_result = reader.next_line() => {
|
event = rx.recv() => {
|
||||||
match line_result {
|
match event {
|
||||||
Ok(Some(line)) => {
|
Some(tauri_plugin_shell::process::CommandEvent::Stdout(line_bytes)) => {
|
||||||
|
let line = String::from_utf8_lossy(&line_bytes);
|
||||||
if line.contains("[download]") && line.contains("%") {
|
if line.contains("[download]") && line.contains("%") {
|
||||||
let fraction = pct_re.captures(&line)
|
let fraction = if let Some(cap) = pct_re.captures(&line) {
|
||||||
.and_then(|cap| cap.get(1))
|
cap.get(1).and_then(|m| m.as_str().parse::<f64>().ok()).unwrap_or(0.0) / 100.0
|
||||||
.and_then(|m| m.as_str().parse::<f64>().ok())
|
} else {
|
||||||
.unwrap_or(0.0) / 100.0;
|
0.0
|
||||||
|
};
|
||||||
|
|
||||||
if fraction < last_fraction && (last_fraction - fraction) > 0.5 {
|
if fraction < last_fraction && (last_fraction - fraction) > 0.5 {
|
||||||
current_track += 1.0;
|
current_track += 1.0;
|
||||||
@@ -960,18 +905,20 @@ pub(crate) async fn start_media_download_internal(
|
|||||||
|
|
||||||
let overall_fraction = ((current_track + fraction) / total_tracks).min(1.0);
|
let overall_fraction = ((current_track + fraction) / total_tracks).min(1.0);
|
||||||
|
|
||||||
let speed = spd_re.captures(&line)
|
let speed = if let Some(cap) = spd_re.captures(&line) {
|
||||||
.and_then(|cap| cap.get(1))
|
cap.get(1).map(|m| m.as_str().to_string()).unwrap_or_else(|| "-".to_string())
|
||||||
.map(|m| m.as_str().to_string())
|
} else {
|
||||||
.unwrap_or_else(|| "-".to_string());
|
"-".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
let eta = eta_re.captures(&line)
|
let eta = if let Some(cap) = eta_re.captures(&line) {
|
||||||
.and_then(|cap| cap.get(1))
|
cap.get(1).map(|m| m.as_str().to_string()).unwrap_or_else(|| "-".to_string())
|
||||||
.map(|m| m.as_str().to_string())
|
} else {
|
||||||
.unwrap_or_else(|| "-".to_string());
|
"-".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
let now = std::time::Instant::now();
|
let now = std::time::Instant::now();
|
||||||
if now.duration_since(last_progress_at) >= std::time::Duration::from_millis(1000) {
|
if now.duration_since(last_progress_at) >= std::time::Duration::from_millis(200) {
|
||||||
let _ = app_handle.emit("download-progress", DownloadProgressEvent {
|
let _ = app_handle.emit("download-progress", DownloadProgressEvent {
|
||||||
id: id.to_string(),
|
id: id.to_string(),
|
||||||
fraction: overall_fraction,
|
fraction: overall_fraction,
|
||||||
@@ -983,22 +930,30 @@ pub(crate) async fn start_media_download_internal(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => break,
|
Some(tauri_plugin_shell::process::CommandEvent::Stderr(_line_bytes)) => {
|
||||||
|
// Consume stderr to avoid blocking
|
||||||
|
}
|
||||||
|
Some(tauri_plugin_shell::process::CommandEvent::Error(err)) => {
|
||||||
|
eprintln!("yt-dlp shell error: {}", err);
|
||||||
|
let _ = app_handle.emit("download-failed", id.to_string());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Some(tauri_plugin_shell::process::CommandEvent::Terminated(payload)) => {
|
||||||
|
println!("child exit status: {:?}", payload.code);
|
||||||
|
if payload.code == Some(0) {
|
||||||
|
let _ = app_handle.emit("download-complete", id.to_string());
|
||||||
|
} else {
|
||||||
|
let _ = app_handle.emit("download-failed", id.to_string());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Some(_) => {}
|
||||||
|
None => break,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let status = child.wait().await;
|
|
||||||
println!("child exit status: {:?}", status);
|
|
||||||
if let Ok(exit_status) = status {
|
|
||||||
if exit_status.success() {
|
|
||||||
let _ = app_handle.emit("download-complete", id.to_string());
|
|
||||||
} else {
|
|
||||||
let _ = app_handle.emit("download-failed", id.to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1436,34 +1391,36 @@ pub fn run() {
|
|||||||
|
|
||||||
crate::scheduler::spawn_scheduler(app.handle().clone());
|
crate::scheduler::spawn_scheduler(app.handle().clone());
|
||||||
|
|
||||||
let resource_dir = app.path().resource_dir().unwrap();
|
use tauri_plugin_shell::ShellExt;
|
||||||
let aria2c_path = resource_dir.join("binaries").join(get_binary_name("aria2c"));
|
let aria2_process = match app.handle().shell().sidecar("aria2c") {
|
||||||
|
Ok(cmd) => {
|
||||||
#[cfg(all(target_os = "macos", debug_assertions))]
|
cmd.arg("--enable-rpc=true")
|
||||||
if let Err(error) = resign_aria2_debug_bundle(&aria2c_path) {
|
.arg(format!("--rpc-listen-port={}", aria2_port))
|
||||||
eprintln!("{error}");
|
.arg(format!("--rpc-secret={}", aria2_secret))
|
||||||
}
|
.arg("--rpc-listen-all=false")
|
||||||
|
.arg("--continue=true")
|
||||||
let aria2_process = std::process::Command::new(&aria2c_path)
|
.arg("--allow-overwrite=false")
|
||||||
.arg("--enable-rpc=true")
|
.arg("--summary-interval=1")
|
||||||
.arg(format!("--rpc-listen-port={}", aria2_port))
|
.arg("--console-log-level=warn")
|
||||||
.arg(format!("--rpc-secret={}", aria2_secret))
|
.arg("--download-result=hide")
|
||||||
.arg("--rpc-listen-all=false")
|
.arg("--check-certificate=true")
|
||||||
.arg("--continue=true")
|
.spawn()
|
||||||
.arg("--allow-overwrite=false")
|
.map(|(_, child)| child)
|
||||||
.arg("--summary-interval=1")
|
.ok()
|
||||||
.arg("--console-log-level=warn")
|
}
|
||||||
.arg("--download-result=hide")
|
Err(e) => {
|
||||||
.arg("--check-certificate=true")
|
eprintln!("Failed to create aria2c sidecar: {}", e);
|
||||||
.spawn();
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
match aria2_process {
|
match aria2_process {
|
||||||
Ok(process) => {
|
Some(process) => {
|
||||||
println!("Spawned global aria2c daemon on port {}", aria2_port);
|
println!("Spawned global aria2c daemon on port {}", aria2_port);
|
||||||
let guard = app.state::<Aria2DaemonGuard>();
|
let guard = app.state::<Aria2DaemonGuard>();
|
||||||
*guard.0.lock().unwrap() = Some(process);
|
*guard.0.lock().unwrap() = Some(process);
|
||||||
}
|
}
|
||||||
Err(e) => eprintln!("Failed to spawn aria2c daemon: {}", e),
|
None => eprintln!("Failed to spawn aria2c daemon"),
|
||||||
}
|
}
|
||||||
|
|
||||||
let app_handle_ws = app.handle().clone();
|
let app_handle_ws = app.handle().clone();
|
||||||
@@ -1574,6 +1531,7 @@ pub fn run() {
|
|||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
|
.plugin(tauri_plugin_shell::init())
|
||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
.plugin(tauri_plugin_notification::init())
|
.plugin(tauri_plugin_notification::init())
|
||||||
.on_window_event(|window, event| {
|
.on_window_event(|window, event| {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "npm run dev",
|
"beforeDevCommand": "npm run dev",
|
||||||
"devUrl": "http://localhost:1420",
|
"devUrl": "http://localhost:1420",
|
||||||
"beforeBuildCommand": "npm run build",
|
"beforeBuildCommand": "node ../scripts/verify-binaries.js && npm run build",
|
||||||
"frontendDist": "../dist"
|
"frontendDist": "../dist"
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
@@ -38,6 +38,12 @@
|
|||||||
"bundle": {
|
"bundle": {
|
||||||
"active": true,
|
"active": true,
|
||||||
"targets": "all",
|
"targets": "all",
|
||||||
|
"externalBin": [
|
||||||
|
"binaries/yt-dlp",
|
||||||
|
"binaries/aria2c",
|
||||||
|
"binaries/ffmpeg",
|
||||||
|
"binaries/deno"
|
||||||
|
],
|
||||||
"icon": [
|
"icon": [
|
||||||
"icons/32x32.png",
|
"icons/32x32.png",
|
||||||
"icons/128x128.png",
|
"icons/128x128.png",
|
||||||
|
|||||||
Reference in New Issue
Block a user