mirror of
https://github.com/Dangoware/dmp-core.git
synced 2025-04-19 13:32:53 -05:00
Finished main query function, added MusicLibrary
to MusicController
This commit is contained in:
parent
d2d92149f8
commit
bde2d194dc
4 changed files with 85 additions and 26 deletions
|
@ -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"
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -268,6 +268,13 @@ impl MusicTracker for DiscordRPC {
|
||||||
} else {
|
} else {
|
||||||
&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();
|
||||||
|
|
||||||
|
@ -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))
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue