From 7bb90db63dbd6c982f6c9548c53d517cbeebffa7 Mon Sep 17 00:00:00 2001 From: Joel Goguen Date: Sun, 21 Jul 2024 17:14:41 -0400 Subject: [PATCH] Do not overwrite existing lyrics Instead of overwriting the lyrics file, which may have been locally modified after download, skip processing if the lyrics file is already present. ``` Skipping 06 Vengeance Venom, lyrics file exists: /opt/docker-data/jellyfin/media/music/Leaves' Eyes/King of Kings (Deluxe Version)/06 Vengeance Venom.lrc [exact_search] requesting: http://lrclib.net/api/get?track_name=09+Haraldskv%C3%A6di&artist_name=Leaves%27+Eyes&album_name=King+of+Kings+%28Deluxe+Version%29&duration=204 ``` --- README.md | 16 +++++++--- src/main.rs | 54 ++++++++++++++++++++++++++++--- tests/common/mod.rs | 5 +-- tests/data/template.lrc | 4 +++ tests/test.rs | 71 ++++++++++++++++++++++------------------- 5 files changed, 106 insertions(+), 44 deletions(-) create mode 100644 tests/data/template.lrc diff --git a/README.md b/README.md index f398b03..640f352 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,20 @@ -### Lyrics getter +# Lyrics getter -Huge thanks to https://github.com/tranxuanthang/lrclib ofc. +Huge thanks to ofc. Uses lrclib.net to get lyrics for my Jellyfin library. Does /get, if unavailable tried to do /search -Is very much dependant on having the Jellyfin suggested music library structure. (Artist/Album/Song). +Is very much dependent on having the Jellyfin suggested music library structure. (Artist/Album/Song). To run go `lyricsrs ` or clone the repo and `cargo run `. -Will overwrite any .lrc files you already have with the existing name. +Will not overwrite any .lrc files you already have with the existing name by default. -Only does synced lyrics because they are cool. +Only does synced lyrics by default because they are cool. +## Flags + +`lyricsrs` accepts command-line flags to change its behaviour: + +- `--overwrite`: Overwrite lyrics files, if present, with lyrics from lrclib +- `--allow-plain`: Allow writing plain lyrics if no synced lyrics are available diff --git a/src/main.rs b/src/main.rs index d08cce3..d34a29f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,10 +21,16 @@ use serde::Deserialize; #[derive(Parser)] struct CLI { // Flags + /// Allow plain lyrics if synced lyrics aren't available #[arg(long = "allow-plain")] allow_plain: bool, + /// Overwrite lyrics files + #[arg(long)] + overwrite: bool, + // Positional args + /// Music directory to process #[arg(required = true)] music_dir: String, } @@ -87,6 +93,7 @@ async fn main() { &music_dir, successful_count, args.allow_plain, + args.overwrite, ) .await; }) @@ -117,6 +124,7 @@ async fn parse_song_path( music_dir: &Path, successful_count: Arc, allow_plain_lyrics: bool, + overwrite_lyrics: bool, ) { if let Some(album_dir) = file_path.parent() { if let Some(artist_dir) = album_dir.parent() { @@ -129,6 +137,7 @@ async fn parse_song_path( file_path, successful_count, allow_plain_lyrics, + overwrite_lyrics, ) .await; } @@ -160,14 +169,12 @@ fn get_audio_duration(file_path: &PathBuf) -> Duration { tagged_file.properties().duration() } -async fn save_synced_lyrics( +fn lyrics_file_name( music_dir: &Path, artist_dir: &Path, album_dir: &Path, song_name: &String, - synced_lyrics: String, - successful_count: Arc, -) { +) -> String { let mut parent_dir = PathBuf::new(); parent_dir.push(music_dir); parent_dir.push(artist_dir); @@ -175,6 +182,19 @@ async fn save_synced_lyrics( let file_path = format!("{}/{}.lrc", parent_dir.to_string_lossy(), song_name); + return file_path; +} + +async fn save_synced_lyrics( + music_dir: &Path, + artist_dir: &Path, + album_dir: &Path, + song_name: &String, + synced_lyrics: String, + successful_count: Arc, +) { + let file_path = lyrics_file_name(music_dir, artist_dir, album_dir, song_name); + // Create a new file or overwrite existing one let mut file = File::create(&file_path) .await @@ -196,6 +216,7 @@ async fn exact_search( file_path: &Path, successful_count: Arc, allow_plain_lyrics: bool, + overwrite_lyrics: bool, ) { let artist_name = artist_dir .file_name() @@ -218,6 +239,18 @@ async fn exact_search( .expect("invalid file_path") .to_string_lossy() .into_owned(); + + // Skip if the lyrics file already exists + let lyrics_path = lyrics_file_name(music_dir, artist_dir, album_dir, &song_name); + if !overwrite_lyrics && Path::new(&lyrics_path).exists() { + println!( + "Skipping {}, lyrics file exists: {}", + song_name, lyrics_path + ); + successful_count.fetch_add(1, Ordering::SeqCst); + return; + } + let clean_song = remove_numbered_prefix(&song_name); let mut full_path = PathBuf::from(""); @@ -305,6 +338,7 @@ async fn exact_search( file_path, successful_count, allow_plain_lyrics, + overwrite_lyrics, ) .await; } else { @@ -327,6 +361,7 @@ async fn fuzzy_search( file_path: &Path, successful_count: Arc, allow_plain_lyrics: bool, + overwrite_lyrics: bool, ) { let artist_name = artist_dir .file_name() @@ -349,6 +384,17 @@ async fn fuzzy_search( .expect("invalid file_path") .to_string_lossy() .into_owned(); + + // Skip if the lyrics file already exists + let lyrics_path = lyrics_file_name(music_dir, artist_dir, album_dir, &song_name); + if !overwrite_lyrics && Path::new(&lyrics_path).exists() { + println!( + "Skipping {}, lyrics file exists: {}", + song_name, lyrics_path + ); + return; + } + let clean_song = remove_numbered_prefix(&song_name); let mut url = "http://lrclib.net/api/search?q=".to_string(); diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 73b123f..9d95d29 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -13,11 +13,11 @@ pub const SONGS: [&str; 11] = [ "Heilung/Drif/02 - Anoana.flac", "LINKIN PARK/Hybrid Theory/09-A Place for my Head.mp3", "LINKIN PARK/LIVING THINGS/6.CASTLE OF GLASS.flac", - "Our Lady Peace/Clumsy/5_4AM.mp3", + "Our Lady Peace/Clumsy/5_4am.mp3", "Our Lady Peace/Spiritual Machines/04 _ In Repair.mp3", ]; -async fn create_files_with_names(output_file: &PathBuf) { +pub async fn create_files_with_names(output_file: &PathBuf) { let dirs = output_file.parent().expect("could not parse dirs"); let file_name = output_file.file_name().expect("could not parse name"); @@ -34,6 +34,7 @@ async fn create_files_with_names(output_file: &PathBuf) { "flac" => "tests/data/template.flac", "mp3" => "tests/data/template.mp3", "m4a" => "tests/data/template.m4a", + "lrc" => "tests/data/template.lrc", _ => todo!(), }; diff --git a/tests/data/template.lrc b/tests/data/template.lrc new file mode 100644 index 0000000..ddab24e --- /dev/null +++ b/tests/data/template.lrc @@ -0,0 +1,4 @@ +[00:36.00] Humppa negala +[00:38.50] Humppa negala +[00:39.25] Humppa negala +[00:41.00] Venismechah diff --git a/tests/test.rs b/tests/test.rs index 2972ec7..b69909a 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -6,56 +6,61 @@ mod common; #[tokio::test] async fn test_cli() { - // TempDir deletes the created directory when the struct is dropped. Call TempDir::leak() to - // keep it around for debugging purposes. - let tmpdir = &TempDir::new().unwrap(); - common::setup(tmpdir).await; - - let target_dir = env::var("CARGO_MANIFEST_DIR").expect("could not get target dir"); - - let mut path = PathBuf::from(target_dir); - path.push("target/release/lyricsrs"); - - let output = Command::new(path) - .arg(tmpdir.path()) - .output() - .expect("Failed to execute command"); - - let stdout_str = String::from_utf8_lossy(&output.stdout); - let stderr_str = String::from_utf8_lossy(&output.stderr); - - println!("Exit code: {}", output.status.code().unwrap()); - println!("STDOUT: {}", stdout_str); - println!("STDERR: {}", stderr_str); + let args: Vec<&str> = Vec::new(); + run_test_command(&args, false).await; +} - assert!(output.status.success()); +#[tokio::test] +async fn test_cli_plain_lyrics_allowed() { + let mut args = Vec::new(); + args.push("--allow-plain"); + run_test_command(&args, false).await; +} - // keep in sync with SONGS in common/mod.rs - let to_find = format!( - "Successful tasks: {}\nFailed tasks: 0\nTotal tasks: {}", - common::SONGS.len(), - common::SONGS.len() - ); - assert!(stdout_str.contains(&to_find)); +#[tokio::test] +async fn test_cli_existing_lyrics() { + let args: Vec<&str> = Vec::new(); + run_test_command(&args, true).await; +} - assert!(common::check_lrcs(tmpdir).await); +#[tokio::test] +async fn test_cli_no_existing_lyrics_with_flag() { + let mut args = Vec::new(); + args.push("--overwrite"); + run_test_command(&args, false).await; } #[tokio::test] -async fn test_cli_plain_lyrics_allowed() { +async fn test_cli_existing_lyrics_with_flag() { + let mut args = Vec::new(); + args.push("--overwrite"); + run_test_command(&args, true).await; +} + +// Generic runner for tests that only need CLI flags changed. Optionally will create a LRC file for +// validating behaviour around lyrics file replacement. +async fn run_test_command(args: &Vec<&str>, add_lrc: bool) { // TempDir deletes the created directory when the struct is dropped. Call TempDir::leak() to // keep it around for debugging purposes. let tmpdir = &TempDir::new().unwrap(); common::setup(tmpdir).await; + if add_lrc { + let mut file_name = tmpdir.child(common::SONGS[3]); + file_name.set_extension("lrc"); + common::create_files_with_names(&file_name).await; + } + let target_dir = env::var("CARGO_MANIFEST_DIR").expect("could not get target dir"); let mut path = PathBuf::from(target_dir); path.push("target/release/lyricsrs"); + let mut cmd_args = Vec::new(); + cmd_args.clone_from(args); + cmd_args.push(tmpdir.path().to_str().unwrap()); let output = Command::new(path) - .arg("--allow-plain") - .arg(tmpdir.path()) + .args(cmd_args) .output() .expect("Failed to execute command");