use std::collections::{BTreeSet, HashMap}; use std::path::{Path, PathBuf}; use audiotags::Tag; use serde::{Deserialize, Serialize}; use walkdir::WalkDir; use config::{Config, File}; #[derive(Debug, Deserialize)] pub struct Settings { pub library_path: PathBuf, } #[derive(Clone, Serialize, Deserialize)] pub struct Song { pub title: String, pub artist: String, pub album: String, pub duration: Option, pub path: PathBuf, } #[derive(Clone, Serialize, Deserialize)] pub struct Library { pub songs: Vec, #[serde(skip)] pub artists: Vec, #[serde(skip)] pub artist_albums: HashMap>, #[serde(skip)] pub album_songs: HashMap<(String, String), Vec>, } impl Library { pub fn new() -> Self { let lib_path = Self::library_path(); if lib_path.exists() { Self::load() } else { let library = Self::scan(); library.save(); library } } pub fn scan() -> Self { let settings = Self::config(); let temp_songs = get_songs(&settings.library_path); let mut songs: Vec = vec![]; for (index, song) in temp_songs.into_iter().enumerate() { let tag = Tag::default().read_from_path(&song).expect("Error"); let song = Song { title: tag.title().unwrap_or("Could not read title").to_string(), artist: tag.artist().unwrap_or("Could not read artist").to_string(), album: tag.album().unwrap().title.to_string(), duration: if tag.duration().is_some() { tag.duration() } else { Some(0.1) }, path: song, }; songs.push(song); } let mut library = Self { songs, artists: Vec::new(), artist_albums: HashMap::new(), album_songs: HashMap::new(), }; library.rebuild_indexes(); library } pub fn load() -> Self { let lib_path = Self::library_path(); let json = std::fs::read_to_string(lib_path).unwrap(); let mut library: Library = serde_json::from_str(&json).unwrap(); library.rebuild_indexes(); library } pub fn albums_for_artist(&self, artist: &String) -> Vec { self.artist_albums .get(artist) .unwrap() .iter() .cloned() .collect() } pub fn songs_for_album(&self, artist: &String, album: &String) -> &Vec { self.album_songs .get(&(artist.clone(), album.clone())) .unwrap() } pub fn save(&self) { let lib_path = Self::library_path(); std::fs::create_dir_all(lib_path.parent().unwrap()).unwrap(); let json = serde_json::to_string(self).unwrap(); std::fs::write(lib_path, json).unwrap(); } fn rebuild_indexes(&mut self) { self.artists.clear(); self.artist_albums.clear(); self.album_songs.clear(); let mut artists = BTreeSet::new(); for (index, song) in self.songs.iter().enumerate() { artists.insert(song.artist.clone()); self.artist_albums .entry(song.artist.clone()) .or_default() .insert(song.album.clone()); self.album_songs .entry((song.artist.clone(), song.album.clone())) .or_default() .push(index); } self.artists = artists.into_iter().collect(); } fn library_path() -> PathBuf { dirs::cache_dir() .expect("Cache directory does not exist") .join("ncmprs") .join("library.json") } fn config() -> Settings { load_config().expect("Failed to load config.toml") } } fn get_songs(library: &Path) -> Vec { let mut songs: Vec = WalkDir::new(library) .into_iter() .filter_map(Result::ok) .filter(|e| { e.file_type().is_file() && e.path() .extension() .is_some_and(|ext| ext == "flac") }) .map(|e| e.into_path()) .collect(); songs.sort(); songs } pub fn load_config() -> Result { let config_path = dirs::config_dir() .expect("Couldn't determine config directory") .join("ncmprs") .join("config.toml"); Config::builder() .add_source(File::from(config_path)) .build()? .try_deserialize() }