diff --git a/.gitignore b/.gitignore index 4d97691..f08a1bd 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,5 @@ target # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ +Cargo.lock +.DS_Store diff --git a/Cargo.lock b/Cargo.lock index 1e8f4a8..31a3414 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,24 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", -] - -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - [[package]] name = "anstream" version = "0.6.20" @@ -171,20 +153,21 @@ checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "config" -version = "0.14.1" +version = "0.15.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68578f196d2a33ff61b27fae256c3164f65e36382648e30666dde05b8cc9dfdf" +checksum = "0faa974509d38b33ff89282db9c3295707ccf031727c0de9772038ec526852ba" dependencies = [ "async-trait", "convert_case", "json5", - "nom", "pathdiff", "ron", "rust-ini", "serde", + "serde-untagged", "serde_json", "toml", + "winnow", "yaml-rust2", ] @@ -254,23 +237,23 @@ dependencies = [ [[package]] name = "dirs" -version = "5.0.1" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ "dirs-sys", ] [[package]] name = "dirs-sys" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.48.0", + "windows-sys 0.60.2", ] [[package]] @@ -292,10 +275,14 @@ dependencies = [ ] [[package]] -name = "equivalent" -version = "1.0.2" +name = "erased-serde" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +checksum = "e004d887f51fcb9fef17317a2f3525c887d8aa3f4f50fed920816a688284a5b7" +dependencies = [ + "serde", + "typeid", +] [[package]] name = "errno" @@ -313,6 +300,12 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "fs_extra" version = "1.3.0" @@ -357,24 +350,23 @@ name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash", - "allocator-api2", -] [[package]] name = "hashbrown" version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] [[package]] name = "hashlink" -version = "0.8.4" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "hashbrown 0.14.5", + "hashbrown 0.15.5", ] [[package]] @@ -383,16 +375,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "indexmap" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" -dependencies = [ - "equivalent", - "hashbrown 0.15.5", -] - [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -457,22 +439,6 @@ version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - [[package]] name = "once_cell" version = "1.21.3" @@ -514,7 +480,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" dependencies = [ "memchr", - "thiserror 2.0.15", + "thiserror", "ucd-trie", ] @@ -577,13 +543,13 @@ checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "redox_users" -version = "0.4.6" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.16", "libredox", - "thiserror 1.0.69", + "thiserror", ] [[package]] @@ -600,9 +566,9 @@ dependencies = [ [[package]] name = "rust-ini" -version = "0.20.0" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0698206bcb8882bf2a9ecb4c1e7785db57ff052297085a6efd4fe42302068a" +checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7" dependencies = [ "cfg-if", "ordered-multimap", @@ -636,6 +602,17 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-untagged" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34836a629bcbc6f1afdf0907a744870039b1e14c0561cb26094fa683b158eff3" +dependencies = [ + "erased-serde", + "serde", + "typeid", +] + [[package]] name = "serde_derive" version = "1.0.219" @@ -661,9 +638,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.9" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83" dependencies = [ "serde", ] @@ -709,33 +686,13 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - [[package]] name = "thiserror" version = "2.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80d76d3f064b981389ecb4b6b7f45a0bf9fdac1d5b9204c7bd6714fecc302850" dependencies = [ - "thiserror-impl 2.0.15", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", + "thiserror-impl", ] [[package]] @@ -760,44 +717,40 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.23" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit", -] - -[[package]] -name = "toml_datetime" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_edit" -version = "0.22.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" -dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "toml_write", + "toml_parser", "winnow", ] [[package]] -name = "toml_write" -version = "0.1.2" +name = "toml_datetime" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_parser" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10" +dependencies = [ + "winnow", +] + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" [[package]] name = "typenum" @@ -856,15 +809,6 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - [[package]] name = "windows-sys" version = "0.59.0" @@ -883,21 +827,6 @@ dependencies = [ "windows-targets 0.53.3", ] -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - [[package]] name = "windows-targets" version = "0.52.6" @@ -931,12 +860,6 @@ dependencies = [ "windows_x86_64_msvc 0.53.0", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -949,12 +872,6 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -967,12 +884,6 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -997,12 +908,6 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -1015,12 +920,6 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -1033,12 +932,6 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -1051,12 +944,6 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -1089,31 +976,11 @@ dependencies = [ [[package]] name = "yaml-rust2" -version = "0.8.1" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8902160c4e6f2fb145dbe9d6760a75e3c9522d8bf796ed7047c85919ac7115f8" +checksum = "4ce2a4ff45552406d02501cea6c18d8a7e50228e7736a872951fe2fe75c91be7" dependencies = [ "arraydeque", "encoding_rs", "hashlink", ] - -[[package]] -name = "zerocopy" -version = "0.8.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] diff --git a/Cargo.toml b/Cargo.toml index 2be4c81..2df4528 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jamdl" -version = "0.1.0" +version = "0.2.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -8,8 +8,8 @@ edition = "2021" [dependencies] anyhow = "1.0.82" clap = { version = "4.5.4", features = ["derive"] } -config = "0.14.0" -dirs = "5.0.1" +config = "0.15.15" +dirs = "6.0.0" fs_extra = "1.3.0" serde = { version = "1.0", features = ["derive"] } tempfile = "3.10.1" diff --git a/README.md b/README.md index e54b700..3021f6d 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A command-line tool to download music from various sources like Apple Music, Sou ## Description -`jamdl` is a wrapper around popular downloaders (`gamdl`, `scdl`, `yt-dlp`) that automates the process of downloading media and transferring it to a specified location, such as a local directory or a remote NAS via SCP. +`jamdl` is a wrapper around popular downloaders (`gamdl`, `scdl`, `yt-dlp`) that automates the process of downloading media and transferring it to a specified location, such as a local directory or a remote NAS via rsync over SSH. ## Prerequisites @@ -12,6 +12,7 @@ You must have the following command-line tools installed and available in your s - `gamdl` (for Apple Music) - `scdl` (for SoundCloud) - `yt-dlp` (for other sources) +- `rsync` (for file transfers; remote copies use rsync over SSH) ## Installation @@ -28,7 +29,7 @@ cargo install --path . You can also install it directly from a git repository: ```bash -cargo install --git +cargo install --git https://git.shed.gay/chaos/jamdl ``` ## Configuration @@ -53,6 +54,39 @@ cookies_file = "/path/to/your/apple-music-cookies-file.txt" - `nas_path`: The destination path on your NAS or local filesystem. - `cookies_file`: The path to your Apple Music cookies file for `gamdl`. +## Profiles + +`jamdl` supports multiple profiles so you can target different destinations (NAS paths, hosts, or local folders) without editing the config each time. Profiles live under a top-level `[profiles]` table. The profile named `default` is used when no `--profile` is specified. + +Example multi-profile configuration: +```toml +# Multi-profile configuration for jamdl + +[profiles.default] +nas_host = "localhost" +nas_user = "your_ssh_user" +nas_path = "/path/on/nas" +cookies_file = "/path/to/your/apple-music-cookies-file.txt" + +[profiles.work] +nas_host = "work-nas" +nas_user = "alice" +nas_path = "/srv/media/music" +cookies_file = "/home/alice/apple-cookies.txt" + +[profiles.laptop] +nas_host = "localhost" +nas_user = "your_ssh_user" +nas_path = "/Users/you/Music" +cookies_file = "/Users/you/cookies.txt" +``` + +- Choose a profile when running `jamdl`: + - `jamdl --profile work "https://music.apple.com/us/album/some-album/123456789"` + - Short form: `jamdl -p work "https://music.apple.com/us/album/some-album/123456789"` +- If the requested profile is missing, `jamdl` falls back to `default` and lists available profiles. +- Backwards compatibility: legacy single-profile configs (top-level keys without `[profiles]`) are still supported. + ## Usage Once configured, you can download media by passing a URL to `jamdl`: diff --git a/src/main.rs b/src/main.rs index 7c89e2d..b0c146f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,13 @@ use anyhow::{Context, Result}; -use clap::Parser; +use clap::{Parser, Subcommand}; use serde::Deserialize; +use std::collections::HashMap; use std::fs; use std::path::Path; use std::process::Command; use tempfile::TempDir; -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Clone)] struct Settings { nas_host: String, nas_user: String, @@ -14,22 +15,48 @@ struct Settings { cookies_file: String, } +#[derive(Debug, Deserialize)] +struct AppConfig { + profiles: HashMap, +} + +#[derive(Subcommand, Debug)] +enum Commands { + /// Show the path to the configuration file and exit + ConfigPath, +} + #[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, + url: Option, + + /// Profile name to use from config (falls back to 'default') + #[arg(short, long, default_value = "default")] + profile: String, + + /// Command to run + #[command(subcommand)] + cmd: Option, } fn main() -> Result<()> { let args = Args::parse(); - let config_dir = dirs::config_dir() - .context("Could not find config directory")? + let config_dir = dirs::home_dir() + .context("Could not find home directory")? + .join(".config") .join("jamdl"); let config_path = config_dir.join("config.toml"); + // Handle 'config-path' subcommand early without creating or reading config + if matches!(args.cmd, Some(Commands::ConfigPath)) { + println!("{}", config_path.display()); + return Ok(()); + } + 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")?; @@ -45,10 +72,39 @@ fn main() -> Result<()> { return Ok(()); } - let settings = config::Config::builder() - .add_source(config::File::from(config_path)) - .build()? - .try_deserialize::()?; + println!("[INFO] Using config file at: {}", config_path.display()); + + let cfg = config::Config::builder() + .add_source(config::File::from(config_path.clone())) + .build()?; + + let selected_profile = args.profile.clone(); + let settings: Settings = match cfg.clone().try_deserialize::() { + Ok(app_cfg) => { + if let Some(s) = app_cfg.profiles.get(&selected_profile).cloned() { + println!("[INFO] Using profile: {}", selected_profile); + s + } else if let Some(s) = app_cfg.profiles.get("default").cloned() { + if selected_profile != "default" { + println!("[WARN] Profile '{}' not found. Falling back to 'default'.", selected_profile); + let available = app_cfg.profiles.keys().cloned().collect::>().join(", "); + println!("[INFO] Available profiles: {}", available); + } else { + println!("[INFO] Using profile: default"); + } + s + } else { + anyhow::bail!("No profile '{}' found and no 'default' profile defined in config.", selected_profile); + } + } + Err(_) => { + if selected_profile != "default" { + println!("[WARN] Requested profile '{}' but config file uses single-profile format. Using that single profile.", selected_profile); + } + println!("[INFO] Using legacy single-profile configuration."); + cfg.try_deserialize::()? + } + }; // Create temp directory let temp_dir = TempDir::new().context("Failed to create temporary directory")?; @@ -58,7 +114,8 @@ fn main() -> Result<()> { temp_path.display() ); - download_media(&args.url, temp_path, &settings)?; + let url = args.url.clone().context("A URL is required when not using a subcommand. Use '--help' for usage.")?; + download_media(&url, temp_path, &settings)?; transfer_files(temp_path, &settings)?; // Cleanup is handled by TempDir's Drop trait @@ -83,12 +140,26 @@ fn download_media(video_url: &str, download_path: &Path, settings: &Settings) -> } } else if video_url.contains("soundcloud.com") { println!("[INFO] Soundcloud link detected. Using scdl..."); + let clean_url = video_url.split('?').next().unwrap(); cmd = Command::new("scdl"); - cmd.args(["-l", video_url]); + cmd.args([ + "-l", + clean_url, + "-c", + "--onlymp3", + "--yt-dlp-args", + "--embed-thumbnail --write-thumbnail", + ]); } 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.args([ + "-o", + "%(title)s.%(ext)s", + "--write-thumbnail", + "--embed-thumbnail", + video_url, + ]); } cmd.current_dir(download_path); @@ -108,27 +179,34 @@ fn transfer_files(source_path: &Path, settings: &Settings) -> Result<()> { 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"); + println!("[INFO] NAS_HOST is localhost, copying files locally via rsync..."); + let mut rsync_cmd = Command::new("rsync"); let source = format!( - "{}/.", + "{}/", + source_path.to_str().context("Invalid source path")? + ); + rsync_cmd.args(["-r", &source, &settings.nas_path]); + + let status = rsync_cmd.status().context("Failed to execute rsync")?; + if !status.success() { + anyhow::bail!("rsync failed with status: {}", status); + } + } else { + println!("[INFO] Transferring files to NAS via rsync over SSH..."); + let mut rsync_cmd = Command::new("rsync"); + 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]); + rsync_cmd.args(["-r", "-e", "ssh", &source, &destination]); - let status = scp_cmd.status().context("Failed to execute scp")?; + let status = rsync_cmd.status().context("Failed to execute rsync")?; if !status.success() { - anyhow::bail!("scp failed with status: {}", status); + anyhow::bail!("rsync failed with status: {}", status); } } } else {