use anyhow::{Context, Result}; use clap::Parser; use serde::Deserialize; use std::fs; use std::path::Path; use std::process::Command; use tempfile::TempDir; #[derive(Debug, Deserialize)] struct Settings { nas_host: String, nas_user: String, nas_path: String, cookies_file: String, } #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] struct Args { /// The URL of the video/music to download #[arg(name = "URL")] url: String, } fn main() -> Result<()> { let args = Args::parse(); let config_dir = dirs::config_dir() .context("Could not find config directory")? .join("jamdl"); let config_path = config_dir.join("config.toml"); if !config_path.exists() { println!("[INFO] No config file found, creating a default one..."); fs::create_dir_all(&config_dir).context("Failed to create config directory")?; let default_config = include_str!("../config.toml"); fs::write(&config_path, default_config).context("Failed to write default config file")?; println!( "[INFO] A default config file was created at: {}", config_path.display() ); println!("[INFO] Please edit this file with your settings and re-run the command."); return Ok(()); } let settings = config::Config::builder() .add_source(config::File::from(config_path)) .build()? .try_deserialize::()?; // Create temp directory let temp_dir = TempDir::new().context("Failed to create temporary directory")?; let temp_path = temp_dir.path(); println!( "[INFO] Created temporary directory at: {}", temp_path.display() ); download_media(&args.url, temp_path, &settings)?; transfer_files(temp_path, &settings)?; // Cleanup is handled by TempDir's Drop trait println!("[INFO] Cleaning up..."); Ok(()) } fn download_media(video_url: &str, download_path: &Path, settings: &Settings) -> Result<()> { let mut cmd: Command; if video_url.contains("music.apple.com") { println!("[INFO] Apple Music link detected. Using gamdl..."); cmd = Command::new("gamdl"); if Path::new(&settings.cookies_file).exists() { cmd.args(["--cookies-path", &settings.cookies_file, video_url]); } else { println!( "[WARN] cookies.txt not found at {} — running gamdl without it", settings.cookies_file ); cmd.arg(video_url); } } else if video_url.contains("soundcloud.com") { println!("[INFO] Soundcloud link detected. Using scdl..."); cmd = Command::new("scdl"); cmd.args(["-l", video_url, "--no-mtime"]); } else { println!("[INFO] Non-Apple Music link. Using yt-dlp..."); cmd = Command::new("yt-dlp"); cmd.args(["-o", "%(title)s.%(ext)s", video_url]); } cmd.current_dir(download_path); let status = cmd .status() .with_context(|| format!("Failed to execute command: {:?}", cmd))?; if !status.success() { anyhow::bail!("Download command failed with status: {}", status); } Ok(()) } fn transfer_files(source_path: &Path, settings: &Settings) -> Result<()> { let is_empty = fs::read_dir(source_path)?.next().is_none(); if !is_empty { if settings.nas_host == "localhost" { println!("[INFO] NAS_HOST is localhost, copying files locally..."); let mut options = fs_extra::dir::CopyOptions::new(); options.content_only = true; fs_extra::dir::copy(source_path, &settings.nas_path, &options) .context("Failed to copy files locally")?; } else { println!("[INFO] Transferring files to NAS via scp..."); let mut scp_cmd = Command::new("scp"); let source = format!( "{}/.", source_path.to_str().context("Invalid source path")? ); let destination = format!( "{}@{}:{}", settings.nas_user, settings.nas_host, settings.nas_path ); scp_cmd.args(["-r", &source, &destination]); let status = scp_cmd.status().context("Failed to execute scp")?; if !status.success() { anyhow::bail!("scp failed with status: {}", status); } } } else { println!("[WARN] No files found to transfer."); } Ok(()) }