use crate::core::{
download_mode::{DownloadSource, DownloadType, Quality},
flags::Flag,
};
use dioxus::prelude::*;
use std::{
process::Stdio,
sync::{Arc, Mutex},
};
use tokio::{
io::{AsyncBufReadExt, BufReader},
process::{Child, Command},
};
pub type ChildHandle = Arc<Mutex<Option<Child>>>;
#[derive(Debug, Clone)]
pub struct DownloadRequest {
pub url: String,
pub batch_file: String,
pub archive_file: String,
pub download_type: DownloadType,
pub download_source: DownloadSource,
pub quality: Quality,
pub output_dir: String,
pub extra_flags: Vec<Flag>,
}
pub fn build_command_string(req: &DownloadRequest) -> String {
let has_input = !req.url.trim().is_empty() || !req.batch_file.trim().is_empty();
if !has_input {
return "yt-dlp [paste a URL or pick a batch file above]".to_string();
}
let exec_args = build_exec_args(req);
let display: Vec<String> = exec_args
.iter()
.map(|a| {
if a.contains(' ') || a.contains('[') || a.contains('%') || a.contains('>') {
format!("\"{}\"", a)
} else {
a.clone()
}
})
.collect();
format!("yt-dlp {}", display.join(" "))
}
pub fn cancel_download(
handle: &ChildHandle,
mut log_lines: Signal<Vec<String>>,
mut is_running: Signal<bool>,
) {
let mut lock = handle.lock().unwrap();
if let Some(child) = lock.as_mut() {
match child.start_kill() {
Ok(_) => {
log_lines
.write()
.push("⛔ Download cancelled by user.".to_string());
}
Err(e) => {
log_lines
.write()
.push(format!("✗ Failed to kill process: {e}"));
}
}
*lock = None;
}
is_running.set(false);
}
pub async fn run_download(
req: DownloadRequest,
mut log_lines: Signal<Vec<String>>,
mut is_running: Signal<bool>,
child_handle: ChildHandle,
) {
is_running.set(true);
log_lines.write().clear();
let has_input = !req.url.trim().is_empty()
|| (req.download_source == DownloadSource::Batch && !req.batch_file.trim().is_empty());
if !has_input {
log_lines
.write()
.push("⚠ Please enter a URL or pick a batch file first.".to_string());
is_running.set(false);
return;
}
log_lines.write().push("▶ Starting download…".to_string());
log_lines
.write()
.push(format!(" {}", build_command_string(&req)));
let args = build_exec_args(&req);
let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/bash".to_string());
let result = Command::new(&shell)
.arg("-i")
.arg("-c")
.arg("yt-dlp \"$@\"") .arg("bash") .args(&args) .stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn();
match result {
Err(e) => {
log_lines
.write()
.push(format!("✗ Failed to spawn yt-dlp: {e}"));
log_lines
.write()
.push(" Make sure yt-dlp is installed and in your PATH.".to_string());
is_running.set(false);
}
Ok(mut child) => {
let stdout = child.stdout.take();
let stderr = child.stderr.take();
{
let mut lock = child_handle.lock().unwrap();
*lock = Some(child);
}
if let Some(stdout) = stdout {
let mut reader = BufReader::new(stdout).lines();
while let Ok(Some(line)) = reader.next_line().await {
log_lines.write().push(line);
}
}
if let Some(stderr) = stderr {
let mut reader = BufReader::new(stderr).lines();
while let Ok(Some(line)) = reader.next_line().await {
log_lines.write().push(format!("⚠ {line}"));
}
}
let child_opt = {
let mut lock = child_handle.lock().unwrap();
lock.take()
};
if let Some(mut child) = child_opt {
match child.wait().await {
Ok(status) if status.success() => {
log_lines.write().push("✔ Done!".to_string());
}
Ok(status) => {
if *is_running.read() {
log_lines
.write()
.push(format!("✗ yt-dlp exited with: {status}"));
}
}
Err(e) => {
log_lines.write().push(format!("✗ Wait error: {e}"));
}
}
}
is_running.set(false);
}
}
}
pub async fn run_raw_command(
raw: String,
mut log_lines: Signal<Vec<String>>,
mut is_running: Signal<bool>,
child_handle: ChildHandle,
) {
is_running.set(true);
log_lines.write().push(format!("$ {raw}"));
if raw.trim().is_empty() {
is_running.set(false);
return;
}
let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/bash".to_string());
let result = Command::new(&shell)
.args(["-i", "-c", &raw])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn();
match result {
Err(e) => {
log_lines.write().push(format!("✗ {e}"));
is_running.set(false);
}
Ok(mut child) => {
let stdout = child.stdout.take();
let stderr = child.stderr.take();
{
let mut lock = child_handle.lock().unwrap();
*lock = Some(child);
}
if let Some(stdout) = stdout {
let mut reader = BufReader::new(stdout).lines();
while let Ok(Some(line)) = reader.next_line().await {
log_lines.write().push(line);
}
}
if let Some(stderr) = stderr {
let mut reader = BufReader::new(stderr).lines();
while let Ok(Some(line)) = reader.next_line().await {
log_lines.write().push(format!("⚠ {line}"));
}
}
let child_opt = {
let mut lock = child_handle.lock().unwrap();
lock.take()
};
if let Some(mut child) = child_opt {
let _ = child.wait().await;
}
is_running.set(false);
}
}
}
fn build_exec_args(req: &DownloadRequest) -> Vec<String> {
let mut args: Vec<String> = Vec::new();
match req.download_type {
DownloadType::Video => {
args.push("-f".to_string());
args.push(req.quality.format_string().to_string());
}
DownloadType::Audio => {
args.push("-x".to_string());
}
}
let template = req.download_source.output_template(&req.output_dir);
args.push("-o".to_string());
args.push(template);
if !req.archive_file.trim().is_empty() {
args.push("--download-archive".to_string());
args.push(req.archive_file.clone());
}
for flag in &req.extra_flags {
for token in flag.flag.split_whitespace() {
args.push(token.to_string());
}
}
match &req.download_source {
DownloadSource::Batch if !req.batch_file.trim().is_empty() => {
args.push("--batch-file".to_string());
args.push(req.batch_file.clone());
}
_ if !req.url.trim().is_empty() => {
args.push(req.url.clone());
}
_ => {}
}
args
}