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-notification",
|
||||
"tauri-plugin-opener",
|
||||
"tauri-plugin-shell",
|
||||
"tauri-plugin-single-instance",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
@@ -3032,6 +3033,16 @@ dependencies = [
|
||||
"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]]
|
||||
name = "pango"
|
||||
version = "0.18.3"
|
||||
@@ -4065,12 +4076,44 @@ dependencies = [
|
||||
"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]]
|
||||
name = "shlex"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.8"
|
||||
@@ -4631,6 +4674,27 @@ dependencies = [
|
||||
"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]]
|
||||
name = "tauri-plugin-single-instance"
|
||||
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-plugin-opener = "2"
|
||||
tauri-plugin-dialog = "2"
|
||||
tauri-plugin-shell = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tokio = { version = "1", features = ["fs", "process", "io-util", "rt", "rt-multi-thread", "macros", "sync", "time"] }
|
||||
|
||||
@@ -9,6 +9,28 @@
|
||||
"opener:default",
|
||||
"dialog: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/
|
||||
use tauri::{Manager, Emitter};
|
||||
use tokio::process::Command as AsyncCommand;
|
||||
use std::process::Stdio;
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
use regex::Regex;
|
||||
use serde::Serialize;
|
||||
use ts_rs::TS;
|
||||
@@ -20,13 +17,7 @@ pub struct MetadataResponse {
|
||||
size_bytes: u64,
|
||||
}
|
||||
|
||||
fn get_binary_name(base: &str) -> String {
|
||||
if cfg!(target_os = "windows") {
|
||||
format!("{}.exe", base)
|
||||
} else {
|
||||
base.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[tauri::command]
|
||||
@@ -177,24 +168,20 @@ async fn fetch_metadata(url: String, user_agent: Option<String>, username: Optio
|
||||
#[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> {
|
||||
println!("fetch_media_metadata called for: {}", url);
|
||||
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 deno_path = resource_dir.join("binaries").join(get_binary_name("deno"));
|
||||
let mut cmd = AsyncCommand::new(&ytdlp_path);
|
||||
cmd.arg("-J")
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
let mut cmd = app_handle.shell().sidecar("yt-dlp").map_err(|e| format!("Failed to create sidecar yt-dlp: {}", e))?;
|
||||
cmd = cmd.arg("-J")
|
||||
.arg("--no-warnings")
|
||||
.arg("--no-playlist")
|
||||
.arg("--no-check-formats")
|
||||
.arg("--socket-timeout").arg("20")
|
||||
.arg("--retries").arg("3")
|
||||
.arg("--extractor-retries").arg("3")
|
||||
.arg("--compat-options").arg("no-youtube-unavailable-videos")
|
||||
.arg("--js-runtimes").arg(format!("deno:{}", deno_path.display()));
|
||||
.arg("--compat-options").arg("no-youtube-unavailable-videos");
|
||||
|
||||
if let Some(browser) = cookie_browser {
|
||||
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())?;
|
||||
let config_path = config_file.into_temp_path();
|
||||
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
|
||||
let output = cmd.output()
|
||||
@@ -236,11 +223,10 @@ async fn fetch_media_metadata(app_handle: tauri::AppHandle, url: String, cookie_
|
||||
#[tauri::command]
|
||||
async fn test_ytdlp(app_handle: tauri::AppHandle) -> Result<String, String> {
|
||||
println!("test_ytdlp called!");
|
||||
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"));
|
||||
println!("Resolved yt-dlp path: {:?}", ytdlp_path);
|
||||
|
||||
let output = AsyncCommand::new(&ytdlp_path)
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
|
||||
let output = app_handle.shell().sidecar("yt-dlp")
|
||||
.map_err(|e| e.to_string())?
|
||||
.arg("--version")
|
||||
.output()
|
||||
.await
|
||||
@@ -249,7 +235,7 @@ async fn test_ytdlp(app_handle: tauri::AppHandle) -> Result<String, String> {
|
||||
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() {
|
||||
let text = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
println!("yt-dlp output: {}", text);
|
||||
@@ -264,11 +250,10 @@ async fn test_ytdlp(app_handle: tauri::AppHandle) -> Result<String, String> {
|
||||
#[tauri::command]
|
||||
async fn test_ffmpeg(app_handle: tauri::AppHandle) -> Result<String, String> {
|
||||
println!("test_ffmpeg called!");
|
||||
let resource_dir = app_handle.path().resource_dir().map_err(|e| e.to_string())?;
|
||||
let ffmpeg_path = resource_dir.join("binaries").join(get_binary_name("ffmpeg"));
|
||||
println!("Resolved ffmpeg path: {:?}", ffmpeg_path);
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
|
||||
let output = AsyncCommand::new(&ffmpeg_path)
|
||||
let output = app_handle.shell().sidecar("ffmpeg")
|
||||
.map_err(|e| e.to_string())?
|
||||
.arg("-version")
|
||||
.output()
|
||||
.await
|
||||
@@ -277,7 +262,7 @@ async fn test_ffmpeg(app_handle: tauri::AppHandle) -> Result<String, String> {
|
||||
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() {
|
||||
let text = String::from_utf8_lossy(&output.stdout).trim().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]
|
||||
async fn test_deno(app_handle: tauri::AppHandle) -> Result<String, String> {
|
||||
println!("test_deno called!");
|
||||
let resource_dir = app_handle.path().resource_dir().map_err(|e| e.to_string())?;
|
||||
let deno_path = resource_dir.join("binaries").join(get_binary_name("deno"));
|
||||
println!("Resolved deno path: {:?}", deno_path);
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
|
||||
let output = AsyncCommand::new(&deno_path)
|
||||
let output = app_handle.shell().sidecar("deno")
|
||||
.map_err(|e| e.to_string())?
|
||||
.arg("--version")
|
||||
.output()
|
||||
.await
|
||||
@@ -308,7 +292,7 @@ async fn test_deno(app_handle: tauri::AppHandle) -> Result<String, String> {
|
||||
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() {
|
||||
let text = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
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::{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 {
|
||||
fn drop(&mut self) {
|
||||
if let Ok(mut lock) = self.0.lock() {
|
||||
if let Some(mut child) = lock.take() {
|
||||
if let Some(child) = lock.take() {
|
||||
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;
|
||||
#[allow(dead_code)]
|
||||
@@ -800,9 +754,6 @@ pub(crate) async fn start_media_download_internal(
|
||||
.unwrap_or("download")
|
||||
.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);
|
||||
|
||||
@@ -822,11 +773,9 @@ pub(crate) async fn start_media_download_internal(
|
||||
1.0
|
||||
};
|
||||
|
||||
let deno_path = resource_dir.join("binaries").join(get_binary_name("deno"));
|
||||
let mut cmd = AsyncCommand::new(&ytdlp_path);
|
||||
cmd.arg("--newline")
|
||||
.arg("--ffmpeg-location")
|
||||
.arg(&ffmpeg_path)
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
let mut cmd = app_handle.shell().sidecar("yt-dlp").map_err(|e| e.to_string())?
|
||||
.arg("--newline")
|
||||
.arg("--no-check-formats")
|
||||
.arg("--socket-timeout").arg("20")
|
||||
.arg("--retries").arg("3")
|
||||
@@ -836,37 +785,35 @@ pub(crate) async fn start_media_download_internal(
|
||||
.arg("--concurrent-fragments").arg("4")
|
||||
.arg("--no-warnings")
|
||||
.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());
|
||||
|
||||
if let Some(limit) = speed_limit {
|
||||
if !limit.is_empty() {
|
||||
cmd.arg("--limit-rate").arg(limit);
|
||||
cmd = cmd.arg("--limit-rate").arg(limit);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(p) = proxy {
|
||||
if !p.is_empty() {
|
||||
cmd.arg("--proxy").arg(p);
|
||||
cmd = cmd.arg("--proxy").arg(p);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(mut cs) = cookie_source {
|
||||
if !cs.is_empty() && cs != "none" {
|
||||
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 !ua.is_empty() {
|
||||
cmd.arg("--user-agent").arg(ua);
|
||||
cmd = cmd.arg("--user-agent").arg(ua);
|
||||
}
|
||||
}
|
||||
|
||||
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())?;
|
||||
@@ -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())?;
|
||||
let config_path = config_file.into_temp_path();
|
||||
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 {
|
||||
cmd.arg("-f").arg(format);
|
||||
cmd = cmd.arg("-f").arg(format);
|
||||
// If the filename implies an audio format, use it as audio output
|
||||
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") {
|
||||
cmd.arg("-x").arg("--audio-format").arg("m4a");
|
||||
cmd = cmd.arg("-x").arg("--audio-format").arg("m4a");
|
||||
} 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 {
|
||||
// Otherwise attempt to merge into mp4 or mkv based on filename
|
||||
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") {
|
||||
cmd.arg("--merge-output-format").arg("webm");
|
||||
cmd = cmd.arg("--merge-output-format").arg("webm");
|
||||
} else {
|
||||
cmd.arg("--merge-output-format").arg("mkv");
|
||||
cmd = cmd.arg("--merge-output-format").arg("mkv");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cmd.arg("--").arg(&url);
|
||||
cmd.stdout(Stdio::piped());
|
||||
cmd.stderr(Stdio::piped()); // Also pipe stderr for better error reporting
|
||||
cmd = cmd.arg("--").arg(&url);
|
||||
|
||||
let mut child = cmd.spawn().map_err(|e| format!("Failed to spawn yt-dlp: {}", e))?;
|
||||
let stdout = child.stdout.take().ok_or("Failed to capture stdout")?;
|
||||
let (mut rx, child) = cmd.spawn().map_err(|e| format!("Failed to spawn yt-dlp: {}", e))?;
|
||||
|
||||
// yt-dlp parsing regex
|
||||
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 _keep_alive = config_path;
|
||||
let mut reader = BufReader::new(stdout).lines();
|
||||
let mut current_track: f64 = 0.0;
|
||||
let mut last_fraction: f64 = 0.0;
|
||||
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);
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = cancel_rx.changed() => {
|
||||
let _ = child.kill().await;
|
||||
let _ = child.kill();
|
||||
return Ok(());
|
||||
}
|
||||
line_result = reader.next_line() => {
|
||||
match line_result {
|
||||
Ok(Some(line)) => {
|
||||
event = rx.recv() => {
|
||||
match event {
|
||||
Some(tauri_plugin_shell::process::CommandEvent::Stdout(line_bytes)) => {
|
||||
let line = String::from_utf8_lossy(&line_bytes);
|
||||
if line.contains("[download]") && line.contains("%") {
|
||||
let fraction = pct_re.captures(&line)
|
||||
.and_then(|cap| cap.get(1))
|
||||
.and_then(|m| m.as_str().parse::<f64>().ok())
|
||||
.unwrap_or(0.0) / 100.0;
|
||||
let fraction = if let Some(cap) = pct_re.captures(&line) {
|
||||
cap.get(1).and_then(|m| m.as_str().parse::<f64>().ok()).unwrap_or(0.0) / 100.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
if fraction < last_fraction && (last_fraction - fraction) > 0.5 {
|
||||
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 speed = spd_re.captures(&line)
|
||||
.and_then(|cap| cap.get(1))
|
||||
.map(|m| m.as_str().to_string())
|
||||
.unwrap_or_else(|| "-".to_string());
|
||||
let speed = if let Some(cap) = spd_re.captures(&line) {
|
||||
cap.get(1).map(|m| m.as_str().to_string()).unwrap_or_else(|| "-".to_string())
|
||||
} else {
|
||||
"-".to_string()
|
||||
};
|
||||
|
||||
let eta = eta_re.captures(&line)
|
||||
.and_then(|cap| cap.get(1))
|
||||
.map(|m| m.as_str().to_string())
|
||||
.unwrap_or_else(|| "-".to_string());
|
||||
let eta = if let Some(cap) = eta_re.captures(&line) {
|
||||
cap.get(1).map(|m| m.as_str().to_string()).unwrap_or_else(|| "-".to_string())
|
||||
} else {
|
||||
"-".to_string()
|
||||
};
|
||||
|
||||
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 {
|
||||
id: id.to_string(),
|
||||
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(())
|
||||
}
|
||||
|
||||
@@ -1436,34 +1391,36 @@ pub fn run() {
|
||||
|
||||
crate::scheduler::spawn_scheduler(app.handle().clone());
|
||||
|
||||
let resource_dir = app.path().resource_dir().unwrap();
|
||||
let aria2c_path = resource_dir.join("binaries").join(get_binary_name("aria2c"));
|
||||
|
||||
#[cfg(all(target_os = "macos", debug_assertions))]
|
||||
if let Err(error) = resign_aria2_debug_bundle(&aria2c_path) {
|
||||
eprintln!("{error}");
|
||||
}
|
||||
|
||||
let aria2_process = std::process::Command::new(&aria2c_path)
|
||||
.arg("--enable-rpc=true")
|
||||
.arg(format!("--rpc-listen-port={}", aria2_port))
|
||||
.arg(format!("--rpc-secret={}", aria2_secret))
|
||||
.arg("--rpc-listen-all=false")
|
||||
.arg("--continue=true")
|
||||
.arg("--allow-overwrite=false")
|
||||
.arg("--summary-interval=1")
|
||||
.arg("--console-log-level=warn")
|
||||
.arg("--download-result=hide")
|
||||
.arg("--check-certificate=true")
|
||||
.spawn();
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
let aria2_process = match app.handle().shell().sidecar("aria2c") {
|
||||
Ok(cmd) => {
|
||||
cmd.arg("--enable-rpc=true")
|
||||
.arg(format!("--rpc-listen-port={}", aria2_port))
|
||||
.arg(format!("--rpc-secret={}", aria2_secret))
|
||||
.arg("--rpc-listen-all=false")
|
||||
.arg("--continue=true")
|
||||
.arg("--allow-overwrite=false")
|
||||
.arg("--summary-interval=1")
|
||||
.arg("--console-log-level=warn")
|
||||
.arg("--download-result=hide")
|
||||
.arg("--check-certificate=true")
|
||||
.spawn()
|
||||
.map(|(_, child)| child)
|
||||
.ok()
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to create aria2c sidecar: {}", e);
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
match aria2_process {
|
||||
Ok(process) => {
|
||||
Some(process) => {
|
||||
println!("Spawned global aria2c daemon on port {}", aria2_port);
|
||||
let guard = app.state::<Aria2DaemonGuard>();
|
||||
*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();
|
||||
@@ -1574,6 +1531,7 @@ pub fn run() {
|
||||
Ok(())
|
||||
})
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(tauri_plugin_notification::init())
|
||||
.on_window_event(|window, event| {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
"devUrl": "http://localhost:1420",
|
||||
"beforeBuildCommand": "npm run build",
|
||||
"beforeBuildCommand": "node ../scripts/verify-binaries.js && npm run build",
|
||||
"frontendDist": "../dist"
|
||||
},
|
||||
"app": {
|
||||
@@ -38,6 +38,12 @@
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"externalBin": [
|
||||
"binaries/yt-dlp",
|
||||
"binaries/aria2c",
|
||||
"binaries/ffmpeg",
|
||||
"binaries/deno"
|
||||
],
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
|
||||
Reference in New Issue
Block a user