Finished main query function, added MusicLibrary to MusicController

This commit is contained in:
G2-Games 2023-10-02 20:27:59 -05:00
parent d2d92149f8
commit bde2d194dc
4 changed files with 85 additions and 26 deletions

View file

@ -33,5 +33,5 @@ arrayvec = "0.7.4"
discord-presence = "0.5.18" discord-presence = "0.5.18"
chrono = { version = "0.4.31", features = ["serde"] } chrono = { version = "0.4.31", features = ["serde"] }
bincode = "1.3.3" bincode = "1.3.3"
wana_kana = "3.0.0"
unidecode = "0.3.0" unidecode = "0.3.0"
rayon = "1.8.0"

View file

@ -3,21 +3,27 @@ use std::sync::{RwLock, Arc};
use crate::music_controller::config::Config; use crate::music_controller::config::Config;
use crate::music_player::music_player::{MusicPlayer, PlayerStatus, DecoderMessage, DSPMessage}; 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 struct MusicController {
pub config: Arc<RwLock<Config>>, pub config: Arc<RwLock<Config>>,
pub library: MusicLibrary,
music_player: MusicPlayer, music_player: MusicPlayer,
} }
impl MusicController { impl MusicController {
/// Creates new MusicController with config at given path /// Creates new MusicController with config at given path
pub fn new(config_path: &PathBuf) -> Result<MusicController, std::io::Error>{ pub fn new(config_path: &PathBuf) -> Result<MusicController, Box<dyn std::error::Error>>{
let config = Arc::new(RwLock::new(Config::new(config_path)?)); let config = Arc::new(RwLock::new(Config::new(config_path)?));
let music_player = MusicPlayer::new(config.clone()); 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 { let controller = MusicController {
config, config,
library,
music_player, music_player,
}; };
@ -25,12 +31,17 @@ impl MusicController {
} }
/// Creates new music controller from a config at given path /// Creates new music controller from a config at given path
pub fn from(config_path: &PathBuf) -> std::result::Result<MusicController, toml::de::Error> { pub fn from(config_path: &PathBuf) -> Result<MusicController, Box<dyn std::error::Error>> {
let config = Arc::new(RwLock::new(Config::from(config_path)?)); let config = Arc::new(RwLock::new(Config::from(config_path)?));
let music_player = MusicPlayer::new(config.clone()); 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 { let controller = MusicController {
config, config,
library,
music_player, music_player,
}; };
@ -63,4 +74,13 @@ impl MusicController {
self.song_control(DecoderMessage::DSP(DSPMessage::UpdateProcessor(Box::new(self.music_player.music_processor.clone())))); self.song_control(DecoderMessage::DSP(DSPMessage::UpdateProcessor(Box::new(self.music_player.music_processor.clone()))));
} }
/// Queries the library for a `Vec<Song>`
pub fn query_library(
&self,
query_string: &String,
target_tags: Vec<Tag>,
sort_by: Vec<Tag>
) -> Option<Vec<&Song>> {
self.library.query(query_string, &target_tags, &sort_by)
}
} }

View file

@ -13,6 +13,11 @@ use std::io::BufWriter;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use unidecode::unidecode; use unidecode::unidecode;
// Fun parallel stuff
use std::sync::{Arc, Mutex, RwLock};
use rayon::iter;
use rayon::prelude::*;
use crate::music_controller::config::Config; use crate::music_controller::config::Config;
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize)]
@ -29,6 +34,7 @@ pub enum Tag {
Genre, Genre,
Comment, Comment,
Track, Track,
Disk,
Key(String) Key(String)
} }
@ -41,6 +47,7 @@ impl ToString for Tag {
Self::Genre => "Genre".into(), Self::Genre => "Genre".into(),
Self::Comment => "Comment".into(), Self::Comment => "Comment".into(),
Self::Track => "TrackNumber".into(), Self::Track => "TrackNumber".into(),
Self::Disk => "DiscNumber".into(),
Self::Key(key) => key.into() Self::Key(key) => key.into()
} }
} }
@ -68,6 +75,17 @@ pub struct Song {
} }
impl 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> { pub fn get_tag(&self, target_key: &Tag) -> Option<&String> {
let index = self.tags.iter().position(|r| r.0 == *target_key); 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 { 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 { impl MusicLibrary {
/// Initialize the database /// 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 database first. This needs to be run before anything else to retrieve
/// the library vec /// the [MusicLibrary] Vec
pub fn init(config: &Config) -> Result<Self, Box<dyn Error>> { pub fn init(config: Arc<RwLock<Config>>) -> Result<Self, Box<dyn Error>> {
let global_config = &*config.read().unwrap();
let mut library: Vec<Song> = Vec::new(); let mut library: Vec<Song> = Vec::new();
let mut backup_path = config.db_path.clone(); let mut backup_path = global_config.db_path.clone();
backup_path.set_extension("bkp"); backup_path.set_extension("bkp");
match config.db_path.try_exists() { match global_config.db_path.try_exists() {
Ok(true) => { Ok(true) => {
// The database exists, so get it from the file // 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); let reader = BufReader::new(database);
library = deserialize_from(reader)?; library = deserialize_from(reader)?;
} }
@ -149,11 +168,11 @@ impl MusicLibrary {
// Create the database if it does not exist // Create the database if it does not exist
// possibly from the backup file // possibly from the backup file
if backup_path.try_exists().is_ok_and(|x| x == true) { 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); let reader = BufReader::new(database);
library = deserialize_from(reader)?; library = deserialize_from(reader)?;
} else { } 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)?; serialize_into(&mut writer, &library)?;
} }
}, },
@ -197,16 +216,21 @@ impl MusicLibrary {
None None
} }
pub fn find_all_music(&mut self, target_path: &str) -> Result<(), Box<dyn std::error::Error>> { pub fn find_all_music(&mut self, target_path: &str, config: &Config) -> Result<(), Box<dyn std::error::Error>> {
let mut current_dir = PathBuf::new(); let mut current_dir = PathBuf::new();
for entry in WalkDir::new(target_path) for (i, entry) in WalkDir::new(target_path)
.follow_links(true) .follow_links(true)
.into_iter() .into_iter()
.filter_map(|e| e.ok()) .filter_map(|e| e.ok()).enumerate()
{ {
let target_file = entry; let target_file = entry;
let is_file = fs::metadata(target_file.path())?.is_file(); 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 // Ensure the target is a file and not a directory, if it isn't, skip this loop
if !is_file { if !is_file {
current_dir = target_file.into_path(); current_dir = target_file.into_path();
@ -225,12 +249,16 @@ impl MusicLibrary {
match self.add_file_to_db(target_file.path()) { match self.add_file_to_db(target_file.path()) {
Ok(_) => (), Ok(_) => (),
Err(_error) => () //println!("{}, {:?}: {}", format, target_file.file_name(), error) Err(_error) => () //println!("{}, {:?}: {}", format, target_file.file_name(), error)
// TODO: Handle more of these errors
}; };
} else if extension.to_ascii_lowercase() == "cue" { } else if extension.to_ascii_lowercase() == "cue" {
// TODO: implement cuesheet support // TODO: implement cuesheet support
} }
} }
// Save the database after scanning finishes
self.save(&config).unwrap();
Ok(()) Ok(())
} }
@ -346,29 +374,32 @@ impl MusicLibrary {
todo!() todo!()
} }
/// Query the database, returning a list of items /// Query the database, returning a list of [Song]s
pub fn query( pub fn query(
&self, &self,
query_string: &String, // The query itself query_string: &String, // The query itself
target_tags: &Vec<Tag>, // The tags to search target_tags: &Vec<Tag>, // The tags to search
sort_by: &Vec<Tag>, // Tags to sort the resulting data by sort_by: &Vec<Tag>, // Tags to sort the resulting data by
) -> Option<Vec<Song>> { ) -> Option<Vec<&Song>> {
let mut songs = Vec::new(); 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 { for tag in &track.tags {
if !target_tags.contains(&tag.0) { if !target_tags.contains(&tag.0) {
continue; continue;
} }
if normalize(&tag.1).contains(&normalize(&query_string)) { if normalize(&tag.1).contains(&normalize(&query_string)) {
songs.push(track.clone()); songs.lock().unwrap().push(track);
break 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 { for opt in sort_by {
let tag_a = match a.get_tag(&opt) { let tag_a = match a.get_tag(&opt) {
Some(tag) => tag, Some(tag) => tag,
@ -402,8 +433,8 @@ impl MusicLibrary {
a.get_tag(&Tag::Title).cmp(&b.get_tag(&Tag::Title)) a.get_tag(&Tag::Title).cmp(&b.get_tag(&Tag::Title))
}); });
if songs.len() > 0 { if new_songs.len() > 0 {
Some(songs) Some(new_songs)
} else { } else {
None None
} }

View file

@ -269,6 +269,13 @@ impl MusicTracker for DiscordRPC {
&unknown &unknown
}; };
// Sets album
let album = if let Some(album) = song.get_tag(&Tag::Album) {
album
} else {
&unknown
};
let _client_thread = self.client.start(); let _client_thread = self.client.start();
// Blocks thread execution until it has connected to local discord client // Blocks thread execution until it has connected to local discord client
@ -281,7 +288,8 @@ impl MusicTracker for DiscordRPC {
// Sets discord account activity to current playing song // Sets discord account activity to current playing song
let send_activity = self.client.set_activity(|activity| { let send_activity = self.client.set_activity(|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)) .assets(|assets| assets.large_image(&self.config.dango_icon))
.timestamps(|time| time.start(start_time)) .timestamps(|time| time.start(start_time))
}); });