From 200e2afe2efddbc70f7fa4fb8bea5fa26a45e398 Mon Sep 17 00:00:00 2001 From: Frieder Hannenheim Date: Sat, 8 Jul 2023 23:57:35 +0200 Subject: [PATCH] - better windows support - rename now supports paths (so you can set /path/to/music/%(album)/%(song) as a template) - yt-dlp-args option are now used as extra arguments instead of replacing the default arguments - module config is done under module.name in the toml now --- Cargo.lock | 96 +++++++++++++++++++++++++++++++++++----- Cargo.toml | 1 + README.md | 24 +++++++--- config/default.toml | 45 +++++++++++++++++++ config_example.toml | 28 ------------ src/config.rs | 43 +++++++++++++++--- src/modules/download.rs | 9 ++-- src/modules/infocopy.rs | 2 +- src/modules/rename.rs | 46 +++++++++++-------- src/modules/tagui/mod.rs | 2 +- 10 files changed, 223 insertions(+), 73 deletions(-) create mode 100644 config/default.toml delete mode 100644 config_example.toml diff --git a/Cargo.lock b/Cargo.lock index 991880c..db6f6f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -303,6 +303,15 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "home" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +dependencies = [ + "windows-sys 0.48.0", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -443,7 +452,7 @@ dependencies = [ "libc", "log", "wasi", - "windows-sys", + "windows-sys 0.45.0", ] [[package]] @@ -454,6 +463,7 @@ dependencies = [ "cursive", "cursive-aligned-view", "fs_extra", + "home", "infer", "lexopt", "lofty", @@ -583,7 +593,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-sys", + "windows-sys 0.45.0", ] [[package]] @@ -1108,7 +1118,16 @@ version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ - "windows-targets", + "windows-targets 0.42.2", +] + +[[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.1", ] [[package]] @@ -1117,13 +1136,28 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +dependencies = [ + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", ] [[package]] @@ -1132,42 +1166,84 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + [[package]] name = "windows_i686_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + [[package]] name = "windows_i686_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + [[package]] name = "winnow" version = "0.4.6" diff --git a/Cargo.toml b/Cargo.toml index 7aa35c2..ccd8004 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,3 +23,4 @@ infer = "0.13.0" log = "0.4.17" fs_extra = "1.3.0" simplelog = "0.12.1" +home = "0.5.5" diff --git a/README.md b/README.md index 5e8e7f7..eee77f4 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,6 @@ Musicfetch is a tool for downloading music from Youtube and other platforms. It ![GitHub](https://img.shields.io/github/license/FriederHannenheim/Musicfetch?logo=gnu) -This is the code for the rework. Versions < v1.0 can be found on the branch `old` - ## Supported Song metadata: - Title - Album Title @@ -18,9 +16,22 @@ This is the code for the rework. Versions < v1.0 can be found on the branch `old Select the nightly Rust toolchain and enter `cargo build --release`. To install musicfetch enter `cargo install --path .` ## Usage -This is the branch for musicfetch >= v1.0. v1.0 was rebuilt from the ground up with massive changes under the hood. Right now it is in it's alpha stage, the core functionality is there but there are still things to be done to make it actually usable. - -You can try it now by placing the `config_example.toml` under `/etc/musicfetch.toml` and invoking musicfetch with a link to an Album on youtube. +``` +musicfetch + +Usage: + musicfetch ... + musicfetch (-c | --cover_url) + musicfetch (-o | --output_dir) + musicfetch -? | -h | --help + +Options: + -? -h --help Show this help + -v --version Print version and exit + -c --cover_url Specify the url of the cover that should be added to the songs + -o --output_dir Specify the directory the songs should be downloaded to + -C --config Use the config with this name +``` ### UI The UI for entering Metadata has been designed to need as few key presses as possible to get to where you want. @@ -30,6 +41,9 @@ On the left you can select the song you want to edit. In front of the song title When you change the track number of a song, they will be reordered in the selectview to reflect that change. Use Shift+Up or Shift+Down to increase or decrease the track number for a song. Alternatively, use the number keys 1-9 to set it directly. +### Configuration +Under $XDG_CONFIG_HOME/musicfetch or $HOME/.config/musicfetch you can find and place .toml files with configuration. `default.toml` is the default and is documented well. Look in there for a list of options and explainations. + ## Dependencies - [yt-dlp](https://github.com/yt-dlp/yt-dlp) diff --git a/config/default.toml b/config/default.toml new file mode 100644 index 0000000..8bacdb5 --- /dev/null +++ b/config/default.toml @@ -0,0 +1,45 @@ + +# This specifies the stages and modules musicfetch will run. +# Stages are run in parallel so be careful not to introduce any +# race conditions by including too many modules in one stage. +[stages] +stage1 = ["fetch_song_info"] +stage2 = ["infocopy"] +stage3 = [ + "albumui", + "trackcounter" +] +stage4 = [ + "tagui", + "download" +] +stage5 = [ + "tag_files", + "albumcover" +] +stage6 = ["rename"] + + +# Infocopy copies values from the yt-dlp json to the songinfo +[module.infocopy] +title = "track" +album = "album" +artist = "artist" + +[module.rename] +# Template for filepaths. Can include paths to folders. These should be absolute, environment variables and '~' are not parsed. +# Variables can be entered like this %(name) +# Available variables: +# %(title) - Song Title +# %(album) - Album Name +# %(artist) - Artist Name +# %(genre) - Genre +# %(year) - Release year +# %(track_no) - Track Number +# %(total_tracks) - Total Tracks in Album +template = "%(title).%(ext)" + +[module.download] +# Extra arguments to give to yt-dlp. For example ['--audio-format', 'mp3'] if you want to download everything as mp3 +yt_dlp_args = [] + diff --git a/config_example.toml b/config_example.toml deleted file mode 100644 index 7d9c35c..0000000 --- a/config_example.toml +++ /dev/null @@ -1,28 +0,0 @@ - - -[stages] -stage1 = ["fetch_song_info"] -stage2 = ["infocopy"] -stage3 = [ - "albumui", - "trackcounter" -] -stage4 = [ - "tagui", - "download" -] -stage5 = [ - "tag_files", - "albumcover" -] -stage6 = ["rename"] - - -# Infocopy copies values from the yt-dlp json to the songinfo -[stages.infocopy] -title = "track" -album = "album" -artist = "artist" - -[stages.rename] -template = "%(artist) - %(title).%(ext)" diff --git a/src/config.rs b/src/config.rs index 03d51cd..e2ce593 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,18 +1,50 @@ -use std::{env, path::PathBuf, fs}; +use std::{env, path::PathBuf, fs::{self, create_dir_all, File}, io::Write}; +use home::home_dir; use log::info; use serde_json::Value; use anyhow::{Result, bail, Context}; - +const DEFAULT_CONFIG: &'static[u8] = include_bytes!("../config/default.toml"); pub fn get_config(name: &str) -> Result { - let Some(dir) = get_config_dir() else { - bail!("Failed finding configuration directory"); + let dir = match get_config_dir() { + Some(dir) => dir, + None => { + eprintln!("Failed to find musicfetch config dir. Creating default config..."); + create_default_config()? + } }; get_config_by_name(name, dir) } +fn create_default_config() -> Result { + let mut config_dir = match env::var("XDG_CONFIG_HOME") { + Ok(dir) => PathBuf::from(dir), + Err(_) => { + match home_dir() { + Some(mut dir) => { + dir.push(".config"); + dir + }, + None => bail!("Failed to find config directory") + } + } + }; + config_dir.push("musicfetch"); + + create_dir_all(&config_dir)?; + + let mut config_path = config_dir.clone(); + config_path.push("default.toml"); + + let mut config_file = File::create(&config_path)?; + + config_file.write(DEFAULT_CONFIG)?; + + Ok(config_dir) +} + fn get_config_by_name(name: &str, dir: PathBuf) -> Result { let mut file_path = dir; file_path.push(format!("{}.toml", name)); @@ -41,8 +73,7 @@ fn get_user_config_dir() -> Option { } } - if let Ok(home_dir) = env::var("HOME") { - let mut path = PathBuf::from(home_dir); + if let Some(mut path) = home_dir() { path.push(".config"); path.push("musicfetch"); diff --git a/src/modules/download.rs b/src/modules/download.rs index 76ef9ce..e1f91b2 100644 --- a/src/modules/download.rs +++ b/src/modules/download.rs @@ -84,7 +84,8 @@ fn download(yt_dlp_json: &str, args: &Vec) -> Result<()> { } fn get_yt_dlp_args(module_config: Option) -> Vec { - match module_config.and_then(|v| v["yt_dlp_args"].as_array().map(|v| v.to_owned())) { + let mut args = YT_DLP_ARGS.map(|s| s.to_owned()).to_vec(); + let mut extra_args = match module_config.and_then(|v| v["yt_dlp_args"].as_array().map(|v| v.to_owned())) { Some(v) => v .into_iter() .map(|v| { @@ -93,8 +94,10 @@ fn get_yt_dlp_args(module_config: Option) -> Vec { .to_owned() }) .collect(), - None => YT_DLP_ARGS.map(|s| s.to_owned()).to_vec(), - } + None => vec![], + }; + args.append(&mut extra_args); + args } fn get_downloaded_filename(yt_dlp_json: &str, args: &Vec) -> Result { diff --git a/src/modules/infocopy.rs b/src/modules/infocopy.rs index 1608afa..99f7f7f 100644 --- a/src/modules/infocopy.rs +++ b/src/modules/infocopy.rs @@ -22,7 +22,7 @@ impl Module for InfocopyModule { // TODO: Allow using regex with captures to copy only parts of strings fn run(global: Arc>, songs: Arc>) -> Result<()> { let global = global.lock().unwrap(); - let infocopy_settings = global["config"]["stages"] + let infocopy_settings = global["config"]["module"] .get("infocopy") .expect("Module infocopy has no settings") .as_object() diff --git a/src/modules/rename.rs b/src/modules/rename.rs index e28d41b..ab5afa7 100644 --- a/src/modules/rename.rs +++ b/src/modules/rename.rs @@ -2,9 +2,10 @@ use crate::{modules::download::DownloadModule, module_util::{song_to_string, get use fs_extra::file::{CopyOptions, self}; +use log::info; use regex::Regex; use serde_json::Value; -use std::{sync::{Arc, Mutex}, path::PathBuf, str::FromStr}; +use std::{sync::{Arc, Mutex}, path::PathBuf, str::FromStr, fs::create_dir_all}; use anyhow::{Result, bail}; @@ -27,7 +28,7 @@ impl Module for RenameModule { let global = global.lock().unwrap(); let global = global.as_object().unwrap(); - let name_template = match &global["config"]["stages"]["rename"]["template"] { + let name_template = match &global["config"]["module"]["rename"]["template"] { Value::String(template) => template.to_owned(), _ => { log::warn!("No rename template in config. Using default"); @@ -39,13 +40,20 @@ impl Module for RenameModule { let ext = get_songinfo_field::(song, "path")?.split(".").last().expect("Song path has no file extension"); song["songinfo"]["ext"] = Value::from(ext); - let filename = get_filename_for_song(&name_template, song)?; + let filename = get_path_for_song(&name_template, song)?; let old_path = PathBuf::from_str(&get_songinfo_field(song, "path")?)?; - let mut new_path = old_path.clone().parent().unwrap().to_path_buf(); - new_path.push(filename); + let new_path = PathBuf::from(filename); + if new_path.is_absolute() && new_path.parent().is_some() { + let mut dir = new_path.clone(); + dir.pop(); + + create_dir_all(dir)?; + } + + info!("renaming file to {}", new_path.display()); file::move_file(old_path, &new_path, &CopyOptions::new())?; song["songinfo"]["path"] = Value::from(new_path.to_str().expect(&format!("Filepath for '{}' is not valid utf-8", song))); @@ -55,13 +63,13 @@ impl Module for RenameModule { } } -fn get_filename_for_song(name_template: &str, song: &Value) -> Result { - let mut filename = name_template.to_owned(); +fn get_path_for_song(path_template: &str, song: &Value) -> Result { + let mut path = path_template.to_owned(); let re = Regex::new(r"%\((\w+)\)").unwrap(); - for caps in re.captures_iter(name_template) { + for caps in re.captures_iter(path_template) { let matched_string = &caps[0]; - let value = match song["songinfo"][&caps[1]].clone() { + let mut value = match song["songinfo"][&caps[1]].clone() { Value::Null => bail!("Song '{}' has no field '{}' or field is empty", song_to_string(&song), &caps[1]), Value::Bool(b) => b.to_string(), Value::Number(n) => n.to_string(), @@ -69,12 +77,12 @@ fn get_filename_for_song(name_template: &str, song: &Value) -> Result { Value::Array(_) => bail!("Field '{}' is an array", &caps[1]), Value::Object(_) => bail!("Field '{}' is an object", &caps[1]), }; + value = value.replace("/", "_"); - filename = filename.replace(matched_string, &value); + path = path.replace(matched_string, &value); } - filename = filename.replace("/", "_"); - Ok(filename) + Ok(path) } @@ -82,7 +90,7 @@ fn get_filename_for_song(name_template: &str, song: &Value) -> Result { mod tests { use serde_json::json; - use super::get_filename_for_song; + use super::get_path_for_song; #[test] fn test_filename_creation() { @@ -94,9 +102,9 @@ mod tests { } }); - assert_eq!("Testartist - Test Song", get_filename_for_song("%(artist) - %(title)", &song).unwrap()); - assert_eq!("1994 Test Song", get_filename_for_song("%(year) %(title)", &song).unwrap()); - assert_eq!("lalala", get_filename_for_song("lalala", &song).unwrap()); + assert_eq!("Testartist - Test Song", get_path_for_song("%(artist) - %(title)", &song).unwrap()); + assert_eq!("1994 Test Song", get_path_for_song("%(year) %(title)", &song).unwrap()); + assert_eq!("lalala", get_path_for_song("lalala", &song).unwrap()); } #[test] @@ -110,8 +118,8 @@ mod tests { } }); - assert!(get_filename_for_song("%(title)", &song).is_err()); - assert!(get_filename_for_song("%(album)", &song).is_err()); - assert!(get_filename_for_song("%(year)", &song).is_err()); + assert!(get_path_for_song("%(title)", &song).is_err()); + assert!(get_path_for_song("%(album)", &song).is_err()); + assert!(get_path_for_song("%(year)", &song).is_err()); } } diff --git a/src/modules/tagui/mod.rs b/src/modules/tagui/mod.rs index 8034459..b0a4afe 100644 --- a/src/modules/tagui/mod.rs +++ b/src/modules/tagui/mod.rs @@ -13,7 +13,7 @@ use self::{ use super::Module; -use anyhow::{Result, bail}; +use anyhow::Result; mod dialog; mod song_edit;