Skip to content

Commit

Permalink
Do not overwrite existing lyrics
Browse files Browse the repository at this point in the history
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
```
  • Loading branch information
jgoguen committed Jul 26, 2024
1 parent f2cbacd commit 7bb90db
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 44 deletions.
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
### Lyrics getter
# Lyrics getter

Huge thanks to https://github.com/tranxuanthang/lrclib ofc.
Huge thanks to <https://github.com/tranxuanthang/lrclib> 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 <music_directory>` or clone the repo and `cargo run <music_directory>`.

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
54 changes: 50 additions & 4 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down Expand Up @@ -87,6 +93,7 @@ async fn main() {
&music_dir,
successful_count,
args.allow_plain,
args.overwrite,
)
.await;
})
Expand Down Expand Up @@ -117,6 +124,7 @@ async fn parse_song_path(
music_dir: &Path,
successful_count: Arc<AtomicUsize>,
allow_plain_lyrics: bool,
overwrite_lyrics: bool,
) {
if let Some(album_dir) = file_path.parent() {
if let Some(artist_dir) = album_dir.parent() {
Expand All @@ -129,6 +137,7 @@ async fn parse_song_path(
file_path,
successful_count,
allow_plain_lyrics,
overwrite_lyrics,
)
.await;
}
Expand Down Expand Up @@ -160,21 +169,32 @@ 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<AtomicUsize>,
) {
) -> String {
let mut parent_dir = PathBuf::new();
parent_dir.push(music_dir);
parent_dir.push(artist_dir);
parent_dir.push(album_dir);

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<AtomicUsize>,
) {
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
Expand All @@ -196,6 +216,7 @@ async fn exact_search(
file_path: &Path,
successful_count: Arc<AtomicUsize>,
allow_plain_lyrics: bool,
overwrite_lyrics: bool,
) {
let artist_name = artist_dir
.file_name()
Expand All @@ -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("");
Expand Down Expand Up @@ -305,6 +338,7 @@ async fn exact_search(
file_path,
successful_count,
allow_plain_lyrics,
overwrite_lyrics,
)
.await;
} else {
Expand All @@ -327,6 +361,7 @@ async fn fuzzy_search(
file_path: &Path,
successful_count: Arc<AtomicUsize>,
allow_plain_lyrics: bool,
overwrite_lyrics: bool,
) {
let artist_name = artist_dir
.file_name()
Expand All @@ -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();

Expand Down
5 changes: 3 additions & 2 deletions tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand All @@ -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!(),
};

Expand Down
4 changes: 4 additions & 0 deletions tests/data/template.lrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[00:36.00] Humppa negala
[00:38.50] Humppa negala
[00:39.25] Humppa negala
[00:41.00] Venismechah
71 changes: 38 additions & 33 deletions tests/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down

0 comments on commit 7bb90db

Please sign in to comment.