Compare commits
2 Commits
87508b6908
...
ce3eaac9c2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce3eaac9c2 | ||
|
|
b7b3a63cf5 |
@ -1,10 +1,20 @@
|
||||
use crate::lrclib;
|
||||
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fs;
|
||||
use std::io::{self, Write};
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum LyricsResult {
|
||||
Synced(String),
|
||||
Plain(String),
|
||||
Instrumental,
|
||||
None,
|
||||
}
|
||||
|
||||
const CONFIG_PATH: &str = "/tmp/lrcli.json";
|
||||
|
||||
type Config = HashMap<String, HashMap<String, String>>;
|
||||
@ -14,19 +24,43 @@ pub fn get_lyrics_text(
|
||||
track_name: &str,
|
||||
album_name: &str,
|
||||
duration: Duration,
|
||||
) -> Result<Value, String> {
|
||||
) -> Result<LyricsResult, String> {
|
||||
let lyrics_json = lrclib::get_lyrics(artist_name, track_name, album_name, duration)
|
||||
.ok_or_else(|| format!("Failed to fetch lyrics for {track_name}"))?;
|
||||
|
||||
let parsed: Value = serde_json::from_str(&lyrics_json)
|
||||
.map_err(|e| format!("Failed to parse lyrics JSON: {e}"))?;
|
||||
|
||||
let synced_lyrics = parsed
|
||||
.get("syncedLyrics")
|
||||
.ok_or_else(|| "Missing `syncedLyrics` field".to_string())?;
|
||||
if parsed
|
||||
.get("instrumental")
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return Ok(LyricsResult::Instrumental);
|
||||
}
|
||||
|
||||
Ok(synced_lyrics.clone())
|
||||
if let Some(synced) = parsed.get("syncedLyrics").and_then(Value::as_str) {
|
||||
return Ok(LyricsResult::Synced(synced.to_string()));
|
||||
}
|
||||
|
||||
if let Some(plain) = parsed.get("plainLyrics").and_then(Value::as_str) {
|
||||
return Ok(LyricsResult::Plain(plain.to_string()));
|
||||
}
|
||||
|
||||
Ok(LyricsResult::None)
|
||||
}
|
||||
|
||||
pub fn save_lyrics_file<P: AsRef<Path>>(audio_path: P, lyrics: String) -> Result<(), io::Error> {
|
||||
let lrc_path = audio_path.as_ref().with_extension("lrc");
|
||||
if !lrc_path.exists() {
|
||||
fs::write(lrc_path, lyrics)?;
|
||||
} else {
|
||||
println!("{:#?} already exists, skipping", lrc_path)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_config() -> io::Result<Config> {
|
||||
match fs::read_to_string(CONFIG_PATH) {
|
||||
Ok(contents) => {
|
||||
@ -61,3 +95,36 @@ pub fn add_song(directory: &str, filename: &str, status: &str) -> io::Result<()>
|
||||
|
||||
save_config(&config)
|
||||
}
|
||||
|
||||
pub fn return_all_processed_files(path: &Path) -> io::Result<HashSet<String>> {
|
||||
let config = load_config()?;
|
||||
let dir_str = path
|
||||
.to_str()
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "Invalid directory path"))?;
|
||||
|
||||
let filenames = config
|
||||
.get(dir_str) // get inner HashMap for the directory
|
||||
.map(|inner| inner.keys().cloned().collect()) // collect keys into HashSet
|
||||
.unwrap_or_else(HashSet::new); // empty if directory not found
|
||||
|
||||
Ok(filenames)
|
||||
}
|
||||
|
||||
// pub fn list_already_processed()
|
||||
|
||||
// pub fn list_directory_entries<P: AsRef<Path>>(path: P) -> io::Result<Vec<String>> {
|
||||
// let mut entries = Vec::new();
|
||||
|
||||
// for entry in fs::read_dir(path)? {
|
||||
// let entry = entry?;
|
||||
// let file_type = entry.file_type()?;
|
||||
|
||||
// if file_type.is_file()
|
||||
// && let Some(name) = entry.file_name().to_str()
|
||||
// {
|
||||
// entries.push(name.to_string());
|
||||
// }
|
||||
// }
|
||||
|
||||
// Ok(entries)
|
||||
// }
|
||||
|
||||
@ -3,6 +3,9 @@ use core::time;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::music::get_all_music_files;
|
||||
|
||||
mod json_parsing;
|
||||
mod lrclib;
|
||||
mod music;
|
||||
@ -45,7 +48,7 @@ fn main() {
|
||||
}
|
||||
} else if let Some(directory) = &args.directory {
|
||||
if !args.watch {
|
||||
if let Err(e) = music::get_all_music_files(directory) {
|
||||
if let Err(e) = music::get_all_music_files(directory, false) {
|
||||
eprintln!("Failed to process directory {}: {e}", directory.display());
|
||||
}
|
||||
} else {
|
||||
@ -57,6 +60,7 @@ fn main() {
|
||||
|
||||
fn watch(path: &Path, wait: Duration) {
|
||||
loop {
|
||||
get_all_music_files(path, true);
|
||||
thread::sleep(wait);
|
||||
}
|
||||
}
|
||||
|
||||
107
src/music.rs
107
src/music.rs
@ -1,7 +1,10 @@
|
||||
use crate::json_parsing;
|
||||
use crate::json_parsing::{self, add_song};
|
||||
|
||||
use audiotags::Tag;
|
||||
use std::collections::HashSet;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
use thiserror::Error;
|
||||
|
||||
@ -41,10 +44,10 @@ pub fn get_song_file(path: &Path) -> Result<(), MusicError> {
|
||||
let absolute_path = fs::canonicalize(&pathbuf).unwrap();
|
||||
let info = get_song_info(&absolute_path)?;
|
||||
|
||||
println!("Track: {}", info.track_name);
|
||||
println!("Artist: {}", info.artist_name);
|
||||
println!("Album: {}", info.album_name);
|
||||
println!("Duration: {:?}\n", info.duration);
|
||||
println!(
|
||||
"Track: {}, Artist: {}, Album: {}",
|
||||
info.track_name, info.artist_name, info.album_name
|
||||
);
|
||||
|
||||
match json_parsing::get_lyrics_text(
|
||||
&info.artist_name,
|
||||
@ -52,43 +55,88 @@ pub fn get_song_file(path: &Path) -> Result<(), MusicError> {
|
||||
&info.album_name,
|
||||
info.duration,
|
||||
) {
|
||||
Ok(lyrics) => {
|
||||
json_parsing::add_song(
|
||||
Ok(result) => {
|
||||
let status = match &result {
|
||||
json_parsing::LyricsResult::Synced(_) => "Synchronized",
|
||||
json_parsing::LyricsResult::Plain(_) => "Plain",
|
||||
json_parsing::LyricsResult::Instrumental => "Instrumental",
|
||||
json_parsing::LyricsResult::None => "NotFound",
|
||||
};
|
||||
|
||||
let _ = add_song(
|
||||
absolute_path.parent().unwrap().to_str().unwrap(),
|
||||
absolute_path.file_name().unwrap().to_str().unwrap(),
|
||||
"Synchronized",
|
||||
status,
|
||||
);
|
||||
|
||||
println!("{lyrics}")
|
||||
match result {
|
||||
json_parsing::LyricsResult::Synced(lyrics)
|
||||
| json_parsing::LyricsResult::Plain(lyrics) => {
|
||||
json_parsing::save_lyrics_file(absolute_path, lyrics);
|
||||
println!("Downloaded lyrics")
|
||||
}
|
||||
json_parsing::LyricsResult::Instrumental => {
|
||||
println!("Instrumental track — no lyrics available");
|
||||
}
|
||||
json_parsing::LyricsResult::None => {
|
||||
println!("No lyrics found");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(e) => {
|
||||
json_parsing::add_song(
|
||||
let _ = add_song(
|
||||
absolute_path.parent().unwrap().to_str().unwrap(),
|
||||
absolute_path.file_name().unwrap().to_str().unwrap(),
|
||||
"NotFound",
|
||||
"Error",
|
||||
);
|
||||
|
||||
eprintln!(
|
||||
"Failed to fetch lyrics for {}, error: {e}",
|
||||
pathbuf.to_str().unwrap()
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_all_music_files(directory: &Path) -> Result<(), MusicError> {
|
||||
pub fn get_all_music_files(directory: &Path, watch: bool) -> Result<(), MusicError> {
|
||||
if !directory.is_dir() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for entry in fs::read_dir(directory)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
let absolute = fs::canonicalize(directory).unwrap();
|
||||
|
||||
if entry.file_type()?.is_dir() {
|
||||
get_all_music_files(&path)?;
|
||||
} else if let Err(e) = get_song_file(&path) {
|
||||
let already_processed_files: HashSet<String> =
|
||||
json_parsing::return_all_processed_files(&absolute).unwrap();
|
||||
|
||||
let files_to_process: Vec<PathBuf> = if watch {
|
||||
get_new_files(&absolute)?
|
||||
} else {
|
||||
let mut all_files = Vec::new();
|
||||
for entry in fs::read_dir(&absolute)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
if entry.file_type()?.is_dir() {
|
||||
get_all_music_files(&path, watch)?; // recursive call for subdirectories
|
||||
} else {
|
||||
all_files.push(path);
|
||||
}
|
||||
}
|
||||
all_files
|
||||
};
|
||||
|
||||
// Process files
|
||||
for path in files_to_process {
|
||||
let path_string = path.file_name().unwrap().to_string_lossy().to_string();
|
||||
|
||||
// Skip already processed files in full-run mode
|
||||
if !watch && already_processed_files.contains(&path_string) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Err(e) = get_song_file(&path) {
|
||||
eprintln!("Skipping file {}: {}", path.display(), e);
|
||||
}
|
||||
}
|
||||
@ -120,3 +168,22 @@ pub fn get_song_info(path: &Path) -> Result<TrackInfo, MusicError> {
|
||||
pub fn get_song_length(path: &Path) -> Result<Duration, MusicError> {
|
||||
mp3_duration::from_path(path).map_err(|_| MusicError::DurationRead(path.display().to_string()))
|
||||
}
|
||||
|
||||
pub fn get_new_files(path: &Path) -> io::Result<Vec<PathBuf>> {
|
||||
// Get the already processed files
|
||||
let processed: HashSet<String> = json_parsing::return_all_processed_files(path)?;
|
||||
|
||||
// Read all files in the directory
|
||||
let mut new_files = Vec::new();
|
||||
for entry in fs::read_dir(path)? {
|
||||
let entry = entry?;
|
||||
let file_name = entry.file_name().to_string_lossy().to_string();
|
||||
|
||||
// Only include files not in processed set
|
||||
if !processed.contains(&file_name) && entry.path().is_file() {
|
||||
new_files.push(entry.path());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(new_files)
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user