mirror of
https://github.com/arabianq/lyrics_fetcher.git
synced 2026-04-28 08:01:22 +00:00
initial commit
This commit is contained in:
+221
@@ -0,0 +1,221 @@
|
||||
mod sources;
|
||||
|
||||
use sources::*;
|
||||
|
||||
use anyhow::Result;
|
||||
use async_walkdir::WalkDir;
|
||||
use itertools::Itertools;
|
||||
|
||||
use futures::StreamExt;
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
process::exit,
|
||||
sync::Arc,
|
||||
};
|
||||
use tokio::sync::Semaphore;
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
use id3::{Tag, TagLike, Version, frame::Lyrics};
|
||||
|
||||
#[derive(Parser)]
|
||||
struct Args {
|
||||
#[arg(required = true)]
|
||||
music_dir_path: PathBuf,
|
||||
|
||||
#[arg(short, long, default_value_t = 1)]
|
||||
threads: usize,
|
||||
|
||||
#[arg(short, long, default_value_t = false)]
|
||||
overwrite_existing: bool,
|
||||
|
||||
#[arg(short, long, default_value_t = false)]
|
||||
allow_inaccurate: bool,
|
||||
|
||||
#[arg(short, long, default_value = "lrclib", value_delimiter = ',')]
|
||||
sources: Vec<String>,
|
||||
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
default_value = "flac,mp3,ogg,opus,aac,wav,m4a,alac,aiff,ape",
|
||||
value_delimiter = ','
|
||||
)]
|
||||
extensions: Vec<String>,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let args = Args::parse();
|
||||
|
||||
let music_dir_path = args.music_dir_path;
|
||||
let threads = args.threads;
|
||||
let overwrite = args.overwrite_existing;
|
||||
let allow_inaccurate = args.allow_inaccurate;
|
||||
let lyrics_sources_raw = args.sources;
|
||||
let extensions: Vec<String> = args.extensions.iter().map(|s| s.to_lowercase()).collect();
|
||||
|
||||
if !music_dir_path.exists() {
|
||||
eprintln!("[ERROR] The specified music directory does not exist.");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if threads < 1 {
|
||||
eprintln!("[ERROR] The number of threads must be at least 1.");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if lyrics_sources_raw.is_empty() {
|
||||
eprintln!("[ERROR] At least one source must be specified.");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if extensions.is_empty() {
|
||||
eprintln!("[ERROR] At least one file extension must be specified.");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
let lyrics_sources_raw: Vec<String> = lyrics_sources_raw
|
||||
.iter()
|
||||
.unique()
|
||||
.map(|s| s.to_lowercase())
|
||||
.collect();
|
||||
|
||||
println!("[INFO] Music directory: {}", music_dir_path.display());
|
||||
println!("[INFO] Number of threads: {}", threads);
|
||||
println!("[INFO] Sources: {}", lyrics_sources_raw.join(", "));
|
||||
|
||||
let mut sources: Vec<Arc<dyn LyricsSource>> = vec![];
|
||||
for source_name in lyrics_sources_raw {
|
||||
let source_name = source_name.trim().to_lowercase();
|
||||
|
||||
match create_source(&source_name).await {
|
||||
Ok(source) => sources.push(source),
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"[WARNING] Failed to initialize source '{}', skipping: {}",
|
||||
source_name, e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if sources.is_empty() {
|
||||
eprintln!("[ERROR] No valid sources were initialized. Exiting.");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
let sources = Arc::new(sources);
|
||||
let semaphone = Arc::new(Semaphore::new(threads));
|
||||
let mut entries = WalkDir::new(music_dir_path);
|
||||
let mut tasks = Vec::new();
|
||||
|
||||
while let Some(entry) = entries.next().await {
|
||||
match entry {
|
||||
Ok(entry) => {
|
||||
if entry.file_type().await?.is_file()
|
||||
&& extensions.contains(
|
||||
&entry
|
||||
.path()
|
||||
.extension()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_lowercase(),
|
||||
)
|
||||
{
|
||||
let file_path = entry.path().to_path_buf();
|
||||
let semaphone_clone = Arc::clone(&semaphone);
|
||||
let sources_clone = Arc::clone(&sources);
|
||||
let task = tokio::spawn(async move {
|
||||
let _permit = semaphone_clone.acquire().await.unwrap();
|
||||
process_file(&file_path, sources_clone, overwrite, allow_inaccurate).await;
|
||||
});
|
||||
tasks.push(task);
|
||||
}
|
||||
}
|
||||
Err(e) => eprintln!("[ERROR] Failed to read directory entry: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
for task in tasks {
|
||||
let _ = task.await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn process_file(
|
||||
file_path: &Path,
|
||||
sources: Arc<Vec<Arc<dyn LyricsSource>>>,
|
||||
overwrite: bool,
|
||||
allow_inaccurate: bool,
|
||||
) {
|
||||
let tag = Tag::async_read_from_path(&file_path).await;
|
||||
|
||||
match tag {
|
||||
Ok(tag) => {
|
||||
let lyrics: Vec<&Lyrics> = tag.lyrics().collect();
|
||||
if !lyrics.is_empty() && !overwrite {
|
||||
println!(
|
||||
"[INFO] File '{}' already has lyrics, skipping (use --overwrite to force)",
|
||||
file_path.display()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
for source in sources.iter() {
|
||||
match source.fetch_lyrics(&tag, allow_inaccurate).await {
|
||||
Ok(lyrics) => {
|
||||
let lyrics = lyrics.trim();
|
||||
if lyrics.is_empty() {
|
||||
println!(
|
||||
"[INFO] Source '{}' did not return any lyrics for file '{}'",
|
||||
source.name(),
|
||||
file_path.display()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
println!(
|
||||
"[INFO] Successfully fetched lyrics for file '{}' from source '{}'",
|
||||
file_path.display(),
|
||||
source.name()
|
||||
);
|
||||
|
||||
let mut tag = tag.clone();
|
||||
tag.remove_all_lyrics();
|
||||
tag.remove_all_synchronised_lyrics();
|
||||
|
||||
tag.add_frame(Lyrics {
|
||||
lang: "XXX".to_string(),
|
||||
description: format!("Fetched from {}", source.name()),
|
||||
text: lyrics.to_string(),
|
||||
});
|
||||
|
||||
tag.write_to_path(&file_path, Version::Id3v24)
|
||||
.map_err(|e| {
|
||||
eprintln!(
|
||||
"[ERROR] Failed to write tags into {}: {}",
|
||||
file_path.display(),
|
||||
e
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
break;
|
||||
}
|
||||
Err(e) => eprintln!(
|
||||
"[ERROR] Failed to fetch lyrics for file '{}' from source '{}': {}",
|
||||
file_path.display(),
|
||||
source.name(),
|
||||
e
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => eprintln!(
|
||||
"[ERROR] Failed to read ID3 tag for file '{}': {}",
|
||||
file_path.display(),
|
||||
e
|
||||
),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user