diff --git a/Cargo.toml b/Cargo.toml index 0f98190..0885526 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,5 +33,5 @@ arrayvec = "0.7.4" discord-presence = "0.5.18" chrono = { version = "0.4.31", features = ["serde"] } bincode = "1.3.3" -wana_kana = "3.0.0" unidecode = "0.3.0" +rayon = "1.8.0" diff --git a/src/music_controller/music_controller.rs b/src/music_controller/music_controller.rs index 545d5aa..ed3656f 100644 --- a/src/music_controller/music_controller.rs +++ b/src/music_controller/music_controller.rs @@ -3,21 +3,27 @@ use std::sync::{RwLock, Arc}; use crate::music_controller::config::Config; use crate::music_player::music_player::{MusicPlayer, PlayerStatus, DecoderMessage, DSPMessage}; -use crate::music_storage::music_db::Song; +use crate::music_storage::music_db::{MusicLibrary, Song, Tag}; pub struct MusicController { pub config: Arc>, + pub library: MusicLibrary, music_player: MusicPlayer, } impl MusicController { /// Creates new MusicController with config at given path - pub fn new(config_path: &PathBuf) -> Result{ + pub fn new(config_path: &PathBuf) -> Result>{ let config = Arc::new(RwLock::new(Config::new(config_path)?)); let music_player = MusicPlayer::new(config.clone()); + let library = match MusicLibrary::init(config.clone()) { + Ok(library) => library, + Err(error) => return Err(error) + }; let controller = MusicController { config, + library, music_player, }; @@ -25,12 +31,17 @@ impl MusicController { } /// Creates new music controller from a config at given path - pub fn from(config_path: &PathBuf) -> std::result::Result { + pub fn from(config_path: &PathBuf) -> Result> { let config = Arc::new(RwLock::new(Config::from(config_path)?)); let music_player = MusicPlayer::new(config.clone()); + let library = match MusicLibrary::init(config.clone()) { + Ok(library) => library, + Err(error) => return Err(error) + }; let controller = MusicController { config, + library, music_player, }; @@ -63,4 +74,13 @@ impl MusicController { self.song_control(DecoderMessage::DSP(DSPMessage::UpdateProcessor(Box::new(self.music_player.music_processor.clone())))); } + /// Queries the library for a `Vec` + pub fn query_library( + &self, + query_string: &String, + target_tags: Vec, + sort_by: Vec + ) -> Option> { + self.library.query(query_string, &target_tags, &sort_by) + } } diff --git a/src/music_storage/music_db.rs b/src/music_storage/music_db.rs index 0292912..01c12ff 100644 --- a/src/music_storage/music_db.rs +++ b/src/music_storage/music_db.rs @@ -13,6 +13,11 @@ use std::io::BufWriter; use std::path::{Path, PathBuf}; use unidecode::unidecode; +// Fun parallel stuff +use std::sync::{Arc, Mutex, RwLock}; +use rayon::iter; +use rayon::prelude::*; + use crate::music_controller::config::Config; #[derive(Debug, Clone, Deserialize, Serialize)] @@ -29,6 +34,7 @@ pub enum Tag { Genre, Comment, Track, + Disk, Key(String) } @@ -41,6 +47,7 @@ impl ToString for Tag { Self::Genre => "Genre".into(), Self::Comment => "Comment".into(), Self::Track => "TrackNumber".into(), + Self::Disk => "DiscNumber".into(), Self::Key(key) => key.into() } } @@ -68,6 +75,17 @@ pub struct Song { } impl Song { + /** + * Get a tag's value + * + * ``` + * // Assuming an already created song: + * + * let tag = this_song.get_tag(Tag::Title); + * + * assert_eq!(tag, "Title"); + * ``` + **/ pub fn get_tag(&self, target_key: &Tag) -> Option<&String> { let index = self.tags.iter().position(|r| r.0 == *target_key); @@ -124,24 +142,25 @@ pub struct MusicLibrary { } pub fn normalize(input_string: &String) -> String { - unidecode(input_string).to_ascii_lowercase() + unidecode(input_string).to_ascii_lowercase().replace(|c: char| !c.is_alphanumeric() && !c.is_ascii_punctuation(), "") } impl MusicLibrary { /// Initialize the database /// - /// If the database file already exists, return the Library, otherwise create + /// If the database file already exists, return the [MusicLibrary], otherwise create /// the database first. This needs to be run before anything else to retrieve - /// the library vec - pub fn init(config: &Config) -> Result> { + /// the [MusicLibrary] Vec + pub fn init(config: Arc>) -> Result> { + let global_config = &*config.read().unwrap(); let mut library: Vec = Vec::new(); - let mut backup_path = config.db_path.clone(); + let mut backup_path = global_config.db_path.clone(); backup_path.set_extension("bkp"); - match config.db_path.try_exists() { + match global_config.db_path.try_exists() { Ok(true) => { // The database exists, so get it from the file - let database = fs::File::open(config.db_path.to_path_buf())?; + let database = fs::File::open(global_config.db_path.to_path_buf())?; let reader = BufReader::new(database); library = deserialize_from(reader)?; } @@ -149,11 +168,11 @@ impl MusicLibrary { // Create the database if it does not exist // possibly from the backup file if backup_path.try_exists().is_ok_and(|x| x == true) { - let database = fs::File::open(config.db_path.to_path_buf())?; + let database = fs::File::open(global_config.db_path.to_path_buf())?; let reader = BufReader::new(database); library = deserialize_from(reader)?; } else { - let mut writer = BufWriter::new(fs::File::create(config.db_path.to_path_buf())?); + let mut writer = BufWriter::new(fs::File::create(global_config.db_path.to_path_buf())?); serialize_into(&mut writer, &library)?; } }, @@ -197,16 +216,21 @@ impl MusicLibrary { None } - pub fn find_all_music(&mut self, target_path: &str) -> Result<(), Box> { + pub fn find_all_music(&mut self, target_path: &str, config: &Config) -> Result<(), Box> { let mut current_dir = PathBuf::new(); - for entry in WalkDir::new(target_path) + for (i, entry) in WalkDir::new(target_path) .follow_links(true) .into_iter() - .filter_map(|e| e.ok()) + .filter_map(|e| e.ok()).enumerate() { let target_file = entry; let is_file = fs::metadata(target_file.path())?.is_file(); + // Save periodically while scanning + if i%250 == 0 { + self.save(config).unwrap(); + } + // Ensure the target is a file and not a directory, if it isn't, skip this loop if !is_file { current_dir = target_file.into_path(); @@ -225,12 +249,16 @@ impl MusicLibrary { match self.add_file_to_db(target_file.path()) { Ok(_) => (), Err(_error) => () //println!("{}, {:?}: {}", format, target_file.file_name(), error) + // TODO: Handle more of these errors }; } else if extension.to_ascii_lowercase() == "cue" { // TODO: implement cuesheet support } } + // Save the database after scanning finishes + self.save(&config).unwrap(); + Ok(()) } @@ -346,29 +374,32 @@ impl MusicLibrary { todo!() } - /// Query the database, returning a list of items + /// Query the database, returning a list of [Song]s pub fn query( &self, query_string: &String, // The query itself target_tags: &Vec, // The tags to search sort_by: &Vec, // Tags to sort the resulting data by - ) -> Option> { - let mut songs = Vec::new(); + ) -> Option> { + let songs = Arc::new(Mutex::new(Vec::new())); - for track in &self.library { + self.library.par_iter().for_each(|track| { for tag in &track.tags { if !target_tags.contains(&tag.0) { continue; } if normalize(&tag.1).contains(&normalize(&query_string)) { - songs.push(track.clone()); + songs.lock().unwrap().push(track); break } } - } + }); - songs.sort_by(|a, b| { + let lock = Arc::try_unwrap(songs).expect("Lock still has multiple owners!"); + let mut new_songs = lock.into_inner().expect("Mutex cannot be locked!"); + + new_songs.par_sort_by(|a, b| { for opt in sort_by { let tag_a = match a.get_tag(&opt) { Some(tag) => tag, @@ -402,8 +433,8 @@ impl MusicLibrary { a.get_tag(&Tag::Title).cmp(&b.get_tag(&Tag::Title)) }); - if songs.len() > 0 { - Some(songs) + if new_songs.len() > 0 { + Some(new_songs) } else { None } diff --git a/src/music_tracker/music_tracker.rs b/src/music_tracker/music_tracker.rs index 8526e89..12e57e8 100644 --- a/src/music_tracker/music_tracker.rs +++ b/src/music_tracker/music_tracker.rs @@ -268,6 +268,13 @@ impl MusicTracker for DiscordRPC { } else { &unknown }; + + // Sets album + let album = if let Some(album) = song.get_tag(&Tag::Album) { + album + } else { + &unknown + }; let _client_thread = self.client.start(); @@ -281,7 +288,8 @@ impl MusicTracker for DiscordRPC { // Sets discord account activity to current playing song let send_activity = self.client.set_activity(|activity| { activity - .state(format!("Listening to: {}", song_name)) + .state(format!("{}", album)) + .details(format!("{}", song_name)) .assets(|assets| assets.large_image(&self.config.dango_icon)) .timestamps(|time| time.start(start_time)) });