Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Do not overwrite existing lyrics #3

Merged
merged 1 commit into from
Aug 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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