From 7f57367aadb0fb7d8445830272745edbf854a71b Mon Sep 17 00:00:00 2001 From: G2-Games Date: Fri, 29 Sep 2023 23:28:12 -0500 Subject: [PATCH 01/20] Initial work on database rewrite --- Cargo.toml | 3 +- src/music_storage/music_db.rs | 332 +++++++++------------------------- src/music_storage/playlist.rs | 19 +- 3 files changed, 91 insertions(+), 263 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3ee99fa..a96c12a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,6 @@ categories = ["multimedia::audio"] [dependencies] file-format = { version = "0.17.3", features = ["reader", "serde"] } lofty = "0.14.0" -rusqlite = { version = "0.29.0", features = ["bundled"] } serde = { version = "1.0.164", features = ["derive"] } time = "0.3.22" toml = "0.7.5" @@ -32,3 +31,5 @@ futures = "0.3.28" rubato = "0.12.0" arrayvec = "0.7.4" discord-presence = "0.5.18" +chrono = { version = "0.4.31", features = ["serde"] } +bincode = "1.3.3" diff --git a/src/music_storage/music_db.rs b/src/music_storage/music_db.rs index 093d7c5..77aaf0b 100644 --- a/src/music_storage/music_db.rs +++ b/src/music_storage/music_db.rs @@ -1,38 +1,72 @@ use file_format::{FileFormat, Kind}; -use serde::Deserialize; -use lofty::{Accessor, AudioFile, Probe, TaggedFileExt, ItemKey, ItemValue, TagType}; -use rusqlite::{params, Connection}; -use std::fs; -use std::path::{Path, PathBuf}; +use lofty::{AudioFile, Probe, TaggedFileExt, ItemKey, ItemValue, TagType}; +use std::{error::Error, io::BufReader}; + use std::time::Duration; -use time::Date; +use chrono::{DateTime, Utc, serde::ts_seconds_option}; use walkdir::WalkDir; +use std::io::BufWriter; +use std::fs; +use serde::{Deserialize, Serialize}; +use std::path::{Path, PathBuf}; +use bincode::{serialize_into, deserialize_from}; + use crate::music_controller::config::Config; -#[derive(Debug, Clone)] -pub struct Song { - pub path: URI, - pub title: Option, - pub album: Option, - pub tracknum: Option, - pub artist: Option, - pub date: Option, - pub genre: Option, - pub plays: Option, - pub favorited: Option, - pub format: Option, // TODO: Make this a proper FileFormat eventually - pub duration: Option, - pub custom_tags: Option>, +pub struct AlbumArt { + pub path: Option; } -#[derive(Clone, Debug)] +/// Stores information about a single song +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Song { + pub path: URI, + pub plays: i32, + pub skips: i32, + pub favorited: bool, + pub rating: u8, + pub format: Option, + pub duration: Duration, + pub play_time: Duration, + #[serde(with = "ts_seconds_option")] + pub last_played: Option>, + #[serde(with = "ts_seconds_option")] + pub date_added: Option>, + pub tags: Vec<(String, String)>, +} + +impl Song { + pub fn get_tag(&self, target_key: String) -> Option { + for tag in self.tags { + if tag.0 == target_key { + return Some(tag.1) + } + } + None + } + + pub fn get_tags(&self, target_keys: Vec) -> Vec> { + let mut results = Vec::new(); + for tag in self.tags { + for key in target_keys { + if tag.0 == key { + results.push(Some(tag.1)) + } + } + results.push(None); + } + results + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] pub enum URI{ Local(String), Remote(Service, String), } -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, Serialize, Deserialize)] pub enum Service { InternetRadio, Spotify, @@ -45,74 +79,40 @@ pub struct Playlist { cover_art: Box, } -pub fn create_db() -> Result<(), rusqlite::Error> { - let path = "./music_database.db3"; - let db_connection = Connection::open(path)?; +/// Initialize the database +/// +/// If the database file already exists, return the database, otherwise create it first +/// This needs to be run before anything else to retrieve the library vec +pub fn init_db(config: &Config) -> Result, Box> { + let mut library: Vec = Vec::new(); - db_connection.pragma_update(None, "synchronous", "0")?; - db_connection.pragma_update(None, "journal_mode", "WAL")?; + match config.db_path.try_exists() { + Ok(_) => { + // The database exists, so get it from the file + let database = fs::File::open(config.db_path.into_boxed_path())?; + let reader = BufReader::new(database); + library = deserialize_from(reader)?; + }, + Err(_) => { + // Create the database if it does not exist + let mut writer = BufWriter::new( + fs::File::create(config.db_path.into_boxed_path())? + ); + serialize_into(&mut writer, &library)?; + } + }; - // Create the important tables - db_connection.execute( - "CREATE TABLE music_collection ( - song_path TEXT PRIMARY KEY, - title TEXT, - album TEXT, - tracknum INTEGER, - artist TEXT, - date INTEGER, - genre TEXT, - plays INTEGER, - favorited BLOB, - format TEXT, - duration INTEGER - )", - (), // empty list of parameters. - )?; - - db_connection.execute( - "CREATE TABLE playlists ( - playlist_name TEXT NOT NULL, - song_path TEXT NOT NULL, - FOREIGN KEY(song_path) REFERENCES music_collection(song_path) - )", - (), // empty list of parameters. - )?; - - db_connection.execute( - "CREATE TABLE custom_tags ( - song_path TEXT NOT NULL, - tag TEXT NOT NULL, - tag_value TEXT, - FOREIGN KEY(song_path) REFERENCES music_collection(song_path) - )", - (), // empty list of parameters. - )?; - - Ok(()) + Ok(library) } -fn path_in_db(query_path: &Path, connection: &Connection) -> bool { - let query_string = format!("SELECT EXISTS(SELECT 1 FROM music_collection WHERE song_path='{}')", query_path.to_string_lossy()); - - let mut query_statement = connection.prepare(&query_string).unwrap(); - let mut rows = query_statement.query([]).unwrap(); - - match rows.next().unwrap() { - Some(value) => value.get::(0).unwrap(), - None => false - } +fn path_in_db(query_path: &Path, library: &Vec) -> bool { + unimplemented!() } - pub fn find_all_music( config: &Config, target_path: &str, ) -> Result<(), Box> { - let db_connection = Connection::open(&*config.db_path)?; - - db_connection.pragma_update(None, "synchronous", "0")?; - db_connection.pragma_update(None, "journal_mode", "WAL")?; let mut current_dir = PathBuf::new(); for entry in WalkDir::new(target_path).follow_links(true).into_iter().filter_map(|e| e.ok()) { @@ -134,25 +134,16 @@ pub fn find_all_music( // If it's a normal file, add it to the database // if it's a cuesheet, do a bunch of fancy stuff if format.kind() == Kind::Audio { - add_file_to_db(target_file.path(), &db_connection) + add_file_to_db(target_file.path()) } else if extension.to_ascii_lowercase() == "cue" { // TODO: implement cuesheet support } } - // create the indexes after all the data is inserted - db_connection.execute( - "CREATE INDEX path_index ON music_collection (song_path)", () - )?; - - db_connection.execute( - "CREATE INDEX custom_tags_index ON custom_tags (song_path)", () - )?; - Ok(()) } -pub fn add_file_to_db(target_file: &Path, connection: &Connection) { +pub fn add_file_to_db(target_file: &Path) { // TODO: Fix error handling here let tagged_file = match lofty::read_from_path(target_file) { Ok(tagged_file) => tagged_file, @@ -179,7 +170,7 @@ pub fn add_file_to_db(target_file: &Path, connection: &Connection) { let mut custom_insert = String::new(); let mut loops = 0; - for item in tag.items() { + for (loops, item) in tag.items().enumerate() { let mut custom_key = String::new(); match item.key() { ItemKey::TrackArtist | @@ -203,10 +194,6 @@ pub fn add_file_to_db(target_file: &Path, connection: &Connection) { if loops > 0 { custom_insert.push_str(", "); } - - custom_insert.push_str(&format!(" (?1, '{}', '{}')", custom_key.replace("\'", "''"), custom_value.replace("\'", "''"))); - - loops += 1; } // Get the format as a string @@ -222,69 +209,10 @@ pub fn add_file_to_db(target_file: &Path, connection: &Connection) { // TODO: Fix error handling let binding = fs::canonicalize(target_file).unwrap(); let abs_path = binding.to_str().unwrap(); - - // Add all the info into the music_collection table - connection.execute( - "INSERT INTO music_collection ( - song_path, - title, - album, - tracknum, - artist, - date, - genre, - plays, - favorited, - format, - duration - ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)", - params![abs_path, tag.title(), tag.album(), tag.track(), tag.artist(), tag.year(), tag.genre(), 0, false, short_format, duration], - ).unwrap(); - - //TODO: Fix this, it's horrible - if custom_insert != "" { - connection.execute( - &format!("INSERT INTO custom_tags ('song_path', 'tag', 'tag_value') VALUES {}", &custom_insert), - params![ - abs_path, - ] - ).unwrap(); - } } -#[derive(Debug, Deserialize, Clone)] -pub enum Tag { - SongPath, - Title, - Album, - TrackNum, - Artist, - Date, - Genre, - Plays, - Favorited, - Format, - Duration, - Custom{tag: String, tag_value: String}, -} +pub fn add_song_to_db(new_song: Song) { -impl Tag { - fn as_str(&self) -> &str { - match self { - Tag::SongPath => "song_path", - Tag::Title => "title", - Tag::Album => "album", - Tag::TrackNum => "tracknum", - Tag::Artist => "artist", - Tag::Date => "date", - Tag::Genre => "genre", - Tag::Plays => "plays", - Tag::Favorited => "favorited", - Tag::Format => "format", - Tag::Duration => "duration", - Tag::Custom{tag, ..} => tag, - } - } } #[derive(Debug)] @@ -305,93 +233,9 @@ impl MusicObject { /// Query the database, returning a list of items pub fn query ( - config: &Config, - text_input: &String, - queried_tags: &Vec<&Tag>, - order_by_tags: &Vec<&Tag>, + 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 db_connection = Connection::open(&*config.db_path).unwrap(); - - // Set up some database settings - db_connection.pragma_update(None, "synchronous", "0").unwrap(); - db_connection.pragma_update(None, "journal_mode", "WAL").unwrap(); - - // Build the "WHERE" part of the SQLite query - let mut where_string = String::new(); - let mut loops = 0; - for tag in queried_tags { - if loops > 0 { - where_string.push_str("OR "); - } - - match tag { - Tag::Custom{tag, ..} => where_string.push_str(&format!("custom_tags.tag = '{tag}' AND custom_tags.tag_value LIKE '{text_input}' ")), - Tag::SongPath => where_string.push_str(&format!("music_collection.{} LIKE '{text_input}' ", tag.as_str())), - _ => where_string.push_str(&format!("{} LIKE '{text_input}' ", tag.as_str())) - } - - loops += 1; - } - - // Build the "ORDER BY" part of the SQLite query - let mut order_by_string = String::new(); - let mut loops = 0; - for tag in order_by_tags { - match tag { - Tag::Custom{..} => continue, - _ => () - } - - if loops > 0 { - order_by_string.push_str(", "); - } - - order_by_string.push_str(tag.as_str()); - - loops += 1; - } - - // Build the final query string - let query_string = format!(" - SELECT music_collection.*, JSON_GROUP_ARRAY(JSON_OBJECT('Custom',JSON_OBJECT('tag', custom_tags.tag, 'tag_value', custom_tags.tag_value))) AS custom_tags - FROM music_collection - LEFT JOIN custom_tags ON music_collection.song_path = custom_tags.song_path - WHERE {where_string} - GROUP BY music_collection.song_path - ORDER BY {order_by_string} - "); - - let mut query_statement = db_connection.prepare(&query_string).unwrap(); - let mut rows = query_statement.query([]).unwrap(); - - let mut final_result:Vec = vec![]; - - while let Some(row) = rows.next().unwrap() { - let custom_tags: Vec = match row.get::(11) { - Ok(result) => serde_json::from_str(&result).unwrap_or(vec![]), - Err(_) => vec![] - }; - - let file_format: FileFormat = FileFormat::from(row.get::(9).unwrap().as_bytes()); - - let new_song = Song { - // TODO: Implement proper errors here - path: URI::Local(String::from("URI")), - title: row.get::(1).ok(), - album: row.get::(2).ok(), - tracknum: row.get::(3).ok(), - artist: row.get::(4).ok(), - date: Date::from_calendar_date(row.get::(5).unwrap_or(0), time::Month::January, 1).ok(), // TODO: Fix this to get the actual date - genre: row.get::(6).ok(), - plays: row.get::(7).ok(), - favorited: row.get::(8).ok(), - format: Some(file_format), - duration: Some(Duration::from_secs(row.get::(10).unwrap_or(0))), - custom_tags: Some(custom_tags), - }; - - final_result.push(MusicObject::Song(new_song)); - }; - - Some(final_result) + unimplemented!() } diff --git a/src/music_storage/playlist.rs b/src/music_storage/playlist.rs index f6fc9b7..e61b405 100644 --- a/src/music_storage/playlist.rs +++ b/src/music_storage/playlist.rs @@ -1,27 +1,10 @@ use std::path::Path; use crate::music_controller::config::Config; -use rusqlite::{params, Connection}; pub fn playlist_add( config: &Config, playlist_name: &str, song_paths: &Vec<&Path> ) { - let db_connection = Connection::open(&*config.db_path).unwrap(); - - for song_path in song_paths { - db_connection.execute( - "INSERT INTO playlists ( - playlist_name, - song_path - ) VALUES ( - ?1, - ?2 - )", - params![ - playlist_name, - song_path.to_str().unwrap() - ], - ).unwrap(); - } + unimplemented!() } From db53bed1116358e9963983b219e0cd0f7f0f1385 Mon Sep 17 00:00:00 2001 From: G2-Games Date: Sat, 30 Sep 2023 23:31:42 -0500 Subject: [PATCH 02/20] Database works --- src/music_controller/config.rs | 2 +- src/music_controller/music_controller.rs | 2 - src/music_player/music_player.rs | 2 +- src/music_storage/music_db.rs | 421 ++++++++++++++--------- src/music_tracker/music_tracker.rs | 14 +- 5 files changed, 260 insertions(+), 181 deletions(-) diff --git a/src/music_controller/config.rs b/src/music_controller/config.rs index 034f12d..f6909f5 100644 --- a/src/music_controller/config.rs +++ b/src/music_controller/config.rs @@ -16,7 +16,7 @@ pub struct Config { impl Default for Config { fn default() -> Self { - let path = PathBuf::from("./music_database.db3"); + let path = PathBuf::from("./music_database"); return Config { db_path: Box::new(path), diff --git a/src/music_controller/music_controller.rs b/src/music_controller/music_controller.rs index da4f676..545d5aa 100644 --- a/src/music_controller/music_controller.rs +++ b/src/music_controller/music_controller.rs @@ -1,8 +1,6 @@ use std::path::PathBuf; use std::sync::{RwLock, Arc}; -use rusqlite::Result; - use crate::music_controller::config::Config; use crate::music_player::music_player::{MusicPlayer, PlayerStatus, DecoderMessage, DSPMessage}; use crate::music_storage::music_db::Song; diff --git a/src/music_player/music_player.rs b/src/music_player/music_player.rs index 5ab0ad9..478fca1 100644 --- a/src/music_player/music_player.rs +++ b/src/music_player/music_player.rs @@ -232,7 +232,7 @@ impl MusicPlayer { // Handles message received from MusicPlayer struct match message { Some(DecoderMessage::OpenSong(song)) => { - let song_uri = song.path.clone(); + let song_uri = song.location.clone(); match SongHandler::new(&song_uri) { Ok(new_handler) => { song_handler = Some(new_handler); diff --git a/src/music_storage/music_db.rs b/src/music_storage/music_db.rs index 77aaf0b..cb429c6 100644 --- a/src/music_storage/music_db.rs +++ b/src/music_storage/music_db.rs @@ -1,31 +1,33 @@ use file_format::{FileFormat, Kind}; -use lofty::{AudioFile, Probe, TaggedFileExt, ItemKey, ItemValue, TagType}; +use lofty::{AudioFile, ItemKey, ItemValue, Probe, TagType, TaggedFileExt}; use std::{error::Error, io::BufReader}; +use chrono::{serde::ts_seconds_option, DateTime, Utc}; use std::time::Duration; -use chrono::{DateTime, Utc, serde::ts_seconds_option}; use walkdir::WalkDir; -use std::io::BufWriter; -use std::fs; +use bincode::{deserialize_from, serialize_into}; use serde::{Deserialize, Serialize}; +use std::fs; +use std::io::BufWriter; use std::path::{Path, PathBuf}; -use bincode::{serialize_into, deserialize_from}; use crate::music_controller::config::Config; +#[derive(Debug, Clone, Deserialize, Serialize)] pub struct AlbumArt { - pub path: Option; + pub index: u16, + pub path: Option, } /// Stores information about a single song #[derive(Debug, Clone, Deserialize, Serialize)] pub struct Song { - pub path: URI, + pub location: URI, pub plays: i32, pub skips: i32, pub favorited: bool, - pub rating: u8, + pub rating: Option, pub format: Option, pub duration: Duration, pub play_time: Duration, @@ -33,25 +35,26 @@ pub struct Song { pub last_played: Option>, #[serde(with = "ts_seconds_option")] pub date_added: Option>, + pub album_art: Vec, pub tags: Vec<(String, String)>, } impl Song { - pub fn get_tag(&self, target_key: String) -> Option { - for tag in self.tags { - if tag.0 == target_key { - return Some(tag.1) - } + pub fn get_tag(&self, target_key: &str) -> Option<&String> { + let index = self.tags.iter().position(|r| r.0 == target_key); + + match index { + Some(i) => return Some(&self.tags[i].1), + None => None, } - None } - pub fn get_tags(&self, target_keys: Vec) -> Vec> { + pub fn get_tags(&self, target_keys: &Vec) -> Vec> { let mut results = Vec::new(); - for tag in self.tags { + for tag in &self.tags { for key in target_keys { - if tag.0 == key { - results.push(Some(tag.1)) + if &tag.0 == key { + results.push(Some(tag.1.to_owned())) } } results.push(None); @@ -60,13 +63,14 @@ impl Song { } } -#[derive(Clone, Debug, Serialize, Deserialize)] -pub enum URI{ +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub enum URI { Local(String), + //Cue(String, Duration), TODO: Make cue stuff work Remote(Service, String), } -#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)] pub enum Service { InternetRadio, Spotify, @@ -79,142 +83,6 @@ pub struct Playlist { cover_art: Box, } -/// Initialize the database -/// -/// If the database file already exists, return the database, otherwise create it first -/// This needs to be run before anything else to retrieve the library vec -pub fn init_db(config: &Config) -> Result, Box> { - let mut library: Vec = Vec::new(); - - match config.db_path.try_exists() { - Ok(_) => { - // The database exists, so get it from the file - let database = fs::File::open(config.db_path.into_boxed_path())?; - let reader = BufReader::new(database); - library = deserialize_from(reader)?; - }, - Err(_) => { - // Create the database if it does not exist - let mut writer = BufWriter::new( - fs::File::create(config.db_path.into_boxed_path())? - ); - serialize_into(&mut writer, &library)?; - } - }; - - Ok(library) -} - -fn path_in_db(query_path: &Path, library: &Vec) -> bool { - unimplemented!() -} - -pub fn find_all_music( - config: &Config, - target_path: &str, -) -> Result<(), Box> { - - let mut current_dir = PathBuf::new(); - for entry in WalkDir::new(target_path).follow_links(true).into_iter().filter_map(|e| e.ok()) { - let target_file = entry; - let is_file = fs::metadata(target_file.path())?.is_file(); - - // 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(); - continue; - } - - let format = FileFormat::from_file(target_file.path())?; - let extension = target_file - .path() - .extension() - .expect("Could not find file extension"); - - // If it's a normal file, add it to the database - // if it's a cuesheet, do a bunch of fancy stuff - if format.kind() == Kind::Audio { - add_file_to_db(target_file.path()) - } else if extension.to_ascii_lowercase() == "cue" { - // TODO: implement cuesheet support - } - } - - Ok(()) -} - -pub fn add_file_to_db(target_file: &Path) { - // TODO: Fix error handling here - let tagged_file = match lofty::read_from_path(target_file) { - Ok(tagged_file) => tagged_file, - - Err(_) => match Probe::open(target_file) - .expect("ERROR: Bad path provided!") - .read() { - Ok(tagged_file) => tagged_file, - - Err(_) => return - } - }; - - // Ensure the tags exist, if not, insert blank data - let blank_tag = &lofty::Tag::new(TagType::Id3v2); - let tag = match tagged_file.primary_tag() { - Some(primary_tag) => primary_tag, - - None => match tagged_file.first_tag() { - Some(first_tag) => first_tag, - None => blank_tag - }, - }; - - let mut custom_insert = String::new(); - let mut loops = 0; - for (loops, item) in tag.items().enumerate() { - let mut custom_key = String::new(); - match item.key() { - ItemKey::TrackArtist | - ItemKey::TrackTitle | - ItemKey::AlbumTitle | - ItemKey::Genre | - ItemKey::TrackNumber | - ItemKey::Year | - ItemKey::RecordingDate => continue, - ItemKey::Unknown(unknown) => custom_key.push_str(&unknown), - custom => custom_key.push_str(&format!("{:?}", custom)) - // TODO: This is kind of cursed, maybe fix? - }; - - let custom_value = match item.value() { - ItemValue::Text(value) => value, - ItemValue::Locator(value) => value, - ItemValue::Binary(_) => "" - }; - - if loops > 0 { - custom_insert.push_str(", "); - } - } - - // Get the format as a string - let short_format: Option = match FileFormat::from_file(target_file) { - Ok(fmt) => Some(fmt.to_string()), - Err(_) => None - }; - - println!("{}", short_format.as_ref().unwrap()); - - let duration = tagged_file.properties().duration().as_secs().to_string(); - - // TODO: Fix error handling - let binding = fs::canonicalize(target_file).unwrap(); - let abs_path = binding.to_str().unwrap(); -} - -pub fn add_song_to_db(new_song: Song) { - -} - #[derive(Debug)] pub enum MusicObject { Song(Song), @@ -222,20 +90,231 @@ pub enum MusicObject { Playlist(Playlist), } -impl MusicObject { - pub fn as_song(&self) -> Option<&Song> { - match self { - MusicObject::Song(data) => Some(data), - _ => None - } - } +#[derive(Debug)] +pub struct MusicLibrary { + pub library: Vec, } -/// Query the database, returning a list of items -pub fn query ( - query_string: &String, // The query itself - target_tags: &Vec, // The tags to search - sort_by: &Vec, // Tags to sort the resulting data by -) -> Option> { - unimplemented!() +impl MusicLibrary { + /// Initialize the database + /// + /// If the database file already exists, return the Library, otherwise create + /// the database first. This needs to be run before anything else to retrieve + /// the library vec + pub fn init(config: &Config) -> Result> { + let mut library: Vec = Vec::new(); + let mut backup_path = config.db_path.clone(); + backup_path.set_extension("bkp"); + + match 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 reader = BufReader::new(database); + library = deserialize_from(reader)?; + } + Ok(false) => { + // 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 reader = BufReader::new(database); + library = deserialize_from(reader)?; + } else { + let mut writer = BufWriter::new(fs::File::create(config.db_path.to_path_buf())?); + serialize_into(&mut writer, &library)?; + } + }, + Err(error) => return Err(error.into()) + }; + + Ok(Self { library }) + } + + pub fn save(&self, config: &Config) -> Result<(), Box> { + match config.db_path.try_exists() { + Ok(true) => { + // The database exists, rename it to `.bkp` and + // write the new database + let mut backup_name = config.db_path.clone(); + backup_name.set_extension("bkp"); + fs::rename(config.db_path.as_path(), backup_name.as_path())?; + + // TODO: Make this save properly like in config.rs + + let mut writer = BufWriter::new(fs::File::create(config.db_path.to_path_buf())?); + serialize_into(&mut writer, &self.library)?; + } + Ok(false) => { + // Create the database if it does not exist + let mut writer = BufWriter::new(fs::File::create(config.db_path.to_path_buf())?); + serialize_into(&mut writer, &self.library)?; + }, + Err(error) => return Err(error.into()) + } + + Ok(()) + } + + fn find_by_uri(&self, path: &URI) -> Option { + for track in &self.library { + if path == &track.location { + return Some(track.clone()); + } + } + None + } + + pub fn find_all_music(&mut self, target_path: &str) -> Result<(), Box> { + let mut current_dir = PathBuf::new(); + for entry in WalkDir::new(target_path) + .follow_links(true) + .into_iter() + .filter_map(|e| e.ok()) + { + let target_file = entry; + let is_file = fs::metadata(target_file.path())?.is_file(); + + // 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(); + continue; + } + + let format = FileFormat::from_file(target_file.path())?; + let extension = target_file + .path() + .extension() + .expect("Could not find file extension"); + + // If it's a normal file, add it to the database + // if it's a cuesheet, do a bunch of fancy stuff + if format.kind() == Kind::Audio { + match self.add_file_to_db(target_file.path()) { + Ok(_) => (), + Err(_error) => () //println!("{}, {:?}: {}", format, target_file.file_name(), error) + }; + } else if extension.to_ascii_lowercase() == "cue" { + // TODO: implement cuesheet support + } + } + + Ok(()) + } + + pub fn add_file_to_db(&mut self, target_file: &Path) -> Result<(), Box> { + // TODO: Fix error handling here + let tagged_file = match lofty::read_from_path(target_file) { + Ok(tagged_file) => tagged_file, + + Err(_) => match Probe::open(target_file)?.read() { + Ok(tagged_file) => tagged_file, + + Err(error) => return Err(error.into()), + }, + }; + + // Ensure the tags exist, if not, insert blank data + let blank_tag = &lofty::Tag::new(TagType::Id3v2); + let tag = match tagged_file.primary_tag() { + Some(primary_tag) => primary_tag, + + None => match tagged_file.first_tag() { + Some(first_tag) => first_tag, + None => blank_tag, + }, + }; + + let mut tags: Vec<(String, String)> = Vec::new(); + for item in tag.items() { + let mut key = String::new(); + match item.key() { + ItemKey::Unknown(unknown) => key.push_str(&unknown), + custom => key = format!("{:?}", custom), + }; + + let value = match item.value() { + ItemValue::Text(value) => String::from(value), + ItemValue::Locator(value) => String::from(value), + ItemValue::Binary(_) => String::from(""), + }; + + tags.push((key, value)) + } + + // Get all the album artwork information + let mut album_art: Vec = Vec::new(); + for (i, _art) in tag.pictures().iter().enumerate() { + let new_art = AlbumArt { + index: i as u16, + path: None, + }; + + album_art.push(new_art) + } + + // Get the format as a string + let format: Option = match FileFormat::from_file(target_file) { + Ok(fmt) => Some(fmt), + Err(_) => None, + }; + + let duration = tagged_file.properties().duration(); + + // TODO: Fix error handling + let binding = fs::canonicalize(target_file).unwrap(); + let abs_path = binding.to_str().unwrap(); + + let new_song = Song { + location: URI::Local(abs_path.to_string()), + plays: 0, + skips: 0, + favorited: false, + rating: None, + format, + duration, + play_time: Duration::from_secs(0), + last_played: None, + date_added: Some(chrono::offset::Utc::now()), + tags, + album_art, + }; + + match self.add_song_to_db(new_song) { + Ok(_) => (), + Err(error) => () + }; + + Ok(()) + } + + pub fn add_song_to_db(&mut self, new_song: Song) -> Result<(), Box> { + match self.find_by_uri(&new_song.location) { + Some(_) => return Err(format!("URI already in database: {:?}", new_song.location).into()), + None => () + } + + self.library.push(new_song); + + Ok(()) + } + + pub fn update_song_tags(&mut self, new_tags: Song) -> Result<(), Box> { + match self.find_by_uri(&new_tags.location) { + Some(_) => (), + None => return Err(format!("URI not in database!").into()) + } + + todo!() + } + + /// Query the database, returning a list of items + 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> { + unimplemented!() + } } diff --git a/src/music_tracker/music_tracker.rs b/src/music_tracker/music_tracker.rs index 213a5f1..5c306cc 100644 --- a/src/music_tracker/music_tracker.rs +++ b/src/music_tracker/music_tracker.rs @@ -75,7 +75,7 @@ impl MusicTracker for LastFM { let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).expect("Your time is off.").as_secs() - 30; let string_timestamp = timestamp.to_string(); - let (artist, track) = match (song.artist, song.title) { + let (artist, track) = match (song.get_tag("Artist"), song.get_tag("Title")) { (Some(artist), Some(track)) => (artist, track), _ => return Err(TrackerError::InvalidSong) }; @@ -94,7 +94,7 @@ impl MusicTracker for LastFM { async fn track_now(&mut self, song: Song) -> Result<(), TrackerError> { let mut params: BTreeMap<&str, &str> = BTreeMap::new(); - let (artist, track) = match (song.artist, song.title) { + let (artist, track) = match (song.get_tag("Artist"), song.get_tag("Title")) { (Some(artist), Some(track)) => (artist, track), _ => return Err(TrackerError::InvalidSong) }; @@ -259,11 +259,13 @@ impl DiscordRPC { #[async_trait] impl MusicTracker for DiscordRPC { async fn track_now(&mut self, song: Song) -> Result<(), TrackerError> { + let unknown = String::from("Unknown"); + // Sets song title - let song_name = if let Some(song_name) = song.title { + let song_name = if let Some(song_name) = song.get_tag("Title") { song_name } else { - String::from("Unknown") + &unknown }; let _client_thread = self.client.start(); @@ -316,7 +318,7 @@ pub struct ListenBrainz { #[async_trait] impl MusicTracker for ListenBrainz { async fn track_now(&mut self, song: Song) -> Result<(), TrackerError> { - let (artist, track) = match (song.artist, song.title) { + let (artist, track) = match (song.get_tag("Artist"), song.get_tag("Title")) { (Some(artist), Some(track)) => (artist, track), _ => return Err(TrackerError::InvalidSong) }; @@ -342,7 +344,7 @@ impl MusicTracker for ListenBrainz { async fn track_song(&mut self, song: Song) -> Result<(), TrackerError> { let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).expect("Your time is off.").as_secs() - 30; - let (artist, track) = match (song.artist, song.title) { + let (artist, track) = match (song.get_tag("Artist"), song.get_tag("Title")) { (Some(artist), Some(track)) => (artist, track), _ => return Err(TrackerError::InvalidSong) }; From d2d92149f8544cfae45eb9aa1392ea52fbd5f50f Mon Sep 17 00:00:00 2001 From: G2-Games Date: Mon, 2 Oct 2023 01:26:36 -0500 Subject: [PATCH 03/20] preliminary query support, still some bugs to iron out --- Cargo.toml | 2 + src/music_storage/music_db.rs | 119 +++++++++++++++++++++++++---- src/music_tracker/music_tracker.rs | 13 ++-- 3 files changed, 114 insertions(+), 20 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a96c12a..0f98190 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,3 +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" diff --git a/src/music_storage/music_db.rs b/src/music_storage/music_db.rs index cb429c6..0292912 100644 --- a/src/music_storage/music_db.rs +++ b/src/music_storage/music_db.rs @@ -11,6 +11,7 @@ use serde::{Deserialize, Serialize}; use std::fs; use std::io::BufWriter; use std::path::{Path, PathBuf}; +use unidecode::unidecode; use crate::music_controller::config::Config; @@ -20,6 +21,31 @@ pub struct AlbumArt { pub path: Option, } +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +pub enum Tag { + Title, + Album, + Artist, + Genre, + Comment, + Track, + Key(String) +} + +impl ToString for Tag { + fn to_string(&self) -> String { + match self { + Self::Title => "TrackTitle".into(), + Self::Album => "AlbumTitle".into(), + Self::Artist => "TrackArtist".into(), + Self::Genre => "Genre".into(), + Self::Comment => "Comment".into(), + Self::Track => "TrackNumber".into(), + Self::Key(key) => key.into() + } + } +} + /// Stores information about a single song #[derive(Debug, Clone, Deserialize, Serialize)] pub struct Song { @@ -35,13 +61,15 @@ pub struct Song { pub last_played: Option>, #[serde(with = "ts_seconds_option")] pub date_added: Option>, + #[serde(with = "ts_seconds_option")] + pub date_modified: Option>, pub album_art: Vec, - pub tags: Vec<(String, String)>, + pub tags: Vec<(Tag, String)>, } impl Song { - pub fn get_tag(&self, target_key: &str) -> Option<&String> { - let index = self.tags.iter().position(|r| r.0 == target_key); + pub fn get_tag(&self, target_key: &Tag) -> Option<&String> { + let index = self.tags.iter().position(|r| r.0 == *target_key); match index { Some(i) => return Some(&self.tags[i].1), @@ -53,7 +81,7 @@ impl Song { let mut results = Vec::new(); for tag in &self.tags { for key in target_keys { - if &tag.0 == key { + if &tag.0.to_string() == key { results.push(Some(tag.1.to_owned())) } } @@ -95,6 +123,10 @@ pub struct MusicLibrary { pub library: Vec, } +pub fn normalize(input_string: &String) -> String { + unidecode(input_string).to_ascii_lowercase() +} + impl MusicLibrary { /// Initialize the database /// @@ -225,12 +257,17 @@ impl MusicLibrary { }, }; - let mut tags: Vec<(String, String)> = Vec::new(); + let mut tags: Vec<(Tag, String)> = Vec::new(); for item in tag.items() { - let mut key = String::new(); - match item.key() { - ItemKey::Unknown(unknown) => key.push_str(&unknown), - custom => key = format!("{:?}", custom), + let key = match item.key() { + ItemKey::TrackTitle => Tag::Title, + ItemKey::TrackNumber => Tag::Track, + ItemKey::TrackArtist => Tag::Artist, + ItemKey::Genre => Tag::Genre, + ItemKey::Comment => Tag::Comment, + ItemKey::AlbumTitle => Tag::Album, + ItemKey::Unknown(unknown) => Tag::Key(unknown.to_string()), + custom => Tag::Key(format!("{:?}", custom)), }; let value = match item.value() { @@ -276,6 +313,7 @@ impl MusicLibrary { play_time: Duration::from_secs(0), last_played: None, date_added: Some(chrono::offset::Utc::now()), + date_modified: Some(chrono::offset::Utc::now()), tags, album_art, }; @@ -311,10 +349,63 @@ impl MusicLibrary { /// Query the database, returning a list of items 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> { - unimplemented!() + 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(); + + for track in &self.library { + for tag in &track.tags { + if !target_tags.contains(&tag.0) { + continue; + } + + if normalize(&tag.1).contains(&normalize(&query_string)) { + songs.push(track.clone()); + break + } + } + } + + songs.sort_by(|a, b| { + for opt in sort_by { + let tag_a = match a.get_tag(&opt) { + Some(tag) => tag, + None => continue + }; + + let tag_b = match b.get_tag(&opt) { + Some(tag) => tag, + None => continue + }; + + // Try to parse the tags as f64 (floating-point numbers) + if let (Ok(num_a), Ok(num_b)) = (tag_a.parse::(), tag_b.parse::()) { + // If parsing succeeds, compare as numbers + if num_a < num_b { + return std::cmp::Ordering::Less; + } else if num_a > num_b { + return std::cmp::Ordering::Greater; + } + } else { + // If parsing fails, compare as strings + if tag_a < tag_b { + return std::cmp::Ordering::Less; + } else if tag_a > tag_b { + return std::cmp::Ordering::Greater; + } + } + } + + // If all tags are equal, sort by some default criteria (e.g., song title) + a.get_tag(&Tag::Title).cmp(&b.get_tag(&Tag::Title)) + }); + + if songs.len() > 0 { + Some(songs) + } else { + None + } } } diff --git a/src/music_tracker/music_tracker.rs b/src/music_tracker/music_tracker.rs index 5c306cc..8526e89 100644 --- a/src/music_tracker/music_tracker.rs +++ b/src/music_tracker/music_tracker.rs @@ -8,7 +8,7 @@ use md5::{Md5, Digest}; use discord_presence::{Event}; use surf::StatusCode; -use crate::music_storage::music_db::Song; +use crate::music_storage::music_db::{Song, Tag}; #[async_trait] pub trait MusicTracker { @@ -75,7 +75,7 @@ impl MusicTracker for LastFM { let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).expect("Your time is off.").as_secs() - 30; let string_timestamp = timestamp.to_string(); - let (artist, track) = match (song.get_tag("Artist"), song.get_tag("Title")) { + let (artist, track) = match (song.get_tag(&Tag::Artist), song.get_tag(&Tag::Title)) { (Some(artist), Some(track)) => (artist, track), _ => return Err(TrackerError::InvalidSong) }; @@ -94,7 +94,7 @@ impl MusicTracker for LastFM { async fn track_now(&mut self, song: Song) -> Result<(), TrackerError> { let mut params: BTreeMap<&str, &str> = BTreeMap::new(); - let (artist, track) = match (song.get_tag("Artist"), song.get_tag("Title")) { + let (artist, track) = match (song.get_tag(&Tag::Artist), song.get_tag(&Tag::Title)) { (Some(artist), Some(track)) => (artist, track), _ => return Err(TrackerError::InvalidSong) }; @@ -256,13 +256,14 @@ impl DiscordRPC { } } + #[async_trait] impl MusicTracker for DiscordRPC { async fn track_now(&mut self, song: Song) -> Result<(), TrackerError> { let unknown = String::from("Unknown"); // Sets song title - let song_name = if let Some(song_name) = song.get_tag("Title") { + let song_name = if let Some(song_name) = song.get_tag(&Tag::Title) { song_name } else { &unknown @@ -318,7 +319,7 @@ pub struct ListenBrainz { #[async_trait] impl MusicTracker for ListenBrainz { async fn track_now(&mut self, song: Song) -> Result<(), TrackerError> { - let (artist, track) = match (song.get_tag("Artist"), song.get_tag("Title")) { + let (artist, track) = match (song.get_tag(&Tag::Artist), song.get_tag(&Tag::Title)) { (Some(artist), Some(track)) => (artist, track), _ => return Err(TrackerError::InvalidSong) }; @@ -344,7 +345,7 @@ impl MusicTracker for ListenBrainz { async fn track_song(&mut self, song: Song) -> Result<(), TrackerError> { let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).expect("Your time is off.").as_secs() - 30; - let (artist, track) = match (song.get_tag("Artist"), song.get_tag("Title")) { + let (artist, track) = match (song.get_tag(&Tag::Artist), song.get_tag(&Tag::Title)) { (Some(artist), Some(track)) => (artist, track), _ => return Err(TrackerError::InvalidSong) }; From bde2d194dc898dda6a886d04469660433d513555 Mon Sep 17 00:00:00 2001 From: G2-Games Date: Mon, 2 Oct 2023 20:27:59 -0500 Subject: [PATCH 04/20] Finished main query function, added `MusicLibrary` to `MusicController` --- Cargo.toml | 2 +- src/music_controller/music_controller.rs | 26 ++++++++- src/music_storage/music_db.rs | 73 +++++++++++++++++------- src/music_tracker/music_tracker.rs | 10 +++- 4 files changed, 85 insertions(+), 26 deletions(-) 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)) }); From 140bae703ad96ba2a6ec1c030522622d2d8d4db3 Mon Sep 17 00:00:00 2001 From: G2-Games Date: Tue, 3 Oct 2023 20:00:13 -0500 Subject: [PATCH 05/20] Can query by URI now --- src/music_controller/music_controller.rs | 3 +- src/music_storage/music_db.rs | 73 ++++++++++++++++-------- 2 files changed, 51 insertions(+), 25 deletions(-) diff --git a/src/music_controller/music_controller.rs b/src/music_controller/music_controller.rs index ed3656f..7ae6e69 100644 --- a/src/music_controller/music_controller.rs +++ b/src/music_controller/music_controller.rs @@ -79,8 +79,9 @@ impl MusicController { &self, query_string: &String, target_tags: Vec, + search_location: bool, sort_by: Vec ) -> Option> { - self.library.query(query_string, &target_tags, &sort_by) + self.library.query(query_string, &target_tags, search_location, &sort_by) } } diff --git a/src/music_storage/music_db.rs b/src/music_storage/music_db.rs index 01c12ff..7810ab4 100644 --- a/src/music_storage/music_db.rs +++ b/src/music_storage/music_db.rs @@ -1,5 +1,6 @@ use file_format::{FileFormat, Kind}; use lofty::{AudioFile, ItemKey, ItemValue, Probe, TagType, TaggedFileExt}; +use std::any::Any; use std::{error::Error, io::BufReader}; use chrono::{serde::ts_seconds_option, DateTime, Utc}; @@ -15,7 +16,6 @@ use unidecode::unidecode; // Fun parallel stuff use std::sync::{Arc, Mutex, RwLock}; -use rayon::iter; use rayon::prelude::*; use crate::music_controller::config::Config; @@ -35,20 +35,22 @@ pub enum Tag { Comment, Track, Disk, - Key(String) + Key(String), + Field(String) } impl ToString for Tag { fn to_string(&self) -> String { match self { - Self::Title => "TrackTitle".into(), - Self::Album => "AlbumTitle".into(), + Self::Title => "TrackTitle".into(), + Self::Album => "AlbumTitle".into(), Self::Artist => "TrackArtist".into(), - Self::Genre => "Genre".into(), + Self::Genre => "Genre".into(), Self::Comment => "Comment".into(), - Self::Track => "TrackNumber".into(), - Self::Disk => "DiscNumber".into(), - Self::Key(key) => key.into() + Self::Track => "TrackNumber".into(), + Self::Disk => "DiscNumber".into(), + Self::Key(key) => key.into(), + Self::Field(f) => f.into() } } } @@ -95,17 +97,12 @@ impl Song { } } - pub fn get_tags(&self, target_keys: &Vec) -> Vec> { - let mut results = Vec::new(); - for tag in &self.tags { - for key in target_keys { - if &tag.0.to_string() == key { - results.push(Some(tag.1.to_owned())) - } - } - results.push(None); + pub fn get_field(&self, target_field: &str) -> Box { + match target_field { + "location" => Box::new(self.location.clone()), + "plays" => Box::new(self.plays.clone()), + _ => todo!() } - results } } @@ -116,6 +113,15 @@ pub enum URI { Remote(Service, String), } +impl ToString for URI { + fn to_string(&self) -> String { + match self { + URI::Local(location) => location.to_string(), + URI::Remote(_, location) => location.to_string() + } + } +} + #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)] pub enum Service { InternetRadio, @@ -142,7 +148,7 @@ pub struct MusicLibrary { } pub fn normalize(input_string: &String) -> String { - unidecode(input_string).to_ascii_lowercase().replace(|c: char| !c.is_alphanumeric() && !c.is_ascii_punctuation(), "") + unidecode(input_string).to_ascii_lowercase().replace(|c: char| !c.is_alphanumeric(), "") } impl MusicLibrary { @@ -207,7 +213,9 @@ impl MusicLibrary { Ok(()) } - fn find_by_uri(&self, path: &URI) -> Option { + /// Queries for a [Song] by its [URI], returning a single Song + /// with the URI that matches + fn query_by_uri(&self, path: &URI) -> Option { for track in &self.library { if path == &track.location { return Some(track.clone()); @@ -348,14 +356,14 @@ impl MusicLibrary { match self.add_song_to_db(new_song) { Ok(_) => (), - Err(error) => () + Err(_error) => () }; Ok(()) } pub fn add_song_to_db(&mut self, new_song: Song) -> Result<(), Box> { - match self.find_by_uri(&new_song.location) { + match self.query_by_uri(&new_song.location) { Some(_) => return Err(format!("URI already in database: {:?}", new_song.location).into()), None => () } @@ -366,7 +374,7 @@ impl MusicLibrary { } pub fn update_song_tags(&mut self, new_tags: Song) -> Result<(), Box> { - match self.find_by_uri(&new_tags.location) { + match self.query_by_uri(&new_tags.location) { Some(_) => (), None => return Err(format!("URI not in database!").into()) } @@ -379,6 +387,7 @@ impl MusicLibrary { &self, query_string: &String, // The query itself target_tags: &Vec, // The tags to search + search_location: bool, // Whether to search the location field or not sort_by: &Vec, // Tags to sort the resulting data by ) -> Option> { let songs = Arc::new(Mutex::new(Vec::new())); @@ -391,9 +400,25 @@ impl MusicLibrary { if normalize(&tag.1).contains(&normalize(&query_string)) { songs.lock().unwrap().push(track); - break + return } } + + if !search_location { + return + } + + // Find a URL in the song + match &track.location { + URI::Local(path) if normalize(&path).contains(&normalize(&query_string)) => { + songs.lock().unwrap().push(track); + return + }, + URI::Remote(_, path) if normalize(&path).contains(&normalize(&query_string)) => { + songs.lock().unwrap().push(track); + return + }, _ => () + }; }); let lock = Arc::try_unwrap(songs).expect("Lock still has multiple owners!"); From 450750f5a0f6172f629a475b8e890c88f22104e5 Mon Sep 17 00:00:00 2001 From: G2-Games Date: Tue, 3 Oct 2023 21:15:56 -0500 Subject: [PATCH 06/20] Can now sort by song fields --- src/music_storage/music_db.rs | 51 +++++++++++++++++++++++++---------- 1 file changed, 37 insertions(+), 14 deletions(-) diff --git a/src/music_storage/music_db.rs b/src/music_storage/music_db.rs index 7810ab4..25e7373 100644 --- a/src/music_storage/music_db.rs +++ b/src/music_storage/music_db.rs @@ -85,7 +85,7 @@ impl Song { * * let tag = this_song.get_tag(Tag::Title); * - * assert_eq!(tag, "Title"); + * assert_eq!(tag, "Some Song Title"); * ``` **/ pub fn get_tag(&self, target_key: &Tag) -> Option<&String> { @@ -97,11 +97,11 @@ impl Song { } } - pub fn get_field(&self, target_field: &str) -> Box { + pub fn get_field(&self, target_field: &str) -> Option { match target_field { - "location" => Box::new(self.location.clone()), - "plays" => Box::new(self.plays.clone()), - _ => todo!() + "location" => Some(self.location.clone().to_string()), + "plays" => Some(self.plays.clone().to_string()), + _ => None // Other field types is not yet supported } } } @@ -383,6 +383,9 @@ impl MusicLibrary { } /// Query the database, returning a list of [Song]s + /// + /// The order in which the `sort_by` `Vec` is arranged + /// determines the output sorting pub fn query( &self, query_string: &String, // The query itself @@ -426,18 +429,38 @@ impl MusicLibrary { new_songs.par_sort_by(|a, b| { for opt in sort_by { - let tag_a = match a.get_tag(&opt) { - Some(tag) => tag, - None => continue + let tag_a = match opt { + Tag::Field(field_selection) => { + match a.get_field(field_selection) { + Some(field_value) => field_value, + None => continue + } + }, + _ => { + match a.get_tag(&opt) { + Some(tag_value) => tag_value.to_owned(), + None => continue + } + } }; - let tag_b = match b.get_tag(&opt) { - Some(tag) => tag, - None => continue + let tag_b = match opt { + Tag::Field(field_selection) => { + match b.get_field(field_selection) { + Some(field_value) => field_value, + None => continue + } + }, + _ => { + match b.get_tag(&opt) { + Some(tag_value) => tag_value.to_owned(), + None => continue + } + } }; - // Try to parse the tags as f64 (floating-point numbers) - if let (Ok(num_a), Ok(num_b)) = (tag_a.parse::(), tag_b.parse::()) { + // Try to parse the tags as f64 + if let (Ok(num_a), Ok(num_b)) = (tag_a.parse::(), tag_b.parse::()) { // If parsing succeeds, compare as numbers if num_a < num_b { return std::cmp::Ordering::Less; @@ -454,7 +477,7 @@ impl MusicLibrary { } } - // If all tags are equal, sort by some default criteria (e.g., song title) + // If all tags are equal, sort by title a.get_tag(&Tag::Title).cmp(&b.get_tag(&Tag::Title)) }); From b39c2c006545680684cc0f2c3e4ede0464946204 Mon Sep 17 00:00:00 2001 From: G2-Games Date: Fri, 6 Oct 2023 01:22:55 -0500 Subject: [PATCH 07/20] Updated some small things, extra fixes --- src/music_controller/music_controller.rs | 2 +- src/music_storage/music_db.rs | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/music_controller/music_controller.rs b/src/music_controller/music_controller.rs index 7ae6e69..7d00b8a 100644 --- a/src/music_controller/music_controller.rs +++ b/src/music_controller/music_controller.rs @@ -74,7 +74,7 @@ impl MusicController { self.song_control(DecoderMessage::DSP(DSPMessage::UpdateProcessor(Box::new(self.music_player.music_processor.clone())))); } - /// Queries the library for a `Vec` + /// Queries the [MusicLibrary], returning a `Vec` pub fn query_library( &self, query_string: &String, diff --git a/src/music_storage/music_db.rs b/src/music_storage/music_db.rs index 25e7373..bf7dc23 100644 --- a/src/music_storage/music_db.rs +++ b/src/music_storage/music_db.rs @@ -420,7 +420,8 @@ impl MusicLibrary { URI::Remote(_, path) if normalize(&path).contains(&normalize(&query_string)) => { songs.lock().unwrap().push(track); return - }, _ => () + }, + _ => () }; }); @@ -477,8 +478,8 @@ impl MusicLibrary { } } - // If all tags are equal, sort by title - a.get_tag(&Tag::Title).cmp(&b.get_tag(&Tag::Title)) + // If all tags are equal, sort by Track number + a.get_tag(&Tag::Track).cmp(&b.get_tag(&Tag::Track)) }); if new_songs.len() > 0 { From fa88fd83a99e733ba5910d52aaecb2f0e810027b Mon Sep 17 00:00:00 2001 From: G2-Games Date: Wed, 1 Nov 2023 01:31:58 -0500 Subject: [PATCH 08/20] Large amounts of database work --- Cargo.toml | 6 +- src/music_player/music_player.rs | 13 +- src/music_storage/music_db.rs | 435 +++++++++++++++++++++++-------- 3 files changed, 335 insertions(+), 119 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0885526..69f5abb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ categories = ["multimedia::audio"] [dependencies] file-format = { version = "0.17.3", features = ["reader", "serde"] } -lofty = "0.14.0" +lofty = "0.16.1" serde = { version = "1.0.164", features = ["derive"] } time = "0.3.22" toml = "0.7.5" @@ -35,3 +35,7 @@ chrono = { version = "0.4.31", features = ["serde"] } bincode = "1.3.3" unidecode = "0.3.0" rayon = "1.8.0" +log = "0.4" +pretty_env_logger = "0.4" +cue = "2.0.0" +jwalk = "0.8.1" diff --git a/src/music_player/music_player.rs b/src/music_player/music_player.rs index 478fca1..f12f68c 100644 --- a/src/music_player/music_player.rs +++ b/src/music_player/music_player.rs @@ -84,11 +84,12 @@ impl SongHandler { } }, URI::Remote(_, location) => { - match RemoteSource::new(location.as_ref(), &config) { + match RemoteSource::new(location.to_str().unwrap(), &config) { Ok(remote_source) => Box::new(remote_source), Err(_) => return Err(()), } }, + _ => todo!() }; let mss = MediaSourceStream::new(src, Default::default()); @@ -97,11 +98,11 @@ impl SongHandler { let meta_opts: MetadataOptions = Default::default(); let fmt_opts: FormatOptions = Default::default(); - let mut hint = Hint::new(); + let hint = Hint::new(); let probed = symphonia::default::get_probe().format(&hint, mss, &fmt_opts, &meta_opts).expect("Unsupported format"); - let mut reader = probed.format; + let reader = probed.format; let track = reader.tracks() .iter() @@ -113,7 +114,7 @@ impl SongHandler { let dec_opts: DecoderOptions = Default::default(); - let mut decoder = symphonia::default::get_codecs().make(&track.codec_params, &dec_opts) + let decoder = symphonia::default::get_codecs().make(&track.codec_params, &dec_opts) .expect("unsupported codec"); return Ok(SongHandler {reader, decoder, time_base, duration}); @@ -170,7 +171,7 @@ impl MusicPlayer { if local_config != global_config { update_trackers(&mut trackers); } - + let mut results = Vec::new(); task::block_on(async { let mut futures = Vec::new(); @@ -183,7 +184,7 @@ impl MusicPlayer { } results = join_all(futures).await; }); - + for result in results { status_sender.send(result).unwrap_or_default() } diff --git a/src/music_storage/music_db.rs b/src/music_storage/music_db.rs index bf7dc23..3135b34 100644 --- a/src/music_storage/music_db.rs +++ b/src/music_storage/music_db.rs @@ -1,11 +1,13 @@ use file_format::{FileFormat, Kind}; use lofty::{AudioFile, ItemKey, ItemValue, Probe, TagType, TaggedFileExt}; -use std::any::Any; +use std::ffi::OsStr; use std::{error::Error, io::BufReader}; use chrono::{serde::ts_seconds_option, DateTime, Utc}; use std::time::Duration; -use walkdir::WalkDir; +//use walkdir::WalkDir; +use cue::cd::CD; +use jwalk::WalkDir; use bincode::{deserialize_from, serialize_into}; use serde::{Deserialize, Serialize}; @@ -15,8 +17,8 @@ use std::path::{Path, PathBuf}; use unidecode::unidecode; // Fun parallel stuff -use std::sync::{Arc, Mutex, RwLock}; use rayon::prelude::*; +use std::sync::{Arc, Mutex, RwLock}; use crate::music_controller::config::Config; @@ -36,21 +38,21 @@ pub enum Tag { Track, Disk, Key(String), - Field(String) + Field(String), } impl ToString for Tag { fn to_string(&self) -> String { match self { - Self::Title => "TrackTitle".into(), - Self::Album => "AlbumTitle".into(), + Self::Title => "TrackTitle".into(), + Self::Album => "AlbumTitle".into(), Self::Artist => "TrackArtist".into(), - Self::Genre => "Genre".into(), + Self::Genre => "Genre".into(), Self::Comment => "Comment".into(), - Self::Track => "TrackNumber".into(), - Self::Disk => "DiscNumber".into(), + Self::Track => "TrackNumber".into(), + Self::Disk => "DiscNumber".into(), Self::Key(key) => key.into(), - Self::Field(f) => f.into() + Self::Field(f) => f.into(), } } } @@ -99,27 +101,78 @@ impl Song { pub fn get_field(&self, target_field: &str) -> Option { match target_field { - "location" => Some(self.location.clone().to_string()), + "location" => Some(self.location.clone().path_string()), "plays" => Some(self.plays.clone().to_string()), - _ => None // Other field types is not yet supported + _ => None, // Other field types are not yet supported } } } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub enum URI { - Local(String), - //Cue(String, Duration), TODO: Make cue stuff work - Remote(Service, String), + Local(PathBuf), + Cue { + location: PathBuf, + start: Duration, + end: Duration, + }, + Remote(Service, PathBuf), } -impl ToString for URI { - fn to_string(&self) -> String { +impl URI { + /// Returns the start time of a CUEsheet song, or an + /// error if the URI is not a Cue variant + pub fn start(&self) -> Result<&Duration, Box> { match self { - URI::Local(location) => location.to_string(), - URI::Remote(_, location) => location.to_string() + URI::Local(_) => Err("\"Local\" has no starting time".into()), + URI::Remote(_, _) => Err("\"Remote\" has no starting time".into()), + URI::Cue { + location: _, + start, + end: _, + } => Ok(start), } } + + /// Returns the end time of a CUEsheet song, or an + /// error if the URI is not a Cue variant + pub fn end(&self) -> Result<&Duration, Box> { + match self { + URI::Local(_) => Err("\"Local\" has no starting time".into()), + URI::Remote(_, _) => Err("\"Remote\" has no starting time".into()), + URI::Cue { + location: _, + start: _, + end, + } => Ok(end), + } + } + + /// Returns the location as a PathBuf + pub fn path(&self) -> &PathBuf { + match self { + URI::Local(location) => location, + URI::Cue { + location, + start: _, + end: _, + } => location, + URI::Remote(_, location) => location, + } + } + + fn path_string(&self) -> String { + let path_str = match self { + URI::Local(location) => location.as_path().to_string_lossy(), + URI::Cue { + location, + start: _, + end: _, + } => location.as_path().to_string_lossy(), + URI::Remote(_, location) => location.as_path().to_string_lossy(), + }; + path_str.to_string() + } } #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)] @@ -129,6 +182,7 @@ pub enum Service { Youtube, } +/* TODO: Rework this entirely #[derive(Debug)] pub struct Playlist { title: String, @@ -141,6 +195,7 @@ pub enum MusicObject { Album(Playlist), Playlist(Playlist), } +*/ #[derive(Debug)] pub struct MusicLibrary { @@ -148,7 +203,9 @@ pub struct MusicLibrary { } pub fn normalize(input_string: &String) -> String { - unidecode(input_string).to_ascii_lowercase().replace(|c: char| !c.is_alphanumeric(), "") + unidecode(input_string) + .to_ascii_lowercase() + .replace(|c: char| !c.is_alphanumeric(), "") } impl MusicLibrary { @@ -178,21 +235,24 @@ impl MusicLibrary { let reader = BufReader::new(database); library = deserialize_from(reader)?; } else { - let mut writer = BufWriter::new(fs::File::create(global_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)?; } - }, - Err(error) => return Err(error.into()) + } + Err(error) => return Err(error.into()), }; Ok(Self { library }) } + /// Serializes the database out to the file + /// specified in the config pub fn save(&self, config: &Config) -> Result<(), Box> { match config.db_path.try_exists() { Ok(true) => { - // The database exists, rename it to `.bkp` and - // write the new database + // The database exists, so rename it to `.bkp` and + // write the new database file let mut backup_name = config.db_path.clone(); backup_name.set_extension("bkp"); fs::rename(config.db_path.as_path(), backup_name.as_path())?; @@ -206,71 +266,122 @@ impl MusicLibrary { // Create the database if it does not exist let mut writer = BufWriter::new(fs::File::create(config.db_path.to_path_buf())?); serialize_into(&mut writer, &self.library)?; - }, - Err(error) => return Err(error.into()) + } + Err(error) => return Err(error.into()), } Ok(()) } - /// Queries for a [Song] by its [URI], returning a single Song - /// with the URI that matches - fn query_by_uri(&self, path: &URI) -> Option { - for track in &self.library { - if path == &track.location { - return Some(track.clone()); - } - } - None + pub fn size(&self) -> usize { + self.library.len() } - pub fn find_all_music(&mut self, target_path: &str, config: &Config) -> Result<(), Box> { - let mut current_dir = PathBuf::new(); - for (i, entry) in WalkDir::new(target_path) + /// Queries for a [Song] by its [URI], returning a single `Song` + /// with the `URI` that matches + fn query_uri(&self, path: &URI) -> Option<(&Song, usize)> { + let result = Arc::new(Mutex::new(None)); + let index = Arc::new(Mutex::new(0)); + let _ = &self.library.par_iter().enumerate().for_each(|(i, track)| { + if path == &track.location { + *result.clone().lock().unwrap() = Some(track); + *index.clone().lock().unwrap() = i; + return; + } + }); + let song = Arc::try_unwrap(result).unwrap().into_inner().unwrap(); + match song { + Some(song) => Some((song, Arc::try_unwrap(index).unwrap().into_inner().unwrap())), + None => None, + } + } + + /// Queries for a [Song] by its [PathBuf], returning a `Vec` + /// with matching `PathBuf`s + fn query_path(&self, path: &PathBuf) -> Option> { + let result: Arc>> = Arc::new(Mutex::new(Vec::new())); + let _ = &self.library.par_iter().for_each(|track| { + if path == track.location.path() { + result.clone().lock().unwrap().push(&track); + return; + } + }); + if result.lock().unwrap().len() > 0 { + Some(Arc::try_unwrap(result).unwrap().into_inner().unwrap()) + } else { + None + } + } + + /// Finds all the music files within a specified folder + pub fn find_all_music( + &mut self, + target_path: &str, + config: &Config, + ) -> Result> { + let mut total = 0; + let mut i = 0; + for entry in WalkDir::new(target_path) .follow_links(true) .into_iter() - .filter_map(|e| e.ok()).enumerate() + .filter_map(|e| e.ok()) { let target_file = entry; - let is_file = fs::metadata(target_file.path())?.is_file(); + let path = target_file.path(); - // 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(); + // Ensure the target is a file and not a directory, + // if it isn't a file, skip this loop + if !path.is_file() { continue; } - let format = FileFormat::from_file(target_file.path())?; - let extension = target_file - .path() - .extension() - .expect("Could not find file extension"); + // Check if the file path is already in the db + if self.query_uri(&URI::Local(path.to_path_buf())).is_some() { + continue; + } + + // Save periodically while scanning + i += 1; + if i % 250 == 0 { + self.save(config).unwrap(); + } + + let format = FileFormat::from_file(&path)?; + let extension: &OsStr = match path.extension() { + Some(ext) => ext, + None => OsStr::new(""), + }; // If it's a normal file, add it to the database // if it's a cuesheet, do a bunch of fancy stuff - if format.kind() == Kind::Audio { - 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 + if (format.kind() == Kind::Audio || format.kind() == Kind::Video) + && extension.to_ascii_lowercase() != "log" + && extension.to_ascii_lowercase() != "vob" + { + match self.add_file(&target_file.path()) { + Ok(_) => total += 1, + 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 + total += match self.add_cuesheet(&target_file.path()) { + Ok(added) => added, + Err(error) => { + println!("{}", error); + 0 + } + } } } // Save the database after scanning finishes self.save(&config).unwrap(); - Ok(()) + Ok(total) } - pub fn add_file_to_db(&mut self, target_file: &Path) -> Result<(), Box> { + pub fn add_file(&mut self, target_file: &Path) -> Result<(), Box> { // TODO: Fix error handling here let tagged_file = match lofty::read_from_path(target_file) { Ok(tagged_file) => tagged_file, @@ -296,12 +407,12 @@ impl MusicLibrary { let mut tags: Vec<(Tag, String)> = Vec::new(); for item in tag.items() { let key = match item.key() { - ItemKey::TrackTitle => Tag::Title, + ItemKey::TrackTitle => Tag::Title, ItemKey::TrackNumber => Tag::Track, ItemKey::TrackArtist => Tag::Artist, - ItemKey::Genre => Tag::Genre, - ItemKey::Comment => Tag::Comment, - ItemKey::AlbumTitle => Tag::Album, + ItemKey::Genre => Tag::Genre, + ItemKey::Comment => Tag::Comment, + ItemKey::AlbumTitle => Tag::Album, ItemKey::Unknown(unknown) => Tag::Key(unknown.to_string()), custom => Tag::Key(format!("{:?}", custom)), }; @@ -336,10 +447,9 @@ impl MusicLibrary { // TODO: Fix error handling let binding = fs::canonicalize(target_file).unwrap(); - let abs_path = binding.to_str().unwrap(); let new_song = Song { - location: URI::Local(abs_path.to_string()), + location: URI::Local(binding), plays: 0, skips: 0, favorited: false, @@ -354,18 +464,120 @@ impl MusicLibrary { album_art, }; - match self.add_song_to_db(new_song) { + match self.add_song(new_song) { Ok(_) => (), - Err(_error) => () + Err(error) => return Err(error), }; Ok(()) } - pub fn add_song_to_db(&mut self, new_song: Song) -> Result<(), Box> { - match self.query_by_uri(&new_song.location) { - Some(_) => return Err(format!("URI already in database: {:?}", new_song.location).into()), - None => () + pub fn add_cuesheet(&mut self, cuesheet: &PathBuf) -> Result> { + let mut tracks_added = 0; + + let cue_data = CD::parse_file(cuesheet.to_owned()).unwrap(); + + // Get album level information + let album_title = &cue_data.get_cdtext().read(cue::cd_text::PTI::Title).unwrap_or(String::new()); + let album_artist = &cue_data.get_cdtext().read(cue::cd_text::PTI::Performer).unwrap_or(String::new()); + + let parent_dir = cuesheet.parent().expect("The file has no parent path??"); + for track in cue_data.tracks() { + let audio_location = parent_dir.join(track.get_filename()); + + if !audio_location.exists() { + continue; + } + + // Try to remove the original audio file from the db if it exists + let _ = self.remove_uri(&URI::Local(audio_location.clone())); + + // Get the track timing information + let start = Duration::from_micros((track.get_start() as f32 * 13333.333333).round() as u64); + let duration = match track.get_length() { + Some(len) => Duration::from_micros((len as f32 * 13333.333333).round() as u64), + None => { + let tagged_file = match lofty::read_from_path(&audio_location) { + Ok(tagged_file) => tagged_file, + + Err(_) => match Probe::open(&audio_location)?.read() { + Ok(tagged_file) => tagged_file, + + Err(error) => return Err(error.into()), + }, + }; + + tagged_file.properties().duration() - start + } + }; + let end = start + duration; + + // Get the format as a string + let format: Option = match FileFormat::from_file(&audio_location) { + Ok(fmt) => Some(fmt), + Err(_) => None, + }; + + let mut tags: Vec<(Tag, String)> = Vec::new(); + tags.push((Tag::Album, album_title.clone())); + tags.push((Tag::Key("AlbumArtist".to_string()), album_artist.clone())); + match track.get_cdtext().read(cue::cd_text::PTI::Title) { + Some(title) => tags.push((Tag::Title, title)), + None => () + }; + match track.get_cdtext().read(cue::cd_text::PTI::Performer) { + Some(artist) => tags.push((Tag::Artist, artist)), + None => () + }; + match track.get_cdtext().read(cue::cd_text::PTI::Genre) { + Some(genre) => tags.push((Tag::Genre, genre)), + None => () + }; + match track.get_cdtext().read(cue::cd_text::PTI::Message) { + Some(comment) => tags.push((Tag::Comment, comment)), + None => () + }; + + let album_art = Vec::new(); + + let new_song = Song { + location: URI::Cue{ + location: audio_location, + start, + end, + }, + plays: 0, + skips: 0, + favorited: false, + rating: None, + format, + duration, + play_time: Duration::from_secs(0), + last_played: None, + date_added: Some(chrono::offset::Utc::now()), + date_modified: Some(chrono::offset::Utc::now()), + tags, + album_art, + }; + + match self.add_song(new_song) { + Ok(_) => tracks_added += 1, + Err(_error) => { + //println!("{}", error); + continue + }, + }; + } + + Ok(tracks_added) + } + + pub fn add_song(&mut self, new_song: Song) -> Result<(), Box> { + match self.query_uri(&new_song.location) { + Some(_) => { + return Err(format!("URI already in database: {:?}", new_song.location).into()) + } + None => (), } self.library.push(new_song); @@ -373,10 +585,23 @@ impl MusicLibrary { Ok(()) } - pub fn update_song_tags(&mut self, new_tags: Song) -> Result<(), Box> { - match self.query_by_uri(&new_tags.location) { + /// Removes a song indexed by URI, returning the position removed + pub fn remove_uri(&mut self, target_uri: &URI) -> Result> { + let location = match self.query_uri(target_uri) { + Some(value) => value.1, + None => return Err("URI not in database".into()), + }; + + self.library.remove(location); + + Ok(location) + } + + /// Scan the song by a location and update its tags + pub fn update_by_file(&mut self, new_tags: Song) -> Result<(), Box> { + match self.query_uri(&new_tags.location) { Some(_) => (), - None => return Err(format!("URI not in database!").into()) + None => return Err(format!("URI not in database!").into()), } todo!() @@ -403,61 +628,47 @@ impl MusicLibrary { if normalize(&tag.1).contains(&normalize(&query_string)) { songs.lock().unwrap().push(track); - return + return; } } if !search_location { - return + return; } // Find a URL in the song - match &track.location { - URI::Local(path) if normalize(&path).contains(&normalize(&query_string)) => { - songs.lock().unwrap().push(track); - return - }, - URI::Remote(_, path) if normalize(&path).contains(&normalize(&query_string)) => { - songs.lock().unwrap().push(track); - return - }, - _ => () - }; + if normalize(&track.location.path_string()).contains(&normalize(&query_string)) { + songs.lock().unwrap().push(track); + return; + } }); let lock = Arc::try_unwrap(songs).expect("Lock still has multiple owners!"); let mut new_songs = lock.into_inner().expect("Mutex cannot be locked!"); + // Sort the returned list of songs new_songs.par_sort_by(|a, b| { for opt in sort_by { let tag_a = match opt { - Tag::Field(field_selection) => { - match a.get_field(field_selection) { - Some(field_value) => field_value, - None => continue - } + Tag::Field(field_selection) => match a.get_field(field_selection) { + Some(field_value) => field_value, + None => continue, + }, + _ => match a.get_tag(&opt) { + Some(tag_value) => tag_value.to_owned(), + None => continue, }, - _ => { - match a.get_tag(&opt) { - Some(tag_value) => tag_value.to_owned(), - None => continue - } - } }; let tag_b = match opt { - Tag::Field(field_selection) => { - match b.get_field(field_selection) { - Some(field_value) => field_value, - None => continue - } + Tag::Field(field_selection) => match b.get_field(field_selection) { + Some(field_value) => field_value, + None => continue, + }, + _ => match b.get_tag(&opt) { + Some(tag_value) => tag_value.to_owned(), + None => continue, }, - _ => { - match b.get_tag(&opt) { - Some(tag_value) => tag_value.to_owned(), - None => continue - } - } }; // Try to parse the tags as f64 From 60add901bde01f5804f191a5d747896fb1bfd2f6 Mon Sep 17 00:00:00 2001 From: G2-Games Date: Wed, 1 Nov 2023 10:57:15 -0500 Subject: [PATCH 09/20] Finished CUE file support --- src/music_storage/music_db.rs | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/src/music_storage/music_db.rs b/src/music_storage/music_db.rs index 3135b34..7eae5b5 100644 --- a/src/music_storage/music_db.rs +++ b/src/music_storage/music_db.rs @@ -493,9 +493,19 @@ impl MusicLibrary { let _ = self.remove_uri(&URI::Local(audio_location.clone())); // Get the track timing information - let start = Duration::from_micros((track.get_start() as f32 * 13333.333333).round() as u64); + let pregap = match track.get_zero_pre() { + Some(pregap) => Duration::from_micros((pregap as f32 * 13333.333333) as u64), + None => Duration::from_secs(0) + }; + let postgap = match track.get_zero_post() { + Some(postgap) => Duration::from_micros((postgap as f32 * 13333.333333) as u64), + None => Duration::from_secs(0) + }; + let mut start = Duration::from_micros((track.get_start() as f32 * 13333.333333) as u64); + start -= pregap; + let duration = match track.get_length() { - Some(len) => Duration::from_micros((len as f32 * 13333.333333).round() as u64), + Some(len) => Duration::from_micros((len as f32 * 13333.333333) as u64), None => { let tagged_file = match lofty::read_from_path(&audio_location) { Ok(tagged_file) => tagged_file, @@ -510,7 +520,7 @@ impl MusicLibrary { tagged_file.properties().duration() - start } }; - let end = start + duration; + let end = start + duration + postgap; // Get the format as a string let format: Option = match FileFormat::from_file(&audio_location) { @@ -563,7 +573,7 @@ impl MusicLibrary { match self.add_song(new_song) { Ok(_) => tracks_added += 1, Err(_error) => { - //println!("{}", error); + println!("{}", _error); continue }, }; @@ -579,6 +589,17 @@ impl MusicLibrary { } None => (), } + match self.query_path(&new_song.location.path()) { + Some(songs) => { + for song in songs { + match &song.location { + URI::Local(location) => return Err(format!("Cuesheet exists: {:?}", new_song.location).into()), + _ => () + } + } + } + None => (), + } self.library.push(new_song); From c5a631e30f38d8edec11a97b2b57598ab7cbaa18 Mon Sep 17 00:00:00 2001 From: G2-Games Date: Wed, 1 Nov 2023 14:13:25 -0500 Subject: [PATCH 10/20] Fixed duplicate entries --- src/music_storage/music_db.rs | 48 ++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/src/music_storage/music_db.rs b/src/music_storage/music_db.rs index 7eae5b5..e59f51c 100644 --- a/src/music_storage/music_db.rs +++ b/src/music_storage/music_db.rs @@ -161,7 +161,7 @@ impl URI { } } - fn path_string(&self) -> String { + pub fn path_string(&self) -> String { let path_str = match self { URI::Local(location) => location.as_path().to_string_lossy(), URI::Cue { @@ -359,7 +359,10 @@ impl MusicLibrary { && extension.to_ascii_lowercase() != "vob" { match self.add_file(&target_file.path()) { - Ok(_) => total += 1, + Ok(_) => { + //println!("{:?}", target_file.path()); + total += 1 + }, Err(_error) => { //println!("{}, {:?}: {}", format, target_file.file_name(), _error) } // TODO: Handle more of these errors @@ -482,7 +485,7 @@ impl MusicLibrary { let album_artist = &cue_data.get_cdtext().read(cue::cd_text::PTI::Performer).unwrap_or(String::new()); let parent_dir = cuesheet.parent().expect("The file has no parent path??"); - for track in cue_data.tracks() { + for (i, track) in cue_data.tracks().iter().enumerate() { let audio_location = parent_dir.join(track.get_filename()); if !audio_location.exists() { @@ -528,12 +531,21 @@ impl MusicLibrary { Err(_) => None, }; + // Get some useful tags let mut tags: Vec<(Tag, String)> = Vec::new(); tags.push((Tag::Album, album_title.clone())); tags.push((Tag::Key("AlbumArtist".to_string()), album_artist.clone())); match track.get_cdtext().read(cue::cd_text::PTI::Title) { Some(title) => tags.push((Tag::Title, title)), - None => () + None => { + match track.get_cdtext().read(cue::cd_text::PTI::UPC_ISRC) { + Some(title) => tags.push((Tag::Title, title)), + None => { + let namestr = format!("{} - {}", i, track.get_filename()); + tags.push((Tag::Title, namestr)) + } + } + } }; match track.get_cdtext().read(cue::cd_text::PTI::Performer) { Some(artist) => tags.push((Tag::Artist, artist)), @@ -573,7 +585,7 @@ impl MusicLibrary { match self.add_song(new_song) { Ok(_) => tracks_added += 1, Err(_error) => { - println!("{}", _error); + //println!("{}", _error); continue }, }; @@ -589,16 +601,11 @@ impl MusicLibrary { } None => (), } - match self.query_path(&new_song.location.path()) { - Some(songs) => { - for song in songs { - match &song.location { - URI::Local(location) => return Err(format!("Cuesheet exists: {:?}", new_song.location).into()), - _ => () - } - } - } - None => (), + match new_song.location { + URI::Local(_) if self.query_path(&new_song.location.path()).is_some() => { + return Err(format!("Location exists for {:?}", new_song.location).into()) + }, + _ => () } self.library.push(new_song); @@ -642,12 +649,13 @@ impl MusicLibrary { let songs = Arc::new(Mutex::new(Vec::new())); self.library.par_iter().for_each(|track| { - for tag in &track.tags { - if !target_tags.contains(&tag.0) { - continue; - } + for tag in target_tags { + let track_result = match track.get_tag(&tag) { + Some(value) => value, + None => continue + }; - if normalize(&tag.1).contains(&normalize(&query_string)) { + if normalize(track_result).contains(&normalize(&query_string)) { songs.lock().unwrap().push(track); return; } From 085927caa16c39968463b8bfacf6eea36f629e8e Mon Sep 17 00:00:00 2001 From: G2-Games Date: Wed, 1 Nov 2023 14:14:28 -0500 Subject: [PATCH 11/20] `cargo fmt` --- src/lib.rs | 6 +- src/music_controller/config.rs | 30 +-- src/music_controller/init.rs | 11 +- src/music_controller/music_controller.rs | 45 ++-- src/music_player/music_output.rs | 153 ++++++++----- src/music_player/music_player.rs | 264 ++++++++++++++--------- src/music_player/music_resampler.rs | 13 +- src/music_processor/music_processor.rs | 20 +- src/music_storage/music_db.rs | 50 +++-- src/music_storage/playlist.rs | 8 +- src/music_tracker/music_tracker.rs | 202 ++++++++++------- 11 files changed, 474 insertions(+), 328 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 3a25b3d..75b98ee 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,13 +12,13 @@ pub mod music_processor { } pub mod music_player { - pub mod music_player; pub mod music_output; + pub mod music_player; pub mod music_resampler; } pub mod music_controller { - pub mod music_controller; pub mod config; pub mod init; -} \ No newline at end of file + pub mod music_controller; +} diff --git a/src/music_controller/config.rs b/src/music_controller/config.rs index f6909f5..82cbd22 100644 --- a/src/music_controller/config.rs +++ b/src/music_controller/config.rs @@ -1,10 +1,10 @@ -use std::path::PathBuf; -use std::fs::read_to_string; use std::fs; +use std::fs::read_to_string; +use std::path::PathBuf; use serde::{Deserialize, Serialize}; -use crate::music_tracker::music_tracker::{LastFMConfig, DiscordRPCConfig, ListenBrainzConfig}; +use crate::music_tracker::music_tracker::{DiscordRPCConfig, LastFMConfig, ListenBrainzConfig}; #[derive(Serialize, Deserialize, PartialEq, Eq)] pub struct Config { @@ -22,51 +22,53 @@ impl Default for Config { db_path: Box::new(path), lastfm: None, - + discord: Some(DiscordRPCConfig { enabled: true, dango_client_id: 1144475145864499240, dango_icon: String::from("flat"), }), - + listenbrainz: Some(ListenBrainzConfig { enabled: false, api_url: String::from("https://api.listenbrainz.org"), auth_token: String::from(""), - }) + }), }; } } impl Config { /// Creates and saves a new config with default values - pub fn new(config_file: &PathBuf) -> std::io::Result { + pub fn new(config_file: &PathBuf) -> std::io::Result { let config = Config::default(); config.save(config_file)?; - + Ok(config) } /// Loads config from given file path pub fn from(config_file: &PathBuf) -> std::result::Result { - return toml::from_str(&read_to_string(config_file) - .expect("Failed to initalize music config: File not found!")); + return toml::from_str( + &read_to_string(config_file) + .expect("Failed to initalize music config: File not found!"), + ); } - + /// Saves config to given path /// Saves -> temp file, if successful, removes old config, and renames temp to given path pub fn save(&self, config_file: &PathBuf) -> std::io::Result<()> { let toml = toml::to_string_pretty(self).unwrap(); - + let mut temp_file = config_file.clone(); temp_file.set_extension("tomltemp"); - + fs::write(&temp_file, toml)?; // If configuration file already exists, delete it match fs::metadata(config_file) { Ok(_) => fs::remove_file(config_file)?, - Err(_) => {}, + Err(_) => {} } fs::rename(temp_file, config_file)?; diff --git a/src/music_controller/init.rs b/src/music_controller/init.rs index 80b91bf..38b101f 100644 --- a/src/music_controller/init.rs +++ b/src/music_controller/init.rs @@ -1,9 +1,7 @@ -use std::path::Path; use std::fs::File; +use std::path::Path; -pub fn init() { - -} +pub fn init() {} fn init_config() { let config_path = "./config.toml"; @@ -13,7 +11,4 @@ fn init_config() { } } -fn init_db() { - -} - +fn init_db() {} diff --git a/src/music_controller/music_controller.rs b/src/music_controller/music_controller.rs index 7d00b8a..24b8379 100644 --- a/src/music_controller/music_controller.rs +++ b/src/music_controller/music_controller.rs @@ -1,8 +1,8 @@ use std::path::PathBuf; -use std::sync::{RwLock, Arc}; +use std::sync::{Arc, RwLock}; use crate::music_controller::config::Config; -use crate::music_player::music_player::{MusicPlayer, PlayerStatus, DecoderMessage, DSPMessage}; +use crate::music_player::music_player::{DSPMessage, DecoderMessage, MusicPlayer, PlayerStatus}; use crate::music_storage::music_db::{MusicLibrary, Song, Tag}; pub struct MusicController { @@ -13,75 +13,78 @@ pub struct MusicController { 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) + Err(error) => return Err(error), }; - + let controller = MusicController { config, library, music_player, }; - - return Ok(controller) + + return Ok(controller); } - + /// Creates new music controller from a config at given path 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) + Err(error) => return Err(error), }; - + let controller = MusicController { config, library, music_player, }; - - return Ok(controller) + + return Ok(controller); } - + /// Sends given message to control music player pub fn song_control(&mut self, message: DecoderMessage) { self.music_player.send_message(message); } - + /// Gets status of the music player pub fn player_status(&mut self) -> PlayerStatus { return self.music_player.get_status(); } - + /// Gets current song being controlled, if any pub fn get_current_song(&self) -> Option { return self.music_player.get_current_song(); } - + /// Gets audio playback volume pub fn get_vol(&self) -> f32 { return self.music_player.music_processor.audio_volume; } - + /// Sets audio playback volume on a scale of 0.0 to 1.0 pub fn set_vol(&mut self, volume: f32) { self.music_player.music_processor.audio_volume = volume; - 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 [MusicLibrary], returning a `Vec` pub fn query_library( &self, query_string: &String, target_tags: Vec, search_location: bool, - sort_by: Vec + sort_by: Vec, ) -> Option> { - self.library.query(query_string, &target_tags, search_location, &sort_by) + self.library + .query(query_string, &target_tags, search_location, &sort_by) } } diff --git a/src/music_player/music_output.rs b/src/music_player/music_output.rs index 795e9e9..76e6d3b 100644 --- a/src/music_player/music_output.rs +++ b/src/music_player/music_output.rs @@ -1,7 +1,7 @@ use std::{result, thread}; -use symphonia::core::audio::{AudioBufferRef, SignalSpec, RawSample, SampleBuffer}; -use symphonia::core::conv::{ConvertibleSample, IntoSample, FromSample}; +use symphonia::core::audio::{AudioBufferRef, RawSample, SampleBuffer, SignalSpec}; +use symphonia::core::conv::{ConvertibleSample, FromSample, IntoSample}; use symphonia::core::units::Duration; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; @@ -25,10 +25,21 @@ pub enum AudioOutputError { pub type Result = result::Result; -pub trait OutputSample: SizedSample + FromSample + IntoSample +cpal::Sample + ConvertibleSample + RawSample + std::marker::Send + 'static {} +pub trait OutputSample: + SizedSample + + FromSample + + IntoSample + + cpal::Sample + + ConvertibleSample + + RawSample + + std::marker::Send + + 'static +{ +} pub struct AudioOutput -where T: OutputSample, +where + T: OutputSample, { ring_buf_producer: rb::Producer, sample_buf: SampleBuffer, @@ -48,80 +59,112 @@ impl OutputSample for f64 {} //create a new trait with functions, then impl that somehow pub fn open_stream(spec: SignalSpec, duration: Duration) -> Result> { - let host = cpal::default_host(); - - // Uses default audio device - let device = match host.default_output_device() { - Some(device) => device, - _ => return Err(AudioOutputError::OpenStreamError), - }; - - let config = match device.default_output_config() { - Ok(config) => config, - Err(err) => return Err(AudioOutputError::OpenStreamError), - }; - - return match config.sample_format(){ - cpal::SampleFormat::I8 => AudioOutput::::create_stream(spec, &device, &config.into(), duration), - cpal::SampleFormat::I16 => AudioOutput::::create_stream(spec, &device, &config.into(), duration), - cpal::SampleFormat::I32 => AudioOutput::::create_stream(spec, &device, &config.into(), duration), - //cpal::SampleFormat::I64 => AudioOutput::::create_stream(spec, &device, &config.into(), duration), - cpal::SampleFormat::U8 => AudioOutput::::create_stream(spec, &device, &config.into(), duration), - cpal::SampleFormat::U16 => AudioOutput::::create_stream(spec, &device, &config.into(), duration), - cpal::SampleFormat::U32 => AudioOutput::::create_stream(spec, &device, &config.into(), duration), - //cpal::SampleFormat::U64 => AudioOutput::::create_stream(spec, &device, &config.into(), duration), - cpal::SampleFormat::F32 => AudioOutput::::create_stream(spec, &device, &config.into(), duration), - cpal::SampleFormat::F64 => AudioOutput::::create_stream(spec, &device, &config.into(), duration), - _ => todo!(), - }; + let host = cpal::default_host(); + + // Uses default audio device + let device = match host.default_output_device() { + Some(device) => device, + _ => return Err(AudioOutputError::OpenStreamError), + }; + + let config = match device.default_output_config() { + Ok(config) => config, + Err(err) => return Err(AudioOutputError::OpenStreamError), + }; + + return match config.sample_format() { + cpal::SampleFormat::I8 => { + AudioOutput::::create_stream(spec, &device, &config.into(), duration) + } + cpal::SampleFormat::I16 => { + AudioOutput::::create_stream(spec, &device, &config.into(), duration) + } + cpal::SampleFormat::I32 => { + AudioOutput::::create_stream(spec, &device, &config.into(), duration) + } + //cpal::SampleFormat::I64 => AudioOutput::::create_stream(spec, &device, &config.into(), duration), + cpal::SampleFormat::U8 => { + AudioOutput::::create_stream(spec, &device, &config.into(), duration) + } + cpal::SampleFormat::U16 => { + AudioOutput::::create_stream(spec, &device, &config.into(), duration) + } + cpal::SampleFormat::U32 => { + AudioOutput::::create_stream(spec, &device, &config.into(), duration) + } + //cpal::SampleFormat::U64 => AudioOutput::::create_stream(spec, &device, &config.into(), duration), + cpal::SampleFormat::F32 => { + AudioOutput::::create_stream(spec, &device, &config.into(), duration) + } + cpal::SampleFormat::F64 => { + AudioOutput::::create_stream(spec, &device, &config.into(), duration) + } + _ => todo!(), + }; } impl AudioOutput { // Creates the stream (TODO: Merge w/open_stream?) - fn create_stream(spec: SignalSpec, device: &cpal::Device, config: &cpal::StreamConfig, duration: Duration) -> Result> { + fn create_stream( + spec: SignalSpec, + device: &cpal::Device, + config: &cpal::StreamConfig, + duration: Duration, + ) -> Result> { let num_channels = config.channels as usize; - + // Ring buffer is created with 200ms audio capacity let ring_len = ((50 * config.sample_rate.0 as usize) / 1000) * num_channels; - let ring_buf= rb::SpscRb::new(ring_len); - + let ring_buf = rb::SpscRb::new(ring_len); + let ring_buf_producer = ring_buf.producer(); let ring_buf_consumer = ring_buf.consumer(); - + let stream_result = device.build_output_stream( - config, + config, move |data: &mut [T], _: &cpal::OutputCallbackInfo| { // Writes samples in the ring buffer to the audio output let written = ring_buf_consumer.read(data).unwrap_or(0); - + // Mutes non-written samples - data[written..].iter_mut().for_each(|sample| *sample = T::MID); + data[written..] + .iter_mut() + .for_each(|sample| *sample = T::MID); }, //TODO: Handle error here properly move |err| println!("Yeah we erroring out here"), - None + None, ); - + if let Err(err) = stream_result { return Err(AudioOutputError::OpenStreamError); } - + let stream = stream_result.unwrap(); - + //Start output stream if let Err(err) = stream.play() { return Err(AudioOutputError::PlayStreamError); } - + let sample_buf = SampleBuffer::::new(duration, spec); - + let mut resampler = None; if spec.rate != config.sample_rate.0 { println!("Resampling enabled"); - resampler = Some(Resampler::new(spec, config.sample_rate.0 as usize, duration)) + resampler = Some(Resampler::new( + spec, + config.sample_rate.0 as usize, + duration, + )) } - - Ok(Box::new(AudioOutput { ring_buf_producer, sample_buf, stream, resampler})) + + Ok(Box::new(AudioOutput { + ring_buf_producer, + sample_buf, + stream, + resampler, + })) } } @@ -131,7 +174,7 @@ impl AudioStream for AudioOutput { if decoded.frames() == 0 { return Ok(()); } - + let mut samples: &[T] = if let Some(resampler) = &mut self.resampler { // Resamples if required match resampler.resample(decoded) { @@ -142,25 +185,25 @@ impl AudioStream for AudioOutput { self.sample_buf.copy_interleaved_ref(decoded); self.sample_buf.samples() }; - + // Write samples into ring buffer while let Some(written) = self.ring_buf_producer.write_blocking(samples) { samples = &samples[written..]; } - + Ok(()) } - + // Flushes resampler if needed fn flush(&mut self) { if let Some(resampler) = &mut self.resampler { - let mut stale_samples = resampler.flush().unwrap_or_default(); - + let mut stale_samples = resampler.flush().unwrap_or_default(); + while let Some(written) = self.ring_buf_producer.write_blocking(stale_samples) { stale_samples = &stale_samples[written..]; } } - + let _ = self.stream.pause(); } -} \ No newline at end of file +} diff --git a/src/music_player/music_player.rs b/src/music_player/music_player.rs index f12f68c..9ed99e5 100644 --- a/src/music_player/music_player.rs +++ b/src/music_player/music_player.rs @@ -1,18 +1,18 @@ -use std::sync::mpsc::{self, Sender, Receiver}; +use std::io::SeekFrom; +use std::sync::mpsc::{self, Receiver, Sender}; use std::sync::{Arc, RwLock}; use std::thread; -use std::io::SeekFrom; use async_std::io::ReadExt; use async_std::task; use futures::future::join_all; -use symphonia::core::codecs::{CODEC_TYPE_NULL, DecoderOptions, Decoder}; +use symphonia::core::codecs::{Decoder, DecoderOptions, CODEC_TYPE_NULL}; +use symphonia::core::errors::Error; use symphonia::core::formats::{FormatOptions, FormatReader, SeekMode, SeekTo}; -use symphonia::core::io::{MediaSourceStream, MediaSource}; +use symphonia::core::io::{MediaSource, MediaSourceStream}; use symphonia::core::meta::MetadataOptions; use symphonia::core::probe::Hint; -use symphonia::core::errors::Error; use symphonia::core::units::{Time, TimeBase}; use futures::AsyncBufRead; @@ -20,8 +20,10 @@ use futures::AsyncBufRead; use crate::music_controller::config::Config; use crate::music_player::music_output::AudioStream; use crate::music_processor::music_processor::MusicProcessor; -use crate::music_storage::music_db::{URI, Song}; -use crate::music_tracker::music_tracker::{MusicTracker, TrackerError, LastFM, DiscordRPC, ListenBrainz}; +use crate::music_storage::music_db::{Song, URI}; +use crate::music_tracker::music_tracker::{ + DiscordRPC, LastFM, ListenBrainz, MusicTracker, TrackerError, +}; // Struct that controls playback of music pub struct MusicPlayer { @@ -49,18 +51,18 @@ pub enum DecoderMessage { Pause, Stop, SeekTo(u64), - DSP(DSPMessage) + DSP(DSPMessage), } #[derive(Clone)] pub enum TrackerMessage { Track(Song), - TrackNow(Song) + TrackNow(Song), } #[derive(Debug, Clone)] pub enum DSPMessage { - UpdateProcessor(Box) + UpdateProcessor(Box), } // Holds a song decoder reader, etc @@ -75,49 +77,59 @@ struct SongHandler { impl SongHandler { pub fn new(uri: &URI) -> Result { // Opens remote/local source and creates MediaSource for symphonia - let config = RemoteOptions {media_buffer_len: 10000, forward_buffer_len: 10000}; + let config = RemoteOptions { + media_buffer_len: 10000, + forward_buffer_len: 10000, + }; let src: Box = match uri { - URI::Local(path) => { - match std::fs::File::open(path) { - Ok(file) => Box::new(file), - Err(_) => return Err(()), - } + URI::Local(path) => match std::fs::File::open(path) { + Ok(file) => Box::new(file), + Err(_) => return Err(()), }, URI::Remote(_, location) => { match RemoteSource::new(location.to_str().unwrap(), &config) { Ok(remote_source) => Box::new(remote_source), Err(_) => return Err(()), } - }, - _ => todo!() + } + _ => todo!(), }; - + let mss = MediaSourceStream::new(src, Default::default()); - + // Use default metadata and format options let meta_opts: MetadataOptions = Default::default(); let fmt_opts: FormatOptions = Default::default(); let hint = Hint::new(); - - let probed = symphonia::default::get_probe().format(&hint, mss, &fmt_opts, &meta_opts).expect("Unsupported format"); - - let reader = probed.format; - - let track = reader.tracks() - .iter() - .find(|t| t.codec_params.codec != CODEC_TYPE_NULL) - .expect("no supported audio tracks"); - + + let probed = symphonia::default::get_probe() + .format(&hint, mss, &fmt_opts, &meta_opts) + .expect("Unsupported format"); + + let reader = probed.format; + + let track = reader + .tracks() + .iter() + .find(|t| t.codec_params.codec != CODEC_TYPE_NULL) + .expect("no supported audio tracks"); + let time_base = track.codec_params.time_base; let duration = track.codec_params.n_frames; - + let dec_opts: DecoderOptions = Default::default(); - - let decoder = symphonia::default::get_codecs().make(&track.codec_params, &dec_opts) - .expect("unsupported codec"); - - return Ok(SongHandler {reader, decoder, time_base, duration}); + + let decoder = symphonia::default::get_codecs() + .make(&track.codec_params, &dec_opts) + .expect("unsupported codec"); + + return Ok(SongHandler { + reader, + decoder, + time_base, + duration, + }); } } @@ -127,9 +139,14 @@ impl MusicPlayer { let (message_sender, message_receiver) = mpsc::channel(); let (status_sender, status_receiver) = mpsc::channel(); let current_song = Arc::new(RwLock::new(None)); - - MusicPlayer::start_player(message_receiver, status_sender, config.clone(), current_song.clone()); - + + MusicPlayer::start_player( + message_receiver, + status_sender, + config.clone(), + current_song.clone(), + ); + MusicPlayer { music_processor: MusicProcessor::new(), music_trackers: Vec::new(), @@ -140,15 +157,19 @@ impl MusicPlayer { config, } } - - fn start_tracker(status_sender: Sender>, tracker_receiver: Receiver, config: Arc>) { + + fn start_tracker( + status_sender: Sender>, + tracker_receiver: Receiver, + config: Arc>, + ) { thread::spawn(move || { let global_config = &*config.read().unwrap(); // Sets local config for trackers to detect changes let local_config = global_config.clone(); let mut trackers: Vec> = Vec::new(); // Updates local trackers to the music controller config // TODO: refactor - let update_trackers = |trackers: &mut Vec>|{ + let update_trackers = |trackers: &mut Vec>| { if let Some(lastfm_config) = global_config.lastfm.clone() { if lastfm_config.enabled { trackers.push(Box::new(LastFM::new(&lastfm_config))); @@ -177,9 +198,13 @@ impl MusicPlayer { let mut futures = Vec::new(); for tracker in trackers.iter_mut() { match message.clone() { - Ok(TrackerMessage::Track(song)) => futures.push(tracker.track_song(song)), - Ok(TrackerMessage::TrackNow(song)) => futures.push(tracker.track_now(song)), - Err(_) => {}, + Ok(TrackerMessage::Track(song)) => { + futures.push(tracker.track_song(song)) + } + Ok(TrackerMessage::TrackNow(song)) => { + futures.push(tracker.track_now(song)) + } + Err(_) => {} } } results = join_all(futures).await; @@ -192,30 +217,41 @@ impl MusicPlayer { } }); } - + // Opens and plays song with given path in separate thread - fn start_player(message_receiver: Receiver, status_sender: Sender, config: Arc>, current_song: Arc>>) { + fn start_player( + message_receiver: Receiver, + status_sender: Sender, + config: Arc>, + current_song: Arc>>, + ) { // Creates thread that audio is decoded in thread::spawn(move || { let current_song = current_song; - + let mut song_handler = None; - + let mut seek_time: Option = None; - + let mut audio_output: Option> = None; - + let mut music_processor = MusicProcessor::new(); - - let (tracker_sender, tracker_receiver): (Sender, Receiver) = mpsc::channel(); - let (tracker_status_sender, tracker_status_receiver): (Sender>, Receiver>) = mpsc::channel(); - + + let (tracker_sender, tracker_receiver): ( + Sender, + Receiver, + ) = mpsc::channel(); + let (tracker_status_sender, tracker_status_receiver): ( + Sender>, + Receiver>, + ) = mpsc::channel(); + MusicPlayer::start_tracker(tracker_status_sender, tracker_receiver, config); - + let mut song_tracked = false; let mut song_time = 0.0; let mut paused = true; - 'main_decode: loop { + 'main_decode: loop { 'handle_message: loop { let message = if paused { // Pauses playback by blocking on waiting for new player messages @@ -254,26 +290,34 @@ impl MusicPlayer { status_sender.send(PlayerStatus::Paused).unwrap(); } Some(DecoderMessage::SeekTo(time)) => seek_time = Some(time), - Some(DecoderMessage::DSP(dsp_message)) => { - match dsp_message { - DSPMessage::UpdateProcessor(new_processor) => music_processor = *new_processor, + Some(DecoderMessage::DSP(dsp_message)) => match dsp_message { + DSPMessage::UpdateProcessor(new_processor) => { + music_processor = *new_processor } - } + }, // Exits main decode loop and subsequently ends thread Some(DecoderMessage::Stop) => { status_sender.send(PlayerStatus::Stopped).unwrap(); - break 'main_decode + break 'main_decode; } - None => {}, + None => {} } status_sender.send(PlayerStatus::Error).unwrap(); - } + } // In theory this check should not need to occur? - if let (Some(song_handler), current_song) = (&mut song_handler, &*current_song.read().unwrap()) { + if let (Some(song_handler), current_song) = + (&mut song_handler, &*current_song.read().unwrap()) + { match seek_time { Some(time) => { - let seek_to = SeekTo::Time { time: Time::from(time), track_id: Some(0) }; - song_handler.reader.seek(SeekMode::Accurate, seek_to).unwrap(); + let seek_to = SeekTo::Time { + time: Time::from(time), + track_id: Some(0), + }; + song_handler + .reader + .seek(SeekMode::Accurate, seek_to) + .unwrap(); seek_time = None; } None => {} //Nothing to do! @@ -286,60 +330,74 @@ impl MusicPlayer { panic!("{}", err); } }; - + if let (Some(time_base), Some(song)) = (song_handler.time_base, current_song) { let time_units = time_base.calc_time(packet.ts); song_time = time_units.seconds as f64 + time_units.frac; // Tracks song now if song has just started if song_time == 0.0 { - tracker_sender.send(TrackerMessage::TrackNow(song.clone())).unwrap(); + tracker_sender + .send(TrackerMessage::TrackNow(song.clone())) + .unwrap(); } - + if let Some(duration) = song_handler.duration { let song_duration = time_base.calc_time(duration); - let song_duration_secs = song_duration.seconds as f64 + song_duration.frac; + let song_duration_secs = + song_duration.seconds as f64 + song_duration.frac; // Tracks song if current time is past half of total song duration or past 4 minutes - if (song_duration_secs / 2.0 < song_time || song_time > 240.0) && !song_tracked { + if (song_duration_secs / 2.0 < song_time || song_time > 240.0) + && !song_tracked + { song_tracked = true; - tracker_sender.send(TrackerMessage::Track(song.clone())).unwrap(); + tracker_sender + .send(TrackerMessage::Track(song.clone())) + .unwrap(); } } } - - status_sender.send(PlayerStatus::Playing(song_time)).unwrap(); - + + status_sender + .send(PlayerStatus::Playing(song_time)) + .unwrap(); + match song_handler.decoder.decode(&packet) { Ok(decoded) => { // Opens audio stream if there is not one if audio_output.is_none() { let spec = *decoded.spec(); let duration = decoded.capacity() as u64; - - audio_output.replace(crate::music_player::music_output::open_stream(spec, duration).unwrap()); + + audio_output.replace( + crate::music_player::music_output::open_stream(spec, duration) + .unwrap(), + ); } // Handles audio normally provided there is an audio stream if let Some(ref mut audio_output) = audio_output { // Changes buffer of the MusicProcessor if the packet has a differing capacity or spec - if music_processor.audio_buffer.capacity() != decoded.capacity() ||music_processor.audio_buffer.spec() != decoded.spec() { + if music_processor.audio_buffer.capacity() != decoded.capacity() + || music_processor.audio_buffer.spec() != decoded.spec() + { let spec = *decoded.spec(); let duration = decoded.capacity() as u64; - + music_processor.set_buffer(duration, spec); } let transformed_audio = music_processor.process(&decoded); - + // Writes transformed packet to audio out audio_output.write(transformed_audio).unwrap() } - }, + } Err(Error::IoError(_)) => { // rest in peace packet continue; - }, + } Err(Error::DecodeError(_)) => { // may you one day be decoded continue; - }, + } Err(err) => { // Unrecoverable, though shouldn't panic here panic!("{}", err); @@ -349,28 +407,28 @@ impl MusicPlayer { } }); } - + // Updates status by checking on messages from spawned thread fn update_player(&mut self) { for message in self.status_receiver.try_recv() { self.player_status = message; } } - - pub fn get_current_song(&self) -> Option{ + + pub fn get_current_song(&self) -> Option { match self.current_song.try_read() { Ok(song) => return (*song).clone(), Err(_) => return None, } } - + // Sends message to spawned thread pub fn send_message(&mut self, message: DecoderMessage) { self.update_player(); // Checks that message sender exists before sending a message off self.message_sender.send(message).unwrap(); } - + pub fn get_status(&mut self) -> PlayerStatus { self.update_player(); return self.player_status; @@ -394,7 +452,7 @@ impl Default for RemoteOptions { media_buffer_len: 100000, forward_buffer_len: 1024, } - } + } } /// A remote source of media @@ -408,12 +466,12 @@ struct RemoteSource { impl RemoteSource { /// Creates a new RemoteSource with given uri and configuration pub fn new(uri: &str, config: &RemoteOptions) -> Result { - let mut response = task::block_on(async { + let mut response = task::block_on(async { return surf::get(uri).await; })?; - + let reader = response.take_body().into_reader(); - + Ok(RemoteSource { reader, media_buffer: Vec::new(), @@ -433,16 +491,16 @@ impl std::io::Read for RemoteSource { Ok(_) => { self.media_buffer.extend_from_slice(&buffer); return Ok(()); - }, + } Err(err) => return Err(err), } }); match read_bytes { Err(err) => return Err(err), - _ => {}, + _ => {} } } - // Reads bytes from the media buffer into the buffer given by + // Reads bytes from the media buffer into the buffer given by let mut bytes_read = 0; for location in 0..1024 { if (location + self.offset as usize) < self.media_buffer.len() { @@ -450,7 +508,7 @@ impl std::io::Read for RemoteSource { bytes_read += 1; } } - + self.offset += bytes_read; return Ok(bytes_read as usize); } @@ -463,13 +521,13 @@ impl std::io::Seek for RemoteSource { match pos { // Offset is set to given position SeekFrom::Start(pos) => { - if pos > self.media_buffer.len() as u64{ + if pos > self.media_buffer.len() as u64 { self.offset = self.media_buffer.len() as u64; } else { self.offset = pos; } return Ok(self.offset); - }, + } // Offset is set to length of buffer + given position SeekFrom::End(pos) => { if self.media_buffer.len() as u64 + pos as u64 > self.media_buffer.len() as u64 { @@ -478,16 +536,16 @@ impl std::io::Seek for RemoteSource { self.offset = self.media_buffer.len() as u64 + pos as u64; } return Ok(self.offset); - }, + } // Offset is set to current offset + given position SeekFrom::Current(pos) => { - if self.offset + pos as u64 > self.media_buffer.len() as u64{ + if self.offset + pos as u64 > self.media_buffer.len() as u64 { self.offset = self.media_buffer.len() as u64; } else { self.offset += pos as u64 } return Ok(self.offset); - }, + } } } } @@ -496,7 +554,7 @@ impl MediaSource for RemoteSource { fn is_seekable(&self) -> bool { return true; } - + fn byte_len(&self) -> Option { return None; } diff --git a/src/music_player/music_resampler.rs b/src/music_player/music_resampler.rs index f654a17..4040835 100644 --- a/src/music_player/music_resampler.rs +++ b/src/music_player/music_resampler.rs @@ -48,7 +48,8 @@ where // Interleave the planar samples from Rubato. let num_channels = self.output.len(); - self.interleaved.resize(num_channels * self.output[0].len(), T::MID); + self.interleaved + .resize(num_channels * self.output[0].len(), T::MID); for (i, frame) in self.interleaved.chunks_exact_mut(num_channels).enumerate() { for (ch, s) in frame.iter_mut().enumerate() { @@ -81,7 +82,13 @@ where let input = vec![Vec::with_capacity(duration); num_channels]; - Self { resampler, input, output, duration, interleaved: Default::default() } + Self { + resampler, + input, + output, + duration, + interleaved: Default::default(), + } } /// Resamples a planar/non-interleaved input. @@ -144,4 +151,4 @@ where let src = input.chan(c); dst.extend(src.iter().map(|&s| s.into_sample())); } -} \ No newline at end of file +} diff --git a/src/music_processor/music_processor.rs b/src/music_processor/music_processor.rs index e6d6608..64f4eaa 100644 --- a/src/music_processor/music_processor.rs +++ b/src/music_processor/music_processor.rs @@ -1,6 +1,6 @@ use std::fmt::Debug; -use symphonia::core::audio::{AudioBuffer, AudioBufferRef, Signal, AsAudioBufferRef, SignalSpec}; +use symphonia::core::audio::{AsAudioBufferRef, AudioBuffer, AudioBufferRef, Signal, SignalSpec}; #[derive(Clone)] pub struct MusicProcessor { @@ -10,7 +10,9 @@ pub struct MusicProcessor { impl Debug for MusicProcessor { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("MusicProcessor").field("audio_volume", &self.audio_volume).finish() + f.debug_struct("MusicProcessor") + .field("audio_volume", &self.audio_volume) + .finish() } } @@ -22,22 +24,22 @@ impl MusicProcessor { audio_volume: 1.0, } } - + /// Processes audio samples - /// + /// /// Currently only supports transformations of volume pub fn process(&mut self, audio_buffer_ref: &AudioBufferRef) -> AudioBufferRef { audio_buffer_ref.convert(&mut self.audio_buffer); - + let process = |sample| sample * self.audio_volume; - + self.audio_buffer.transform(process); - + return self.audio_buffer.as_audio_buffer_ref(); } - + /// Sets buffer of the MusicProcessor pub fn set_buffer(&mut self, duration: u64, spec: SignalSpec) { self.audio_buffer = AudioBuffer::new(duration, spec); } -} \ No newline at end of file +} diff --git a/src/music_storage/music_db.rs b/src/music_storage/music_db.rs index e59f51c..b6457d3 100644 --- a/src/music_storage/music_db.rs +++ b/src/music_storage/music_db.rs @@ -362,7 +362,7 @@ impl MusicLibrary { Ok(_) => { //println!("{:?}", target_file.path()); total += 1 - }, + } Err(_error) => { //println!("{}, {:?}: {}", format, target_file.file_name(), _error) } // TODO: Handle more of these errors @@ -481,8 +481,14 @@ impl MusicLibrary { let cue_data = CD::parse_file(cuesheet.to_owned()).unwrap(); // Get album level information - let album_title = &cue_data.get_cdtext().read(cue::cd_text::PTI::Title).unwrap_or(String::new()); - let album_artist = &cue_data.get_cdtext().read(cue::cd_text::PTI::Performer).unwrap_or(String::new()); + let album_title = &cue_data + .get_cdtext() + .read(cue::cd_text::PTI::Title) + .unwrap_or(String::new()); + let album_artist = &cue_data + .get_cdtext() + .read(cue::cd_text::PTI::Performer) + .unwrap_or(String::new()); let parent_dir = cuesheet.parent().expect("The file has no parent path??"); for (i, track) in cue_data.tracks().iter().enumerate() { @@ -498,17 +504,17 @@ impl MusicLibrary { // Get the track timing information let pregap = match track.get_zero_pre() { Some(pregap) => Duration::from_micros((pregap as f32 * 13333.333333) as u64), - None => Duration::from_secs(0) + None => Duration::from_secs(0), }; let postgap = match track.get_zero_post() { Some(postgap) => Duration::from_micros((postgap as f32 * 13333.333333) as u64), - None => Duration::from_secs(0) + None => Duration::from_secs(0), }; let mut start = Duration::from_micros((track.get_start() as f32 * 13333.333333) as u64); start -= pregap; let duration = match track.get_length() { - Some(len) => Duration::from_micros((len as f32 * 13333.333333) as u64), + Some(len) => Duration::from_micros((len as f32 * 13333.333333) as u64), None => { let tagged_file = match lofty::read_from_path(&audio_location) { Ok(tagged_file) => tagged_file, @@ -537,33 +543,31 @@ impl MusicLibrary { tags.push((Tag::Key("AlbumArtist".to_string()), album_artist.clone())); match track.get_cdtext().read(cue::cd_text::PTI::Title) { Some(title) => tags.push((Tag::Title, title)), - None => { - match track.get_cdtext().read(cue::cd_text::PTI::UPC_ISRC) { - Some(title) => tags.push((Tag::Title, title)), - None => { - let namestr = format!("{} - {}", i, track.get_filename()); - tags.push((Tag::Title, namestr)) - } + None => match track.get_cdtext().read(cue::cd_text::PTI::UPC_ISRC) { + Some(title) => tags.push((Tag::Title, title)), + None => { + let namestr = format!("{} - {}", i, track.get_filename()); + tags.push((Tag::Title, namestr)) } - } + }, }; match track.get_cdtext().read(cue::cd_text::PTI::Performer) { Some(artist) => tags.push((Tag::Artist, artist)), - None => () + None => (), }; match track.get_cdtext().read(cue::cd_text::PTI::Genre) { Some(genre) => tags.push((Tag::Genre, genre)), - None => () + None => (), }; match track.get_cdtext().read(cue::cd_text::PTI::Message) { Some(comment) => tags.push((Tag::Comment, comment)), - None => () + None => (), }; let album_art = Vec::new(); let new_song = Song { - location: URI::Cue{ + location: URI::Cue { location: audio_location, start, end, @@ -586,8 +590,8 @@ impl MusicLibrary { Ok(_) => tracks_added += 1, Err(_error) => { //println!("{}", _error); - continue - }, + continue; + } }; } @@ -604,8 +608,8 @@ impl MusicLibrary { match new_song.location { URI::Local(_) if self.query_path(&new_song.location.path()).is_some() => { return Err(format!("Location exists for {:?}", new_song.location).into()) - }, - _ => () + } + _ => (), } self.library.push(new_song); @@ -652,7 +656,7 @@ impl MusicLibrary { for tag in target_tags { let track_result = match track.get_tag(&tag) { Some(value) => value, - None => continue + None => continue, }; if normalize(track_result).contains(&normalize(&query_string)) { diff --git a/src/music_storage/playlist.rs b/src/music_storage/playlist.rs index e61b405..797d0d2 100644 --- a/src/music_storage/playlist.rs +++ b/src/music_storage/playlist.rs @@ -1,10 +1,6 @@ -use std::path::Path; use crate::music_controller::config::Config; +use std::path::Path; -pub fn playlist_add( - config: &Config, - playlist_name: &str, - song_paths: &Vec<&Path> -) { +pub fn playlist_add(config: &Config, playlist_name: &str, song_paths: &Vec<&Path>) { unimplemented!() } diff --git a/src/music_tracker/music_tracker.rs b/src/music_tracker/music_tracker.rs index 12e57e8..9ffd73d 100644 --- a/src/music_tracker/music_tracker.rs +++ b/src/music_tracker/music_tracker.rs @@ -1,11 +1,11 @@ -use std::time::{SystemTime, UNIX_EPOCH}; -use std::collections::BTreeMap; use serde_json::json; +use std::collections::BTreeMap; +use std::time::{SystemTime, UNIX_EPOCH}; use async_trait::async_trait; -use serde::{Serialize, Deserialize}; -use md5::{Md5, Digest}; -use discord_presence::{Event}; +use discord_presence::Event; +use md5::{Digest, Md5}; +use serde::{Deserialize, Serialize}; use surf::StatusCode; use crate::music_storage::music_db::{Song, Tag}; @@ -14,13 +14,13 @@ use crate::music_storage::music_db::{Song, Tag}; pub trait MusicTracker { /// Adds one listen to a song halfway through playback async fn track_song(&mut self, song: Song) -> Result<(), TrackerError>; - + /// Adds a 'listening' status to the music tracker service of choice async fn track_now(&mut self, song: Song) -> Result<(), TrackerError>; - + /// Reads config files, and attempts authentication with service async fn test_tracker(&mut self) -> Result<(), TrackerError>; - + /// Returns plays for a given song according to tracker service async fn get_times_tracked(&mut self, song: &Song) -> Result; } @@ -50,7 +50,7 @@ impl TrackerError { StatusCode::ServiceUnavailable => TrackerError::ServiceUnavailable, StatusCode::NotFound => TrackerError::ServiceUnavailable, _ => TrackerError::Unknown, - } + }; } } @@ -63,62 +63,66 @@ pub struct LastFMConfig { } pub struct LastFM { - config: LastFMConfig + config: LastFMConfig, } #[async_trait] impl MusicTracker for LastFM { async fn track_song(&mut self, song: Song) -> Result<(), TrackerError> { let mut params: BTreeMap<&str, &str> = BTreeMap::new(); - + // Sets timestamp of song beginning play time - let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).expect("Your time is off.").as_secs() - 30; + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Your time is off.") + .as_secs() + - 30; let string_timestamp = timestamp.to_string(); - + let (artist, track) = match (song.get_tag(&Tag::Artist), song.get_tag(&Tag::Title)) { (Some(artist), Some(track)) => (artist, track), - _ => return Err(TrackerError::InvalidSong) + _ => return Err(TrackerError::InvalidSong), }; - + params.insert("method", "track.scrobble"); params.insert("artist", &artist); params.insert("track", &track); params.insert("timestamp", &string_timestamp); - + return match self.api_request(params).await { Ok(_) => Ok(()), Err(err) => Err(TrackerError::from_surf_error(err)), - } + }; } - + async fn track_now(&mut self, song: Song) -> Result<(), TrackerError> { let mut params: BTreeMap<&str, &str> = BTreeMap::new(); - + let (artist, track) = match (song.get_tag(&Tag::Artist), song.get_tag(&Tag::Title)) { (Some(artist), Some(track)) => (artist, track), - _ => return Err(TrackerError::InvalidSong) + _ => return Err(TrackerError::InvalidSong), }; - + params.insert("method", "track.updateNowPlaying"); params.insert("artist", &artist); params.insert("track", &track); - + return match self.api_request(params).await { Ok(_) => Ok(()), Err(err) => Err(TrackerError::from_surf_error(err)), - } + }; } - + async fn test_tracker(&mut self) -> Result<(), TrackerError> { let mut params: BTreeMap<&str, &str> = BTreeMap::new(); params.insert("method", "chart.getTopArtists"); - + return match self.api_request(params).await { Ok(_) => Ok(()), Err(err) => Err(TrackerError::from_surf_error(err)), - } + }; } - + async fn get_times_tracked(&mut self, song: &Song) -> Result { todo!(); } @@ -126,7 +130,7 @@ impl MusicTracker for LastFM { #[derive(Deserialize, Serialize)] struct AuthToken { - token: String + token: String, } #[derive(Deserialize, Serialize, Debug)] @@ -138,72 +142,85 @@ struct SessionResponse { #[derive(Deserialize, Serialize, Debug)] struct Session { - session: SessionResponse + session: SessionResponse, } impl LastFM { /// Returns a url to be approved by the user along with the auth token pub async fn get_auth(api_key: &String) -> Result { let method = String::from("auth.gettoken"); - let api_request_url = format!("http://ws.audioscrobbler.com/2.0/?method={method}&api_key={api_key}&format=json"); - + let api_request_url = format!( + "http://ws.audioscrobbler.com/2.0/?method={method}&api_key={api_key}&format=json" + ); + let auth_token: AuthToken = surf::get(api_request_url).await?.body_json().await?; - - let auth_url = format!("http://www.last.fm/api/auth/?api_key={api_key}&token={}", auth_token.token); - + + let auth_url = format!( + "http://www.last.fm/api/auth/?api_key={api_key}&token={}", + auth_token.token + ); + return Ok(auth_url); } - + /// Returns a LastFM session key - pub async fn get_session_key(api_key: &String, shared_secret: &String, auth_token: &String) -> Result { + pub async fn get_session_key( + api_key: &String, + shared_secret: &String, + auth_token: &String, + ) -> Result { let method = String::from("auth.getSession"); // Creates api_sig as defined in last.fm documentation - let api_sig = format!("api_key{api_key}methodauth.getSessiontoken{auth_token}{shared_secret}"); + let api_sig = + format!("api_key{api_key}methodauth.getSessiontoken{auth_token}{shared_secret}"); // Creates insecure MD5 hash for last.fm api sig let mut hasher = Md5::new(); hasher.update(api_sig); let hash_result = hasher.finalize(); let hex_string_hash = format!("{:#02x}", hash_result); - + let api_request_url = format!("http://ws.audioscrobbler.com/2.0/?method={method}&api_key={api_key}&token={auth_token}&api_sig={hex_string_hash}&format=json"); - + let response = surf::get(api_request_url).recv_string().await?; - + // Sets session key from received response let session_response: Session = serde_json::from_str(&response)?; return Ok(session_response.session.key); } - + /// Creates a new LastFM struct with a given config pub fn new(config: &LastFMConfig) -> LastFM { let last_fm = LastFM { - config: config.clone() + config: config.clone(), }; return last_fm; } - + // Creates an api request with the given parameters - pub async fn api_request(&self, mut params: BTreeMap<&str, &str>) -> Result { + pub async fn api_request( + &self, + mut params: BTreeMap<&str, &str>, + ) -> Result { params.insert("api_key", &self.config.dango_api_key); params.insert("sk", &self.config.session_key); - + // Creates and sets api call signature let api_sig = LastFM::request_sig(¶ms, &self.config.shared_secret); params.insert("api_sig", &api_sig); let mut string_params = String::from(""); - + // Creates method call string // Just iterate over values??? for key in params.keys() { let param_value = params.get(key).unwrap(); string_params.push_str(&format!("{key}={param_value}&")); } - + string_params.pop(); - + let url = "http://ws.audioscrobbler.com/2.0/"; - + let response = surf::post(url).body_string(string_params).await; return response; @@ -218,16 +235,16 @@ impl LastFM { sig_string.push_str(&format!("{key}{}", param_value.unwrap())); } sig_string.push_str(shared_secret); - + // Hashes signature using **INSECURE** MD5 (Required by last.fm api) let mut md5_hasher = Md5::new(); md5_hasher.update(sig_string); let hash_result = md5_hasher.finalize(); let hashed_sig = format!("{:#02x}", hash_result); - + return hashed_sig; } - + // Removes last.fm account from dango-music-player pub fn reset_account() { todo!(); @@ -237,13 +254,13 @@ impl LastFM { #[derive(Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct DiscordRPCConfig { pub enabled: bool, - pub dango_client_id: u64, + pub dango_client_id: u64, pub dango_icon: String, } pub struct DiscordRPC { config: DiscordRPCConfig, - pub client: discord_presence::client::Client + pub client: discord_presence::client::Client, } impl DiscordRPC { @@ -256,7 +273,6 @@ impl DiscordRPC { } } - #[async_trait] impl MusicTracker for DiscordRPC { async fn track_now(&mut self, song: Song) -> Result<(), TrackerError> { @@ -275,16 +291,19 @@ impl MusicTracker for DiscordRPC { } else { &unknown }; - + let _client_thread = self.client.start(); - + // Blocks thread execution until it has connected to local discord client let ready = self.client.block_until_event(Event::Ready); if ready.is_err() { return Err(TrackerError::ServiceUnavailable); } - - let start_time = std::time::SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_millis() as u64; + + let start_time = std::time::SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_millis() as u64; // Sets discord account activity to current playing song let send_activity = self.client.set_activity(|activity| { activity @@ -293,21 +312,21 @@ impl MusicTracker for DiscordRPC { .assets(|assets| assets.large_image(&self.config.dango_icon)) .timestamps(|time| time.start(start_time)) }); - + match send_activity { Ok(_) => return Ok(()), Err(_) => return Err(TrackerError::ServiceUnavailable), } } - + async fn track_song(&mut self, song: Song) -> Result<(), TrackerError> { - return Ok(()) + return Ok(()); } - + async fn test_tracker(&mut self) -> Result<(), TrackerError> { - return Ok(()) + return Ok(()); } - + async fn get_times_tracked(&mut self, song: &Song) -> Result { return Ok(0); } @@ -329,7 +348,7 @@ impl MusicTracker for ListenBrainz { async fn track_now(&mut self, song: Song) -> Result<(), TrackerError> { let (artist, track) = match (song.get_tag(&Tag::Artist), song.get_tag(&Tag::Title)) { (Some(artist), Some(track)) => (artist, track), - _ => return Err(TrackerError::InvalidSong) + _ => return Err(TrackerError::InvalidSong), }; // Creates a json to submit a single song as defined in the listenbrainz documentation let json_req = json!({ @@ -343,21 +362,28 @@ impl MusicTracker for ListenBrainz { } ] }); - - return match self.api_request(&json_req.to_string(), &String::from("/1/submit-listens")).await { + + return match self + .api_request(&json_req.to_string(), &String::from("/1/submit-listens")) + .await + { Ok(_) => Ok(()), - Err(err) => Err(TrackerError::from_surf_error(err)) - } + Err(err) => Err(TrackerError::from_surf_error(err)), + }; } - + async fn track_song(&mut self, song: Song) -> Result<(), TrackerError> { - let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).expect("Your time is off.").as_secs() - 30; - + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Your time is off.") + .as_secs() + - 30; + let (artist, track) = match (song.get_tag(&Tag::Artist), song.get_tag(&Tag::Title)) { (Some(artist), Some(track)) => (artist, track), - _ => return Err(TrackerError::InvalidSong) + _ => return Err(TrackerError::InvalidSong), }; - + let json_req = json!({ "listen_type": "single", "payload": [ @@ -370,11 +396,14 @@ impl MusicTracker for ListenBrainz { } ] }); - - return match self.api_request(&json_req.to_string(), &String::from("/1/submit-listens")).await { + + return match self + .api_request(&json_req.to_string(), &String::from("/1/submit-listens")) + .await + { Ok(_) => Ok(()), - Err(err) => Err(TrackerError::from_surf_error(err)) - } + Err(err) => Err(TrackerError::from_surf_error(err)), + }; } async fn test_tracker(&mut self) -> Result<(), TrackerError> { todo!() @@ -387,12 +416,19 @@ impl MusicTracker for ListenBrainz { impl ListenBrainz { pub fn new(config: &ListenBrainzConfig) -> Self { ListenBrainz { - config: config.clone() + config: config.clone(), } } // Makes an api request to configured url with given json - pub async fn api_request(&self, request: &String, endpoint: &String) -> Result { - let reponse = surf::post(format!("{}{}", &self.config.api_url, endpoint)).body_string(request.clone()).header("Authorization", format!("Token {}", self.config.auth_token)).await; - return reponse + pub async fn api_request( + &self, + request: &String, + endpoint: &String, + ) -> Result { + let reponse = surf::post(format!("{}{}", &self.config.api_url, endpoint)) + .body_string(request.clone()) + .header("Authorization", format!("Token {}", self.config.auth_token)) + .await; + return reponse; } } From eeceb27800675913a263c8807b393350ea26992b Mon Sep 17 00:00:00 2001 From: G2-Games Date: Fri, 3 Nov 2023 08:39:19 -0500 Subject: [PATCH 12/20] Added album query function --- Cargo.toml | 2 + src/music_controller/music_controller.rs | 2 +- src/music_storage/music_db.rs | 161 ++++++++++++++++------- 3 files changed, 113 insertions(+), 52 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 69f5abb..8daf129 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,3 +39,5 @@ log = "0.4" pretty_env_logger = "0.4" cue = "2.0.0" jwalk = "0.8.1" +base64 = "0.21.5" +zip = "0.6.6" diff --git a/src/music_controller/music_controller.rs b/src/music_controller/music_controller.rs index 24b8379..cb39ded 100644 --- a/src/music_controller/music_controller.rs +++ b/src/music_controller/music_controller.rs @@ -85,6 +85,6 @@ impl MusicController { sort_by: Vec, ) -> Option> { self.library - .query(query_string, &target_tags, search_location, &sort_by) + .query(query_string, &target_tags, &sort_by) } } diff --git a/src/music_storage/music_db.rs b/src/music_storage/music_db.rs index b6457d3..7d325e3 100644 --- a/src/music_storage/music_db.rs +++ b/src/music_storage/music_db.rs @@ -1,15 +1,16 @@ use file_format::{FileFormat, Kind}; use lofty::{AudioFile, ItemKey, ItemValue, Probe, TagType, TaggedFileExt}; use std::ffi::OsStr; +use std::collections::{HashMap, HashSet}; use std::{error::Error, io::BufReader}; use chrono::{serde::ts_seconds_option, DateTime, Utc}; use std::time::Duration; -//use walkdir::WalkDir; use cue::cd::CD; use jwalk::WalkDir; use bincode::{deserialize_from, serialize_into}; +use base64::{engine::general_purpose, Engine as _}; use serde::{Deserialize, Serialize}; use std::fs; use std::io::BufWriter; @@ -33,6 +34,7 @@ pub enum Tag { Title, Album, Artist, + AlbumArtist, Genre, Comment, Track, @@ -47,6 +49,7 @@ impl ToString for Tag { Self::Title => "TrackTitle".into(), Self::Album => "AlbumTitle".into(), Self::Artist => "TrackArtist".into(), + Self::AlbumArtist => "AlbumArtist".into(), Self::Genre => "Genre".into(), Self::Comment => "Comment".into(), Self::Track => "TrackNumber".into(), @@ -103,6 +106,15 @@ impl Song { match target_field { "location" => Some(self.location.clone().path_string()), "plays" => Some(self.plays.clone().to_string()), + "format" => { + match self.format { + Some(format) => match format.short_name() { + Some(short) => Some(short.to_string()), + None => None + }, + None => None + } + }, _ => None, // Other field types are not yet supported } } @@ -113,6 +125,7 @@ pub enum URI { Local(PathBuf), Cue { location: PathBuf, + index: usize, start: Duration, end: Duration, }, @@ -120,6 +133,17 @@ pub enum URI { } impl URI { + pub fn index(&self) -> Result<&usize, Box> { + match self { + URI::Local(_) => Err("\"Local\" has no stored index".into()), + URI::Remote(_, _) => Err("\"Remote\" has no stored index".into()), + URI::Cue { + index, + .. + } => Ok(index), + } + } + /// Returns the start time of a CUEsheet song, or an /// error if the URI is not a Cue variant pub fn start(&self) -> Result<&Duration, Box> { @@ -127,9 +151,8 @@ impl URI { URI::Local(_) => Err("\"Local\" has no starting time".into()), URI::Remote(_, _) => Err("\"Remote\" has no starting time".into()), URI::Cue { - location: _, start, - end: _, + .. } => Ok(start), } } @@ -141,9 +164,8 @@ impl URI { URI::Local(_) => Err("\"Local\" has no starting time".into()), URI::Remote(_, _) => Err("\"Remote\" has no starting time".into()), URI::Cue { - location: _, - start: _, end, + .. } => Ok(end), } } @@ -154,8 +176,7 @@ impl URI { URI::Local(location) => location, URI::Cue { location, - start: _, - end: _, + .. } => location, URI::Remote(_, location) => location, } @@ -166,8 +187,7 @@ impl URI { URI::Local(location) => location.as_path().to_string_lossy(), URI::Cue { location, - start: _, - end: _, + .. } => location.as_path().to_string_lossy(), URI::Remote(_, location) => location.as_path().to_string_lossy(), }; @@ -182,30 +202,24 @@ pub enum Service { Youtube, } -/* TODO: Rework this entirely #[derive(Debug)] -pub struct Playlist { - title: String, - cover_art: Box, +pub struct Album<'a> { + pub title: &'a String, + pub artist: Option<&'a String>, + pub cover: Option<&'a AlbumArt>, + pub tracks: Vec<&'a Song>, } -#[derive(Debug)] -pub enum MusicObject { - Song(Song), - Album(Playlist), - Playlist(Playlist), -} -*/ - #[derive(Debug)] pub struct MusicLibrary { pub library: Vec, } pub fn normalize(input_string: &String) -> String { - unidecode(input_string) - .to_ascii_lowercase() - .replace(|c: char| !c.is_alphanumeric(), "") + unidecode(input_string).chars().filter_map(|c: char| { + let x = c.to_ascii_lowercase(); + c.is_alphanumeric().then_some(x) + }).collect() } impl MusicLibrary { @@ -218,7 +232,7 @@ impl MusicLibrary { let global_config = &*config.read().unwrap(); let mut library: Vec = Vec::new(); let mut backup_path = global_config.db_path.clone(); - backup_path.set_extension("bkp"); + backup_path.set_extension("tmp"); match global_config.db_path.try_exists() { Ok(true) => { @@ -253,14 +267,12 @@ impl MusicLibrary { Ok(true) => { // The database exists, so rename it to `.bkp` and // write the new database file - let mut backup_name = config.db_path.clone(); - backup_name.set_extension("bkp"); - fs::rename(config.db_path.as_path(), backup_name.as_path())?; - - // TODO: Make this save properly like in config.rs - - let mut writer = BufWriter::new(fs::File::create(config.db_path.to_path_buf())?); + let mut writer_name = config.db_path.clone(); + writer_name.set_extension("tmp"); + let mut writer = BufWriter::new(fs::File::create(writer_name.to_path_buf())?); serialize_into(&mut writer, &self.library)?; + + fs::rename(writer_name.as_path(), config.db_path.clone().as_path())?; } Ok(false) => { // Create the database if it does not exist @@ -273,6 +285,7 @@ impl MusicLibrary { Ok(()) } + /// Returns the library size in number of tracks pub fn size(&self) -> usize { self.library.len() } @@ -314,7 +327,7 @@ impl MusicLibrary { } /// Finds all the music files within a specified folder - pub fn find_all_music( + pub fn scan_folder( &mut self, target_path: &str, config: &Config, @@ -413,9 +426,12 @@ impl MusicLibrary { ItemKey::TrackTitle => Tag::Title, ItemKey::TrackNumber => Tag::Track, ItemKey::TrackArtist => Tag::Artist, + ItemKey::AlbumArtist => Tag::AlbumArtist, ItemKey::Genre => Tag::Genre, ItemKey::Comment => Tag::Comment, ItemKey::AlbumTitle => Tag::Album, + ItemKey::DiscNumber => Tag::Disk, + ItemKey::Unknown(unknown) if unknown == "ACOUSTID_FINGERPRINT" => continue, ItemKey::Unknown(unknown) => Tag::Key(unknown.to_string()), custom => Tag::Key(format!("{:?}", custom)), }; @@ -423,7 +439,7 @@ impl MusicLibrary { let value = match item.value() { ItemValue::Text(value) => String::from(value), ItemValue::Locator(value) => String::from(value), - ItemValue::Binary(_) => String::from(""), + ItemValue::Binary(bin) => format!("BIN#{}", general_purpose::STANDARD.encode(bin)), }; tags.push((key, value)) @@ -541,6 +557,7 @@ impl MusicLibrary { let mut tags: Vec<(Tag, String)> = Vec::new(); tags.push((Tag::Album, album_title.clone())); tags.push((Tag::Key("AlbumArtist".to_string()), album_artist.clone())); + tags.push((Tag::Track, (i + 1).to_string())); match track.get_cdtext().read(cue::cd_text::PTI::Title) { Some(title) => tags.push((Tag::Title, title)), None => match track.get_cdtext().read(cue::cd_text::PTI::UPC_ISRC) { @@ -569,6 +586,7 @@ impl MusicLibrary { let new_song = Song { location: URI::Cue { location: audio_location, + index: i, start, end, }, @@ -641,39 +659,53 @@ impl MusicLibrary { /// Query the database, returning a list of [Song]s /// - /// The order in which the `sort_by` `Vec` is arranged - /// determines the output sorting + /// The order in which the sort by Vec is arranged + /// determines the output sorting. + /// + /// Example: + /// ``` + /// query( + /// &String::from("query"), + /// &vec![ + /// Tag::Title + /// ], + /// &vec![ + /// Tag::Field("location".to_string()), + /// Tag::Album, + /// Tag::Disk, + /// Tag::Track, + /// ], + /// ) + /// ``` + /// This would find all titles containing the sequence + /// "query", and would return the results sorted first + /// by path, then album, disk number, and finally track number. pub fn query( &self, query_string: &String, // The query itself target_tags: &Vec, // The tags to search - search_location: bool, // Whether to search the location field or not sort_by: &Vec, // Tags to sort the resulting data by ) -> Option> { let songs = Arc::new(Mutex::new(Vec::new())); self.library.par_iter().for_each(|track| { for tag in target_tags { - let track_result = match track.get_tag(&tag) { - Some(value) => value, - None => continue, + let track_result = match tag { + Tag::Field(target) => match track.get_field(&target) { + Some(value) => value, + None => continue, + }, + _ => match track.get_tag(&tag) { + Some(value) => value.to_owned(), + None => continue, + }, }; - if normalize(track_result).contains(&normalize(&query_string)) { + if normalize(&track_result).contains(&normalize(&query_string)) { songs.lock().unwrap().push(track); return; } } - - if !search_location { - return; - } - - // Find a URL in the song - if normalize(&track.location.path_string()).contains(&normalize(&query_string)) { - songs.lock().unwrap().push(track); - return; - } }); let lock = Arc::try_unwrap(songs).expect("Lock still has multiple owners!"); @@ -732,4 +764,31 @@ impl MusicLibrary { None } } + + pub fn albums(&self) -> Result, Box> { + let mut albums: Vec = Vec::new(); + for result in &self.library { + let title = match result.get_tag(&Tag::Album){ + Some(title) => title, + None => continue + }; + + match albums.binary_search_by_key(&normalize(&title), |album| normalize(&album.title.to_owned())) { + Ok(pos) => { + albums[pos].tracks.push(result); + }, + Err(pos) => { + let new_album = Album { + title, + artist: result.get_tag(&Tag::AlbumArtist), + tracks: vec![result], + cover: None, + }; + albums.insert(pos, new_album); + } + } + } + + Ok(albums) + } } From 421ab0c757fb99941738d1528117491269a43aed Mon Sep 17 00:00:00 2001 From: G2-Games Date: Fri, 3 Nov 2023 09:49:53 -0500 Subject: [PATCH 13/20] Added benchmark for testing --- src/music_controller/music_controller.rs | 2 +- src/music_storage/music_db.rs | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/music_controller/music_controller.rs b/src/music_controller/music_controller.rs index cb39ded..9455e58 100644 --- a/src/music_controller/music_controller.rs +++ b/src/music_controller/music_controller.rs @@ -85,6 +85,6 @@ impl MusicController { sort_by: Vec, ) -> Option> { self.library - .query(query_string, &target_tags, &sort_by) + .query_tracks(query_string, &target_tags, &sort_by) } } diff --git a/src/music_storage/music_db.rs b/src/music_storage/music_db.rs index 7d325e3..0e02d49 100644 --- a/src/music_storage/music_db.rs +++ b/src/music_storage/music_db.rs @@ -664,7 +664,7 @@ impl MusicLibrary { /// /// Example: /// ``` - /// query( + /// query_tracks( /// &String::from("query"), /// &vec![ /// Tag::Title @@ -680,7 +680,7 @@ impl MusicLibrary { /// This would find all titles containing the sequence /// "query", and would return the results sorted first /// by path, then album, disk number, and finally track number. - pub fn query( + pub fn query_tracks( &self, query_string: &String, // The query itself target_tags: &Vec, // The tags to search @@ -773,7 +773,7 @@ impl MusicLibrary { None => continue }; - match albums.binary_search_by_key(&normalize(&title), |album| normalize(&album.title.to_owned())) { + match albums.binary_search_by_key(&title, |album| album.title) { Ok(pos) => { albums[pos].tracks.push(result); }, @@ -791,4 +791,5 @@ impl MusicLibrary { Ok(albums) } + } From f890f1a0262d08eff3796db1fb774727b21d3d24 Mon Sep 17 00:00:00 2001 From: G2_Games Date: Fri, 3 Nov 2023 10:30:22 -0500 Subject: [PATCH 14/20] Improved performance by reducing reallocations --- src/music_storage/music_db.rs | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/music_storage/music_db.rs b/src/music_storage/music_db.rs index 0e02d49..a115e53 100644 --- a/src/music_storage/music_db.rs +++ b/src/music_storage/music_db.rs @@ -215,11 +215,8 @@ pub struct MusicLibrary { pub library: Vec, } -pub fn normalize(input_string: &String) -> String { - unidecode(input_string).chars().filter_map(|c: char| { - let x = c.to_ascii_lowercase(); - c.is_alphanumeric().then_some(x) - }).collect() +pub fn normalize(input_string: &String) { + unidecode(input_string).retain(|c| !c.is_whitespace()); } impl MusicLibrary { @@ -690,7 +687,7 @@ impl MusicLibrary { self.library.par_iter().for_each(|track| { for tag in target_tags { - let track_result = match tag { + let mut track_result = match tag { Tag::Field(target) => match track.get_field(&target) { Some(value) => value, None => continue, @@ -701,7 +698,10 @@ impl MusicLibrary { }, }; - if normalize(&track_result).contains(&normalize(&query_string)) { + normalize(&mut query_string.to_owned()); + normalize(&mut track_result); + + if track_result.contains(query_string) { songs.lock().unwrap().push(track); return; } @@ -772,8 +772,12 @@ impl MusicLibrary { Some(title) => title, None => continue }; + normalize(title); - match albums.binary_search_by_key(&title, |album| album.title) { + match albums.binary_search_by_key(&title, |album| { + normalize(&album.title); + album.title + }) { Ok(pos) => { albums[pos].tracks.push(result); }, From d330c5f0bda45de6477f6758e553b96534fac21e Mon Sep 17 00:00:00 2001 From: G2_Games Date: Fri, 3 Nov 2023 12:29:04 -0500 Subject: [PATCH 15/20] Switched album list to BTreeMap, improved perf --- src/music_storage/music_db.rs | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/music_storage/music_db.rs b/src/music_storage/music_db.rs index a115e53..5070d97 100644 --- a/src/music_storage/music_db.rs +++ b/src/music_storage/music_db.rs @@ -1,7 +1,7 @@ use file_format::{FileFormat, Kind}; use lofty::{AudioFile, ItemKey, ItemValue, Probe, TagType, TaggedFileExt}; use std::ffi::OsStr; -use std::collections::{HashMap, HashSet}; +use std::collections::{HashMap, HashSet, BTreeMap}; use std::{error::Error, io::BufReader}; use chrono::{serde::ts_seconds_option, DateTime, Utc}; @@ -766,7 +766,7 @@ impl MusicLibrary { } pub fn albums(&self) -> Result, Box> { - let mut albums: Vec = Vec::new(); + let mut albums: BTreeMap<&String, Album> = BTreeMap::new(); for result in &self.library { let title = match result.get_tag(&Tag::Album){ Some(title) => title, @@ -774,26 +774,21 @@ impl MusicLibrary { }; normalize(title); - match albums.binary_search_by_key(&title, |album| { - normalize(&album.title); - album.title - }) { - Ok(pos) => { - albums[pos].tracks.push(result); - }, - Err(pos) => { + match albums.get_mut(&title) { + Some(album) => album.tracks.push(result), + None => { let new_album = Album { title, artist: result.get_tag(&Tag::AlbumArtist), tracks: vec![result], cover: None, }; - albums.insert(pos, new_album); + albums.insert(title, new_album); } } } - Ok(albums) + Ok(albums.into_par_iter().map(|album| album.1).collect()) } } From e878e72a990728a950469ad8c0a1be71b17545cd Mon Sep 17 00:00:00 2001 From: G2-Games Date: Sat, 4 Nov 2023 04:57:20 -0500 Subject: [PATCH 16/20] Implemented album search, compressed on-disk library --- Cargo.toml | 4 +- src/lib.rs | 1 + src/music_storage/music_db.rs | 241 +++++++++++++++++++++------------- src/music_storage/utils.rs | 52 ++++++++ 4 files changed, 208 insertions(+), 90 deletions(-) create mode 100644 src/music_storage/utils.rs diff --git a/Cargo.toml b/Cargo.toml index 8daf129..86ab8a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ rubato = "0.12.0" arrayvec = "0.7.4" discord-presence = "0.5.18" chrono = { version = "0.4.31", features = ["serde"] } -bincode = "1.3.3" +bincode = { version = "2.0.0-rc.3", features = ["serde"] } unidecode = "0.3.0" rayon = "1.8.0" log = "0.4" @@ -41,3 +41,5 @@ cue = "2.0.0" jwalk = "0.8.1" base64 = "0.21.5" zip = "0.6.6" +flate2 = "1.0.28" +snap = "1.1.0" diff --git a/src/lib.rs b/src/lib.rs index 75b98ee..9afe354 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,7 @@ pub mod music_tracker { pub mod music_storage { pub mod music_db; pub mod playlist; + pub mod utils; } pub mod music_processor { diff --git a/src/music_storage/music_db.rs b/src/music_storage/music_db.rs index 5070d97..4e6e226 100644 --- a/src/music_storage/music_db.rs +++ b/src/music_storage/music_db.rs @@ -1,35 +1,39 @@ +// Crate things +use crate::music_controller::config::Config; +use super::utils::{normalize, read_library, write_library}; + +// Various std things +use std::collections::BTreeMap; +use std::error::Error; + +// Files use file_format::{FileFormat, Kind}; +use jwalk::WalkDir; use lofty::{AudioFile, ItemKey, ItemValue, Probe, TagType, TaggedFileExt}; use std::ffi::OsStr; -use std::collections::{HashMap, HashSet, BTreeMap}; -use std::{error::Error, io::BufReader}; +use cue::cd::CD; +use std::fs; +use std::path::{Path, PathBuf}; +// Time use chrono::{serde::ts_seconds_option, DateTime, Utc}; use std::time::Duration; -use cue::cd::CD; -use jwalk::WalkDir; -use bincode::{deserialize_from, serialize_into}; +// Serialization/Compression use base64::{engine::general_purpose, Engine as _}; use serde::{Deserialize, Serialize}; -use std::fs; -use std::io::BufWriter; -use std::path::{Path, PathBuf}; -use unidecode::unidecode; // Fun parallel stuff use rayon::prelude::*; use std::sync::{Arc, Mutex, RwLock}; -use crate::music_controller::config::Config; - #[derive(Debug, Clone, Deserialize, Serialize)] pub struct AlbumArt { pub index: u16, pub path: Option, } -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)] pub enum Tag { Title, Album, @@ -78,7 +82,7 @@ pub struct Song { #[serde(with = "ts_seconds_option")] pub date_modified: Option>, pub album_art: Vec, - pub tags: Vec<(Tag, String)>, + pub tags: BTreeMap, } impl Song { @@ -94,12 +98,7 @@ impl Song { * ``` **/ pub fn get_tag(&self, target_key: &Tag) -> Option<&String> { - let index = self.tags.iter().position(|r| r.0 == *target_key); - - match index { - Some(i) => return Some(&self.tags[i].1), - None => None, - } + self.tags.get(target_key) } pub fn get_field(&self, target_field: &str) -> Option { @@ -202,12 +201,56 @@ pub enum Service { Youtube, } -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct Album<'a> { - pub title: &'a String, - pub artist: Option<&'a String>, - pub cover: Option<&'a AlbumArt>, - pub tracks: Vec<&'a Song>, + title: &'a String, + artist: Option<&'a String>, + cover: Option<&'a AlbumArt>, + discs: BTreeMap>, +} + +impl Album<'_> { + /// Returns the album title + pub fn title(&self) -> &String { + self.title + } + + /// Returns the Album Artist, if they exist + pub fn artist(&self) -> Option<&String> { + self.artist + } + + /// Returns the album cover as an AlbumArt struct, if it exists + pub fn cover(&self) -> Option<&AlbumArt> { + self.cover + } + + pub fn tracks(&self) -> Vec<&Song> { + let mut songs = Vec::new(); + for disc in &self.discs { + songs.append(&mut disc.1.clone()) + } + songs + } + + pub fn discs(&self) -> &BTreeMap> { + &self.discs + } + + /// Returns the specified track at `index` from the album, returning + /// an error if the track index is out of range + pub fn track(&self, disc: usize, index: usize) -> Option<&Song> { + Some(self.discs.get(&disc)?[index]) + } + + /// Returns the number of songs in the album + pub fn len(&self) -> usize { + let mut total = 0; + for disc in &self.discs { + total += disc.1.len(); + } + total + } } #[derive(Debug)] @@ -215,10 +258,6 @@ pub struct MusicLibrary { pub library: Vec, } -pub fn normalize(input_string: &String) { - unidecode(input_string).retain(|c| !c.is_whitespace()); -} - impl MusicLibrary { /// Initialize the database /// @@ -229,29 +268,22 @@ impl MusicLibrary { let global_config = &*config.read().unwrap(); let mut library: Vec = Vec::new(); let mut backup_path = global_config.db_path.clone(); - backup_path.set_extension("tmp"); + backup_path.set_extension("bkp"); - match global_config.db_path.try_exists() { - Ok(true) => { - // The database exists, so get it from the file - let database = fs::File::open(global_config.db_path.to_path_buf())?; - let reader = BufReader::new(database); - library = deserialize_from(reader)?; - } - Ok(false) => { + match global_config.db_path.exists() { + true => { + library = read_library(*global_config.db_path.clone())?; + }, + false => { // 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(global_config.db_path.to_path_buf())?; - let reader = BufReader::new(database); - library = deserialize_from(reader)?; + if backup_path.exists() { + library = read_library(*backup_path.clone())?; + write_library(&library, global_config.db_path.to_path_buf(), false)?; } else { - let mut writer = - BufWriter::new(fs::File::create(global_config.db_path.to_path_buf())?); - serialize_into(&mut writer, &library)?; + write_library(&library, global_config.db_path.to_path_buf(), false)?; } } - Err(error) => return Err(error.into()), }; Ok(Self { library }) @@ -261,20 +293,8 @@ impl MusicLibrary { /// specified in the config pub fn save(&self, config: &Config) -> Result<(), Box> { match config.db_path.try_exists() { - Ok(true) => { - // The database exists, so rename it to `.bkp` and - // write the new database file - let mut writer_name = config.db_path.clone(); - writer_name.set_extension("tmp"); - let mut writer = BufWriter::new(fs::File::create(writer_name.to_path_buf())?); - serialize_into(&mut writer, &self.library)?; - - fs::rename(writer_name.as_path(), config.db_path.clone().as_path())?; - } - Ok(false) => { - // Create the database if it does not exist - let mut writer = BufWriter::new(fs::File::create(config.db_path.to_path_buf())?); - serialize_into(&mut writer, &self.library)?; + Ok(exists) => { + write_library(&self.library, config.db_path.to_path_buf(), exists)?; } Err(error) => return Err(error.into()), } @@ -352,7 +372,7 @@ impl MusicLibrary { // Save periodically while scanning i += 1; - if i % 250 == 0 { + if i % 500 == 0 { self.save(config).unwrap(); } @@ -417,7 +437,7 @@ impl MusicLibrary { }, }; - let mut tags: Vec<(Tag, String)> = Vec::new(); + let mut tags: BTreeMap = BTreeMap::new(); for item in tag.items() { let key = match item.key() { ItemKey::TrackTitle => Tag::Title, @@ -439,7 +459,7 @@ impl MusicLibrary { ItemValue::Binary(bin) => format!("BIN#{}", general_purpose::STANDARD.encode(bin)), }; - tags.push((key, value)) + tags.insert(key, value); } // Get all the album artwork information @@ -551,31 +571,31 @@ impl MusicLibrary { }; // Get some useful tags - let mut tags: Vec<(Tag, String)> = Vec::new(); - tags.push((Tag::Album, album_title.clone())); - tags.push((Tag::Key("AlbumArtist".to_string()), album_artist.clone())); - tags.push((Tag::Track, (i + 1).to_string())); + let mut tags: BTreeMap = BTreeMap::new(); + tags.insert(Tag::Album, album_title.clone()); + tags.insert(Tag::Key("AlbumArtist".to_string()), album_artist.clone()); + tags.insert(Tag::Track, (i + 1).to_string()); match track.get_cdtext().read(cue::cd_text::PTI::Title) { - Some(title) => tags.push((Tag::Title, title)), + Some(title) => tags.insert(Tag::Title, title), None => match track.get_cdtext().read(cue::cd_text::PTI::UPC_ISRC) { - Some(title) => tags.push((Tag::Title, title)), + Some(title) => tags.insert(Tag::Title, title), None => { let namestr = format!("{} - {}", i, track.get_filename()); - tags.push((Tag::Title, namestr)) + tags.insert(Tag::Title, namestr) } }, }; match track.get_cdtext().read(cue::cd_text::PTI::Performer) { - Some(artist) => tags.push((Tag::Artist, artist)), - None => (), + Some(artist) => tags.insert(Tag::Artist, artist), + None => None, }; match track.get_cdtext().read(cue::cd_text::PTI::Genre) { - Some(genre) => tags.push((Tag::Genre, genre)), - None => (), + Some(genre) => tags.insert(Tag::Genre, genre), + None => None, }; match track.get_cdtext().read(cue::cd_text::PTI::Message) { - Some(comment) => tags.push((Tag::Comment, comment)), - None => (), + Some(comment) => tags.insert(Tag::Comment, comment), + None => None, }; let album_art = Vec::new(); @@ -687,21 +707,18 @@ impl MusicLibrary { self.library.par_iter().for_each(|track| { for tag in target_tags { - let mut track_result = match tag { + let track_result = match tag { Tag::Field(target) => match track.get_field(&target) { Some(value) => value, None => continue, }, _ => match track.get_tag(&tag) { - Some(value) => value.to_owned(), + Some(value) => value.clone(), None => continue, }, }; - normalize(&mut query_string.to_owned()); - normalize(&mut track_result); - - if track_result.contains(query_string) { + if normalize(&track_result.to_string()).contains(&normalize(&query_string.to_owned())) { songs.lock().unwrap().push(track); return; } @@ -765,30 +782,76 @@ impl MusicLibrary { } } - pub fn albums(&self) -> Result, Box> { - let mut albums: BTreeMap<&String, Album> = BTreeMap::new(); + /// Generates all albums from the track list + pub fn albums(&self) -> BTreeMap { + let mut albums: BTreeMap = BTreeMap::new(); for result in &self.library { let title = match result.get_tag(&Tag::Album){ Some(title) => title, None => continue }; - normalize(title); + let disc_num = result.get_tag(&Tag::Disk).unwrap_or(&"".to_string()).parse::().unwrap_or(1); - match albums.get_mut(&title) { - Some(album) => album.tracks.push(result), + let norm_title = normalize(title); + match albums.get_mut(&norm_title) { + Some(album) => { + match album.discs.get_mut(&disc_num) { + Some(disc) => disc.push(result), + None => { + album.discs.insert(disc_num, vec![result]); + } + } + }, None => { let new_album = Album { title, artist: result.get_tag(&Tag::AlbumArtist), - tracks: vec![result], + discs: BTreeMap::from([(disc_num, vec![result])]), cover: None, }; - albums.insert(title, new_album); + albums.insert(norm_title, new_album); } } } - Ok(albums.into_par_iter().map(|album| album.1).collect()) + let blank = String::from(""); + albums.par_iter_mut().for_each(|album| { + for disc in &mut album.1.discs { + disc.1.par_sort_by(|a, b| { + if let (Ok(num_a), Ok(num_b)) = ( + a.get_tag(&Tag::Title).unwrap_or(&blank).parse::(), + b.get_tag(&Tag::Title).unwrap_or(&blank).parse::() + ) { + // If parsing succeeds, compare as numbers + match num_a < num_b { + true => return std::cmp::Ordering::Less, + false => return std::cmp::Ordering::Greater + } + } + match a.get_field("location").unwrap() < b.get_field("location").unwrap() { + true => return std::cmp::Ordering::Less, + false => return std::cmp::Ordering::Greater + } + }); + } + }); + albums } + pub fn query_albums(&self, + query_string: &String, // The query itself + ) -> Result, Box> { + let all_albums = self.albums(); + + let normalized_query = normalize(query_string); + let albums: Vec = all_albums.par_iter().filter_map(|album| + if normalize(album.0).contains(&normalized_query) { + Some(album.1.clone()) + } else { + None + } + ).collect(); + + Ok(albums) + } } diff --git a/src/music_storage/utils.rs b/src/music_storage/utils.rs new file mode 100644 index 0000000..1a6d8a4 --- /dev/null +++ b/src/music_storage/utils.rs @@ -0,0 +1,52 @@ +use std::io::{BufReader, BufWriter}; +use std::{path::PathBuf, error::Error, fs}; +use flate2::Compression; +use flate2::write::ZlibEncoder; +use flate2::read::ZlibDecoder; + +use snap; + +use unidecode::unidecode; +use crate::music_storage::music_db::Song; + +pub fn normalize(input_string: &String) -> String { + let mut normalized = unidecode(input_string); + + // Remove non alphanumeric characters + normalized.retain(|c| c.is_alphabetic()); + normalized = normalized.to_ascii_lowercase(); + + normalized +} + +pub fn read_library(path: PathBuf) -> Result, Box> { + let database = fs::File::open(path)?; + let reader = BufReader::new(database); + //let mut d = ZlibDecoder::new(reader); + + let mut d = snap::read::FrameDecoder::new(reader); + + let library: Vec = bincode::serde::decode_from_std_read(&mut d, bincode::config::standard().with_little_endian().with_variable_int_encoding())?; + Ok(library) +} + +pub fn write_library(library: &Vec, path: PathBuf, take_backup: bool) -> Result<(), Box> { + let mut writer_name = path.clone(); + writer_name.set_extension("tmp"); + let mut backup_name = path.clone(); + backup_name.set_extension("bkp"); + + let writer = BufWriter::new(fs::File::create(writer_name.to_path_buf())?); + //let mut e = ZlibEncoder::new(writer, Compression::default()); + + let mut e = snap::write::FrameEncoder::new(writer); + + bincode::serde::encode_into_std_write(&library, &mut e, bincode::config::standard().with_little_endian().with_variable_int_encoding())?; + + if path.exists() && take_backup { + fs::rename(&path, backup_name)?; + } + fs::rename(writer_name, &path)?; + + Ok(()) +} From 37b393246ff3db83c711b8fe13d99b0336314d3f Mon Sep 17 00:00:00 2001 From: G2-Games Date: Sat, 4 Nov 2023 18:40:05 -0500 Subject: [PATCH 17/20] Fixed some search issues --- src/music_storage/music_db.rs | 68 +++++++++++++++++------------------ src/music_storage/utils.rs | 18 +++++----- 2 files changed, 41 insertions(+), 45 deletions(-) diff --git a/src/music_storage/music_db.rs b/src/music_storage/music_db.rs index 4e6e226..e79eae8 100644 --- a/src/music_storage/music_db.rs +++ b/src/music_storage/music_db.rs @@ -730,49 +730,43 @@ impl MusicLibrary { // Sort the returned list of songs new_songs.par_sort_by(|a, b| { - for opt in sort_by { - let tag_a = match opt { + for sort_option in sort_by { + let tag_a = match sort_option { Tag::Field(field_selection) => match a.get_field(field_selection) { Some(field_value) => field_value, None => continue, }, - _ => match a.get_tag(&opt) { + _ => match a.get_tag(&sort_option) { Some(tag_value) => tag_value.to_owned(), None => continue, }, }; - let tag_b = match opt { + let tag_b = match sort_option { Tag::Field(field_selection) => match b.get_field(field_selection) { Some(field_value) => field_value, None => continue, }, - _ => match b.get_tag(&opt) { + _ => match b.get_tag(&sort_option) { Some(tag_value) => tag_value.to_owned(), None => continue, }, }; - // Try to parse the tags as f64 if let (Ok(num_a), Ok(num_b)) = (tag_a.parse::(), tag_b.parse::()) { // If parsing succeeds, compare as numbers - if num_a < num_b { - return std::cmp::Ordering::Less; - } else if num_a > num_b { - return std::cmp::Ordering::Greater; - } + return num_a.cmp(&num_b); } else { // If parsing fails, compare as strings - if tag_a < tag_b { - return std::cmp::Ordering::Less; - } else if tag_a > tag_b { - return std::cmp::Ordering::Greater; - } + return tag_a.cmp(&tag_b); } } // If all tags are equal, sort by Track number - a.get_tag(&Tag::Track).cmp(&b.get_tag(&Tag::Track)) + let path_a = PathBuf::from(a.get_field("location").unwrap()); + let path_b = PathBuf::from(b.get_field("location").unwrap()); + + path_a.file_name().cmp(&path_b.file_name()) }); if new_songs.len() > 0 { @@ -786,22 +780,22 @@ impl MusicLibrary { pub fn albums(&self) -> BTreeMap { let mut albums: BTreeMap = BTreeMap::new(); for result in &self.library { - let title = match result.get_tag(&Tag::Album){ + let title = match result.get_tag(&Tag::Album) { Some(title) => title, None => continue }; - let disc_num = result.get_tag(&Tag::Disk).unwrap_or(&"".to_string()).parse::().unwrap_or(1); - let norm_title = normalize(title); + + let disc_num = result.get_tag(&Tag::Disk).unwrap_or(&"".to_string()).parse::().unwrap_or(1); match albums.get_mut(&norm_title) { + // If the album is in the list, add the track to the appropriate disc in it Some(album) => { match album.discs.get_mut(&disc_num) { Some(disc) => disc.push(result), - None => { - album.discs.insert(disc_num, vec![result]); - } + None => {album.discs.insert(disc_num, vec![result]);} } }, + // If the album is not in the list, make a new one and add it None => { let new_album = Album { title, @@ -814,27 +808,29 @@ impl MusicLibrary { } } + // Sort the tracks in each disk in each album let blank = String::from(""); albums.par_iter_mut().for_each(|album| { for disc in &mut album.1.discs { disc.1.par_sort_by(|a, b| { - if let (Ok(num_a), Ok(num_b)) = ( - a.get_tag(&Tag::Title).unwrap_or(&blank).parse::(), - b.get_tag(&Tag::Title).unwrap_or(&blank).parse::() - ) { - // If parsing succeeds, compare as numbers - match num_a < num_b { - true => return std::cmp::Ordering::Less, - false => return std::cmp::Ordering::Greater - } - } - match a.get_field("location").unwrap() < b.get_field("location").unwrap() { - true => return std::cmp::Ordering::Less, - false => return std::cmp::Ordering::Greater + let a_track = a.get_tag(&Tag::Track).unwrap_or(&blank); + let b_track = b.get_tag(&Tag::Track).unwrap_or(&blank); + + if let (Ok(num_a), Ok(num_b)) = (a_track.parse::(), b_track.parse::()) { + // If parsing the track numbers succeeds, compare as numbers + num_a.cmp(&num_b) + } else { + // If parsing doesn't succeed, compare the locations + let path_a = PathBuf::from(a.get_field("location").unwrap()); + let path_b = PathBuf::from(b.get_field("location").unwrap()); + + path_a.file_name().cmp(&path_b.file_name()) } }); } }); + + // Return the albums! albums } diff --git a/src/music_storage/utils.rs b/src/music_storage/utils.rs index 1a6d8a4..59cb4d8 100644 --- a/src/music_storage/utils.rs +++ b/src/music_storage/utils.rs @@ -1,8 +1,5 @@ use std::io::{BufReader, BufWriter}; use std::{path::PathBuf, error::Error, fs}; -use flate2::Compression; -use flate2::write::ZlibEncoder; -use flate2::read::ZlibDecoder; use snap; @@ -10,37 +7,40 @@ use unidecode::unidecode; use crate::music_storage::music_db::Song; pub fn normalize(input_string: &String) -> String { + // Normalize the unicode and convert everything to lowercase let mut normalized = unidecode(input_string); // Remove non alphanumeric characters - normalized.retain(|c| c.is_alphabetic()); - normalized = normalized.to_ascii_lowercase(); + normalized.retain(|c| c.is_alphanumeric()); normalized } pub fn read_library(path: PathBuf) -> Result, Box> { + + // Create a new snap reader over the database file let database = fs::File::open(path)?; let reader = BufReader::new(database); - //let mut d = ZlibDecoder::new(reader); - let mut d = snap::read::FrameDecoder::new(reader); + // Decode the library from the serialized data into the vec let library: Vec = bincode::serde::decode_from_std_read(&mut d, bincode::config::standard().with_little_endian().with_variable_int_encoding())?; Ok(library) } pub fn write_library(library: &Vec, path: PathBuf, take_backup: bool) -> Result<(), Box> { + + // Create 2 new names for the file, a temporary one for writing out, and a backup let mut writer_name = path.clone(); writer_name.set_extension("tmp"); let mut backup_name = path.clone(); backup_name.set_extension("bkp"); + // Create a new BufWriter on the file and make a snap frame encoer for it too let writer = BufWriter::new(fs::File::create(writer_name.to_path_buf())?); - //let mut e = ZlibEncoder::new(writer, Compression::default()); - let mut e = snap::write::FrameEncoder::new(writer); + // Write out the data using bincode bincode::serde::encode_into_std_write(&library, &mut e, bincode::config::standard().with_little_endian().with_variable_int_encoding())?; if path.exists() && take_backup { From 7ddc829dacf672ae67463c47d1fbc05641be0489 Mon Sep 17 00:00:00 2001 From: G2-Games Date: Sat, 4 Nov 2023 18:42:21 -0500 Subject: [PATCH 18/20] `cargo fmt` --- src/music_storage/music_db.rs | 90 ++++++++++++++++------------------- src/music_storage/utils.rs | 27 ++++++++--- 2 files changed, 62 insertions(+), 55 deletions(-) diff --git a/src/music_storage/music_db.rs b/src/music_storage/music_db.rs index e79eae8..c6a59be 100644 --- a/src/music_storage/music_db.rs +++ b/src/music_storage/music_db.rs @@ -1,17 +1,17 @@ // Crate things -use crate::music_controller::config::Config; use super::utils::{normalize, read_library, write_library}; +use crate::music_controller::config::Config; // Various std things use std::collections::BTreeMap; use std::error::Error; // Files +use cue::cd::CD; use file_format::{FileFormat, Kind}; use jwalk::WalkDir; use lofty::{AudioFile, ItemKey, ItemValue, Probe, TagType, TaggedFileExt}; use std::ffi::OsStr; -use cue::cd::CD; use std::fs; use std::path::{Path, PathBuf}; @@ -105,14 +105,12 @@ impl Song { match target_field { "location" => Some(self.location.clone().path_string()), "plays" => Some(self.plays.clone().to_string()), - "format" => { - match self.format { - Some(format) => match format.short_name() { - Some(short) => Some(short.to_string()), - None => None - }, - None => None - } + "format" => match self.format { + Some(format) => match format.short_name() { + Some(short) => Some(short.to_string()), + None => None, + }, + None => None, }, _ => None, // Other field types are not yet supported } @@ -136,10 +134,7 @@ impl URI { match self { URI::Local(_) => Err("\"Local\" has no stored index".into()), URI::Remote(_, _) => Err("\"Remote\" has no stored index".into()), - URI::Cue { - index, - .. - } => Ok(index), + URI::Cue { index, .. } => Ok(index), } } @@ -149,10 +144,7 @@ impl URI { match self { URI::Local(_) => Err("\"Local\" has no starting time".into()), URI::Remote(_, _) => Err("\"Remote\" has no starting time".into()), - URI::Cue { - start, - .. - } => Ok(start), + URI::Cue { start, .. } => Ok(start), } } @@ -162,10 +154,7 @@ impl URI { match self { URI::Local(_) => Err("\"Local\" has no starting time".into()), URI::Remote(_, _) => Err("\"Remote\" has no starting time".into()), - URI::Cue { - end, - .. - } => Ok(end), + URI::Cue { end, .. } => Ok(end), } } @@ -173,10 +162,7 @@ impl URI { pub fn path(&self) -> &PathBuf { match self { URI::Local(location) => location, - URI::Cue { - location, - .. - } => location, + URI::Cue { location, .. } => location, URI::Remote(_, location) => location, } } @@ -184,10 +170,7 @@ impl URI { pub fn path_string(&self) -> String { let path_str = match self { URI::Local(location) => location.as_path().to_string_lossy(), - URI::Cue { - location, - .. - } => location.as_path().to_string_lossy(), + URI::Cue { location, .. } => location.as_path().to_string_lossy(), URI::Remote(_, location) => location.as_path().to_string_lossy(), }; path_str.to_string() @@ -273,7 +256,7 @@ impl MusicLibrary { match global_config.db_path.exists() { true => { library = read_library(*global_config.db_path.clone())?; - }, + } false => { // Create the database if it does not exist // possibly from the backup file @@ -718,7 +701,9 @@ impl MusicLibrary { }, }; - if normalize(&track_result.to_string()).contains(&normalize(&query_string.to_owned())) { + if normalize(&track_result.to_string()) + .contains(&normalize(&query_string.to_owned())) + { songs.lock().unwrap().push(track); return; } @@ -782,17 +767,21 @@ impl MusicLibrary { for result in &self.library { let title = match result.get_tag(&Tag::Album) { Some(title) => title, - None => continue + None => continue, }; let norm_title = normalize(title); - let disc_num = result.get_tag(&Tag::Disk).unwrap_or(&"".to_string()).parse::().unwrap_or(1); + let disc_num = result + .get_tag(&Tag::Disk) + .unwrap_or(&"".to_string()) + .parse::() + .unwrap_or(1); match albums.get_mut(&norm_title) { // If the album is in the list, add the track to the appropriate disc in it - Some(album) => { - match album.discs.get_mut(&disc_num) { - Some(disc) => disc.push(result), - None => {album.discs.insert(disc_num, vec![result]);} + Some(album) => match album.discs.get_mut(&disc_num) { + Some(disc) => disc.push(result), + None => { + album.discs.insert(disc_num, vec![result]); } }, // If the album is not in the list, make a new one and add it @@ -816,7 +805,8 @@ impl MusicLibrary { let a_track = a.get_tag(&Tag::Track).unwrap_or(&blank); let b_track = b.get_tag(&Tag::Track).unwrap_or(&blank); - if let (Ok(num_a), Ok(num_b)) = (a_track.parse::(), b_track.parse::()) { + if let (Ok(num_a), Ok(num_b)) = (a_track.parse::(), b_track.parse::()) + { // If parsing the track numbers succeeds, compare as numbers num_a.cmp(&num_b) } else { @@ -834,19 +824,23 @@ impl MusicLibrary { albums } - pub fn query_albums(&self, - query_string: &String, // The query itself + pub fn query_albums( + &self, + query_string: &String, // The query itself ) -> Result, Box> { let all_albums = self.albums(); let normalized_query = normalize(query_string); - let albums: Vec = all_albums.par_iter().filter_map(|album| - if normalize(album.0).contains(&normalized_query) { - Some(album.1.clone()) - } else { - None - } - ).collect(); + let albums: Vec = all_albums + .par_iter() + .filter_map(|album| { + if normalize(album.0).contains(&normalized_query) { + Some(album.1.clone()) + } else { + None + } + }) + .collect(); Ok(albums) } diff --git a/src/music_storage/utils.rs b/src/music_storage/utils.rs index 59cb4d8..590f8ce 100644 --- a/src/music_storage/utils.rs +++ b/src/music_storage/utils.rs @@ -1,10 +1,10 @@ use std::io::{BufReader, BufWriter}; -use std::{path::PathBuf, error::Error, fs}; +use std::{error::Error, fs, path::PathBuf}; use snap; -use unidecode::unidecode; use crate::music_storage::music_db::Song; +use unidecode::unidecode; pub fn normalize(input_string: &String) -> String { // Normalize the unicode and convert everything to lowercase @@ -17,19 +17,26 @@ pub fn normalize(input_string: &String) -> String { } pub fn read_library(path: PathBuf) -> Result, Box> { - // Create a new snap reader over the database file let database = fs::File::open(path)?; let reader = BufReader::new(database); let mut d = snap::read::FrameDecoder::new(reader); // Decode the library from the serialized data into the vec - let library: Vec = bincode::serde::decode_from_std_read(&mut d, bincode::config::standard().with_little_endian().with_variable_int_encoding())?; + let library: Vec = bincode::serde::decode_from_std_read( + &mut d, + bincode::config::standard() + .with_little_endian() + .with_variable_int_encoding(), + )?; Ok(library) } -pub fn write_library(library: &Vec, path: PathBuf, take_backup: bool) -> Result<(), Box> { - +pub fn write_library( + library: &Vec, + path: PathBuf, + take_backup: bool, +) -> Result<(), Box> { // Create 2 new names for the file, a temporary one for writing out, and a backup let mut writer_name = path.clone(); writer_name.set_extension("tmp"); @@ -41,7 +48,13 @@ pub fn write_library(library: &Vec, path: PathBuf, take_backup: bool) -> R let mut e = snap::write::FrameEncoder::new(writer); // Write out the data using bincode - bincode::serde::encode_into_std_write(&library, &mut e, bincode::config::standard().with_little_endian().with_variable_int_encoding())?; + bincode::serde::encode_into_std_write( + &library, + &mut e, + bincode::config::standard() + .with_little_endian() + .with_variable_int_encoding(), + )?; if path.exists() && take_backup { fs::rename(&path, backup_name)?; From 2e0ce48506f80a8ef817300f429f1ba1b44d95c7 Mon Sep 17 00:00:00 2001 From: G2-Games Date: Mon, 6 Nov 2023 02:39:09 -0600 Subject: [PATCH 19/20] A bunch of improvements --- Cargo.toml | 1 - src/lib.rs | 6 +- src/music_controller/music_controller.rs | 15 +--- src/music_player/music_player.rs | 32 ------- src/music_processor/music_processor.rs | 45 ---------- src/music_storage/music_db.rs | 110 +++++++++++++---------- src/music_storage/utils.rs | 42 +++++++-- 7 files changed, 101 insertions(+), 150 deletions(-) delete mode 100644 src/music_processor/music_processor.rs diff --git a/Cargo.toml b/Cargo.toml index 86ab8a8..3376e1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,7 +38,6 @@ rayon = "1.8.0" log = "0.4" pretty_env_logger = "0.4" cue = "2.0.0" -jwalk = "0.8.1" base64 = "0.21.5" zip = "0.6.6" flate2 = "1.0.28" diff --git a/src/lib.rs b/src/lib.rs index 9afe354..7aa37eb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,11 +5,7 @@ pub mod music_tracker { pub mod music_storage { pub mod music_db; pub mod playlist; - pub mod utils; -} - -pub mod music_processor { - pub mod music_processor; + mod utils; } pub mod music_player { diff --git a/src/music_controller/music_controller.rs b/src/music_controller/music_controller.rs index 9455e58..51be432 100644 --- a/src/music_controller/music_controller.rs +++ b/src/music_controller/music_controller.rs @@ -2,7 +2,7 @@ use std::path::PathBuf; use std::sync::{Arc, RwLock}; use crate::music_controller::config::Config; -use crate::music_player::music_player::{DSPMessage, DecoderMessage, MusicPlayer, PlayerStatus}; +use crate::music_player::music_player::{DecoderMessage, MusicPlayer, PlayerStatus}; use crate::music_storage::music_db::{MusicLibrary, Song, Tag}; pub struct MusicController { @@ -63,19 +63,6 @@ impl MusicController { return self.music_player.get_current_song(); } - /// Gets audio playback volume - pub fn get_vol(&self) -> f32 { - return self.music_player.music_processor.audio_volume; - } - - /// Sets audio playback volume on a scale of 0.0 to 1.0 - pub fn set_vol(&mut self, volume: f32) { - self.music_player.music_processor.audio_volume = volume; - self.song_control(DecoderMessage::DSP(DSPMessage::UpdateProcessor(Box::new( - self.music_player.music_processor.clone(), - )))); - } - /// Queries the [MusicLibrary], returning a `Vec` pub fn query_library( &self, diff --git a/src/music_player/music_player.rs b/src/music_player/music_player.rs index 9ed99e5..e3c43ad 100644 --- a/src/music_player/music_player.rs +++ b/src/music_player/music_player.rs @@ -19,7 +19,6 @@ use futures::AsyncBufRead; use crate::music_controller::config::Config; use crate::music_player::music_output::AudioStream; -use crate::music_processor::music_processor::MusicProcessor; use crate::music_storage::music_db::{Song, URI}; use crate::music_tracker::music_tracker::{ DiscordRPC, LastFM, ListenBrainz, MusicTracker, TrackerError, @@ -27,7 +26,6 @@ use crate::music_tracker::music_tracker::{ // Struct that controls playback of music pub struct MusicPlayer { - pub music_processor: MusicProcessor, player_status: PlayerStatus, music_trackers: Vec>, current_song: Arc>>, @@ -51,7 +49,6 @@ pub enum DecoderMessage { Pause, Stop, SeekTo(u64), - DSP(DSPMessage), } #[derive(Clone)] @@ -60,11 +57,6 @@ pub enum TrackerMessage { TrackNow(Song), } -#[derive(Debug, Clone)] -pub enum DSPMessage { - UpdateProcessor(Box), -} - // Holds a song decoder reader, etc struct SongHandler { pub reader: Box, @@ -148,7 +140,6 @@ impl MusicPlayer { ); MusicPlayer { - music_processor: MusicProcessor::new(), music_trackers: Vec::new(), player_status: PlayerStatus::Stopped, current_song, @@ -235,8 +226,6 @@ impl MusicPlayer { let mut audio_output: Option> = None; - let mut music_processor = MusicProcessor::new(); - let (tracker_sender, tracker_receiver): ( Sender, Receiver, @@ -290,11 +279,6 @@ impl MusicPlayer { status_sender.send(PlayerStatus::Paused).unwrap(); } Some(DecoderMessage::SeekTo(time)) => seek_time = Some(time), - Some(DecoderMessage::DSP(dsp_message)) => match dsp_message { - DSPMessage::UpdateProcessor(new_processor) => { - music_processor = *new_processor - } - }, // Exits main decode loop and subsequently ends thread Some(DecoderMessage::Stop) => { status_sender.send(PlayerStatus::Stopped).unwrap(); @@ -373,22 +357,6 @@ impl MusicPlayer { .unwrap(), ); } - // Handles audio normally provided there is an audio stream - if let Some(ref mut audio_output) = audio_output { - // Changes buffer of the MusicProcessor if the packet has a differing capacity or spec - if music_processor.audio_buffer.capacity() != decoded.capacity() - || music_processor.audio_buffer.spec() != decoded.spec() - { - let spec = *decoded.spec(); - let duration = decoded.capacity() as u64; - - music_processor.set_buffer(duration, spec); - } - let transformed_audio = music_processor.process(&decoded); - - // Writes transformed packet to audio out - audio_output.write(transformed_audio).unwrap() - } } Err(Error::IoError(_)) => { // rest in peace packet diff --git a/src/music_processor/music_processor.rs b/src/music_processor/music_processor.rs deleted file mode 100644 index 64f4eaa..0000000 --- a/src/music_processor/music_processor.rs +++ /dev/null @@ -1,45 +0,0 @@ -use std::fmt::Debug; - -use symphonia::core::audio::{AsAudioBufferRef, AudioBuffer, AudioBufferRef, Signal, SignalSpec}; - -#[derive(Clone)] -pub struct MusicProcessor { - pub audio_buffer: AudioBuffer, - pub audio_volume: f32, -} - -impl Debug for MusicProcessor { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("MusicProcessor") - .field("audio_volume", &self.audio_volume) - .finish() - } -} - -impl MusicProcessor { - /// Returns new MusicProcessor with blank buffer and 100% volume - pub fn new() -> Self { - MusicProcessor { - audio_buffer: AudioBuffer::unused(), - audio_volume: 1.0, - } - } - - /// Processes audio samples - /// - /// Currently only supports transformations of volume - pub fn process(&mut self, audio_buffer_ref: &AudioBufferRef) -> AudioBufferRef { - audio_buffer_ref.convert(&mut self.audio_buffer); - - let process = |sample| sample * self.audio_volume; - - self.audio_buffer.transform(process); - - return self.audio_buffer.as_audio_buffer_ref(); - } - - /// Sets buffer of the MusicProcessor - pub fn set_buffer(&mut self, duration: u64, spec: SignalSpec) { - self.audio_buffer = AudioBuffer::new(duration, spec); - } -} diff --git a/src/music_storage/music_db.rs b/src/music_storage/music_db.rs index c6a59be..9e96e44 100644 --- a/src/music_storage/music_db.rs +++ b/src/music_storage/music_db.rs @@ -1,17 +1,17 @@ // Crate things -use super::utils::{normalize, read_library, write_library}; +use super::utils::{normalize, read_library, write_library, find_images}; use crate::music_controller::config::Config; // Various std things use std::collections::BTreeMap; use std::error::Error; +use std::ops::ControlFlow::{Break, Continue}; // Files use cue::cd::CD; use file_format::{FileFormat, Kind}; -use jwalk::WalkDir; +use walkdir::WalkDir; use lofty::{AudioFile, ItemKey, ItemValue, Probe, TagType, TaggedFileExt}; -use std::ffi::OsStr; use std::fs; use std::path::{Path, PathBuf}; @@ -28,9 +28,9 @@ use rayon::prelude::*; use std::sync::{Arc, Mutex, RwLock}; #[derive(Debug, Clone, Deserialize, Serialize)] -pub struct AlbumArt { - pub index: u16, - pub path: Option, +pub enum AlbumArt { + Embedded(usize), + External(URI), } #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)] @@ -112,7 +112,7 @@ impl Song { }, None => None, }, - _ => None, // Other field types are not yet supported + _ => todo!(), // Other field types are not yet supported } } } @@ -182,6 +182,7 @@ pub enum Service { InternetRadio, Spotify, Youtube, + None, } #[derive(Clone, Debug)] @@ -236,6 +237,8 @@ impl Album<'_> { } } +const BLOCKED_EXTENSIONS: [&str; 3] = ["vob", "log", "txt"]; + #[derive(Debug)] pub struct MusicLibrary { pub library: Vec, @@ -272,8 +275,7 @@ impl MusicLibrary { Ok(Self { library }) } - /// Serializes the database out to the file - /// specified in the config + /// Serializes the database out to the file specified in the config pub fn save(&self, config: &Config) -> Result<(), Box> { match config.db_path.try_exists() { Ok(exists) => { @@ -293,19 +295,16 @@ impl MusicLibrary { /// Queries for a [Song] by its [URI], returning a single `Song` /// with the `URI` that matches fn query_uri(&self, path: &URI) -> Option<(&Song, usize)> { - let result = Arc::new(Mutex::new(None)); - let index = Arc::new(Mutex::new(0)); - let _ = &self.library.par_iter().enumerate().for_each(|(i, track)| { + let result = self.library.par_iter().enumerate().try_for_each(|(i, track)| { if path == &track.location { - *result.clone().lock().unwrap() = Some(track); - *index.clone().lock().unwrap() = i; - return; + return std::ops::ControlFlow::Break((track, i)); } + Continue(()) }); - let song = Arc::try_unwrap(result).unwrap().into_inner().unwrap(); - match song { - Some(song) => Some((song, Arc::try_unwrap(index).unwrap().into_inner().unwrap())), - None => None, + + match result { + Break(song) => Some(song), + Continue(_) => None, } } @@ -313,7 +312,7 @@ impl MusicLibrary { /// with matching `PathBuf`s fn query_path(&self, path: &PathBuf) -> Option> { let result: Arc>> = Arc::new(Mutex::new(Vec::new())); - let _ = &self.library.par_iter().for_each(|track| { + let _ = self.library.par_iter().for_each(|track| { if path == track.location.path() { result.clone().lock().unwrap().push(&track); return; @@ -326,20 +325,18 @@ impl MusicLibrary { } } - /// Finds all the music files within a specified folder + /// Finds all the audio files within a specified folder pub fn scan_folder( &mut self, target_path: &str, config: &Config, ) -> Result> { let mut total = 0; - let mut i = 0; - for entry in WalkDir::new(target_path) + for target_file in WalkDir::new(target_path) .follow_links(true) .into_iter() .filter_map(|e| e.ok()) { - let target_file = entry; let path = target_file.path(); // Ensure the target is a file and not a directory, @@ -348,6 +345,7 @@ impl MusicLibrary { continue; } + /* TODO: figure out how to increase the speed of this maybe // Check if the file path is already in the db if self.query_uri(&URI::Local(path.to_path_buf())).is_some() { continue; @@ -358,30 +356,27 @@ impl MusicLibrary { if i % 500 == 0 { self.save(config).unwrap(); } + */ let format = FileFormat::from_file(&path)?; - let extension: &OsStr = match path.extension() { - Some(ext) => ext, - None => OsStr::new(""), + let extension = match path.extension() { + Some(ext) => ext.to_string_lossy().to_ascii_lowercase(), + None => String::new(), }; // If it's a normal file, add it to the database // if it's a cuesheet, do a bunch of fancy stuff if (format.kind() == Kind::Audio || format.kind() == Kind::Video) - && extension.to_ascii_lowercase() != "log" - && extension.to_ascii_lowercase() != "vob" + && !BLOCKED_EXTENSIONS.contains(&extension.as_str()) { match self.add_file(&target_file.path()) { - Ok(_) => { - //println!("{:?}", target_file.path()); - total += 1 - } + Ok(_) => total += 1, Err(_error) => { - //println!("{}, {:?}: {}", format, target_file.file_name(), _error) + println!("{}, {:?}: {}", format, target_file.file_name(), _error) } // TODO: Handle more of these errors }; - } else if extension.to_ascii_lowercase() == "cue" { - total += match self.add_cuesheet(&target_file.path()) { + } else if extension == "cue" { + total += match self.add_cuesheet(&target_file.path().to_path_buf()) { Ok(added) => added, Err(error) => { println!("{}", error); @@ -437,25 +432,26 @@ impl MusicLibrary { }; let value = match item.value() { - ItemValue::Text(value) => String::from(value), - ItemValue::Locator(value) => String::from(value), + ItemValue::Text(value) => value.clone(), + ItemValue::Locator(value) => value.clone(), ItemValue::Binary(bin) => format!("BIN#{}", general_purpose::STANDARD.encode(bin)), }; tags.insert(key, value); } - // Get all the album artwork information + // Get all the album artwork information from the file let mut album_art: Vec = Vec::new(); for (i, _art) in tag.pictures().iter().enumerate() { - let new_art = AlbumArt { - index: i as u16, - path: None, - }; + let new_art = AlbumArt::Embedded(i as usize); album_art.push(new_art) } + // Find images around the music file that can be used + let mut found_images = find_images(&target_file.to_path_buf()).unwrap(); + album_art.append(&mut found_images); + // Get the format as a string let format: Option = match FileFormat::from_file(target_file) { Ok(fmt) => Some(fmt), @@ -515,7 +511,10 @@ impl MusicLibrary { } // Try to remove the original audio file from the db if it exists - let _ = self.remove_uri(&URI::Local(audio_location.clone())); + match self.remove_uri(&URI::Local(audio_location.clone())) { + Ok(_) => tracks_added -= 1, + Err(_) => () + }; // Get the track timing information let pregap = match track.get_zero_pre() { @@ -687,6 +686,7 @@ impl MusicLibrary { sort_by: &Vec, // Tags to sort the resulting data by ) -> Option> { let songs = Arc::new(Mutex::new(Vec::new())); + //let matcher = SkimMatcherV2::default(); self.library.par_iter().for_each(|track| { for tag in target_tags { @@ -701,9 +701,19 @@ impl MusicLibrary { }, }; - if normalize(&track_result.to_string()) - .contains(&normalize(&query_string.to_owned())) - { + /* + let match_level = match matcher.fuzzy_match(&normalize(&track_result), &normalize(query_string)) { + Some(conf) => conf, + None => continue + }; + + if match_level > 100 { + songs.lock().unwrap().push(track); + return; + } + */ + + if normalize(&track_result.to_string()).contains(&normalize(&query_string.to_owned())) { songs.lock().unwrap().push(track); return; } @@ -776,6 +786,7 @@ impl MusicLibrary { .unwrap_or(&"".to_string()) .parse::() .unwrap_or(1); + match albums.get_mut(&norm_title) { // If the album is in the list, add the track to the appropriate disc in it Some(album) => match album.discs.get_mut(&disc_num) { @@ -786,11 +797,13 @@ impl MusicLibrary { }, // If the album is not in the list, make a new one and add it None => { + let album_art = result.album_art.get(0); + let new_album = Album { title, artist: result.get_tag(&Tag::AlbumArtist), discs: BTreeMap::from([(disc_num, vec![result])]), - cover: None, + cover: album_art, }; albums.insert(norm_title, new_album); } @@ -824,6 +837,7 @@ impl MusicLibrary { albums } + /// Queries a list of albums by title pub fn query_albums( &self, query_string: &String, // The query itself diff --git a/src/music_storage/utils.rs b/src/music_storage/utils.rs index 590f8ce..0847ae0 100644 --- a/src/music_storage/utils.rs +++ b/src/music_storage/utils.rs @@ -1,13 +1,14 @@ use std::io::{BufReader, BufWriter}; use std::{error::Error, fs, path::PathBuf}; +use walkdir::WalkDir; +use file_format::{FileFormat, Kind}; use snap; -use crate::music_storage::music_db::Song; +use super::music_db::{Song, AlbumArt, URI}; use unidecode::unidecode; -pub fn normalize(input_string: &String) -> String { - // Normalize the unicode and convert everything to lowercase +pub(super) fn normalize(input_string: &String) -> String { let mut normalized = unidecode(input_string); // Remove non alphanumeric characters @@ -16,7 +17,7 @@ pub fn normalize(input_string: &String) -> String { normalized } -pub fn read_library(path: PathBuf) -> Result, Box> { +pub(super) fn read_library(path: PathBuf) -> Result, Box> { // Create a new snap reader over the database file let database = fs::File::open(path)?; let reader = BufReader::new(database); @@ -32,7 +33,7 @@ pub fn read_library(path: PathBuf) -> Result, Box> { Ok(library) } -pub fn write_library( +pub(super) fn write_library( library: &Vec, path: PathBuf, take_backup: bool, @@ -63,3 +64,34 @@ pub fn write_library( Ok(()) } + +pub fn find_images(song_path: &PathBuf) -> Result, Box> { + let mut images: Vec = Vec::new(); + + let song_dir = song_path.parent().ok_or("")?; + for target_file in WalkDir::new(song_dir) + .follow_links(true) + .into_iter() + .filter_map(|e| e.ok()) + { + if target_file.depth() >= 3 { // Don't recurse very deep + break + } + + let path = target_file.path(); + if !path.is_file() { + continue; + } + + let format = FileFormat::from_file(&path)?.kind(); + if format != Kind::Image { + break + } + + let image_uri = URI::Local(path.to_path_buf().canonicalize().unwrap()); + + images.push(AlbumArt::External(image_uri)); + } + + Ok(images) +} From f00df67844d1571140d0c4134a13961e1a637d46 Mon Sep 17 00:00:00 2001 From: G2-Games Date: Mon, 6 Nov 2023 16:41:28 -0600 Subject: [PATCH 20/20] Work in progress optimizations --- Cargo.toml | 10 +- src/music_storage/music_db.rs | 203 ++++++++++++++++++---------------- 2 files changed, 110 insertions(+), 103 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3376e1e..d30fe73 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,12 +12,12 @@ keywords = ["audio", "music"] categories = ["multimedia::audio"] [dependencies] -file-format = { version = "0.17.3", features = ["reader", "serde"] } +file-format = { version = "0.22.0", features = ["reader-asf", "reader-ebml", "reader-mp4", "reader-rm", "reader-txt", "reader-xml", "serde"] } lofty = "0.16.1" -serde = { version = "1.0.164", features = ["derive"] } +serde = { version = "1.0.191", features = ["derive"] } time = "0.3.22" toml = "0.7.5" -walkdir = "2.3.3" +walkdir = "2.4.0" cpal = "0.15.2" heapless = "0.7.16" rb = "0.4.1" @@ -37,8 +37,6 @@ unidecode = "0.3.0" rayon = "1.8.0" log = "0.4" pretty_env_logger = "0.4" -cue = "2.0.0" base64 = "0.21.5" -zip = "0.6.6" -flate2 = "1.0.28" snap = "1.1.0" +rcue = "0.1.3" diff --git a/src/music_storage/music_db.rs b/src/music_storage/music_db.rs index 9e96e44..3372323 100644 --- a/src/music_storage/music_db.rs +++ b/src/music_storage/music_db.rs @@ -8,7 +8,7 @@ use std::error::Error; use std::ops::ControlFlow::{Break, Continue}; // Files -use cue::cd::CD; +use rcue::parser::parse_from_file; use file_format::{FileFormat, Kind}; use walkdir::WalkDir; use lofty::{AudioFile, ItemKey, ItemValue, Probe, TagType, TaggedFileExt}; @@ -33,6 +33,15 @@ pub enum AlbumArt { External(URI), } +impl AlbumArt { + pub fn uri(&self) -> Option<&URI> { + match self { + Self::Embedded(_) => None, + Self::External(uri) => Some(uri), + } + } +} + #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)] pub enum Tag { Title, @@ -490,21 +499,15 @@ impl MusicLibrary { pub fn add_cuesheet(&mut self, cuesheet: &PathBuf) -> Result> { let mut tracks_added = 0; - let cue_data = CD::parse_file(cuesheet.to_owned()).unwrap(); + let cue_data = parse_from_file(&cuesheet.as_path().to_string_lossy(), false).unwrap(); // Get album level information - let album_title = &cue_data - .get_cdtext() - .read(cue::cd_text::PTI::Title) - .unwrap_or(String::new()); - let album_artist = &cue_data - .get_cdtext() - .read(cue::cd_text::PTI::Performer) - .unwrap_or(String::new()); + let album_title = &cue_data.title; + let album_artist = &cue_data.performer; let parent_dir = cuesheet.parent().expect("The file has no parent path??"); - for (i, track) in cue_data.tracks().iter().enumerate() { - let audio_location = parent_dir.join(track.get_filename()); + for file in cue_data.files.iter() { + let audio_location = &parent_dir.join(file.file.clone()); if !audio_location.exists() { continue; @@ -516,100 +519,106 @@ impl MusicLibrary { Err(_) => () }; - // Get the track timing information - let pregap = match track.get_zero_pre() { - Some(pregap) => Duration::from_micros((pregap as f32 * 13333.333333) as u64), - None => Duration::from_secs(0), - }; - let postgap = match track.get_zero_post() { - Some(postgap) => Duration::from_micros((postgap as f32 * 13333.333333) as u64), - None => Duration::from_secs(0), - }; - let mut start = Duration::from_micros((track.get_start() as f32 * 13333.333333) as u64); - start -= pregap; + let next_track = file.tracks.clone(); + let mut next_track = next_track.iter().skip(1); + for (i, track) in file.tracks.iter().enumerate() { + // Get the track timing information + let pregap = match track.pregap { + Some(pregap) => pregap, + None => Duration::from_secs(0), + }; + let postgap = match track.postgap { + Some(postgap) => postgap, + None => Duration::from_secs(0), + }; + let mut start = track.indices[0].1; + start -= pregap; - let duration = match track.get_length() { - Some(len) => Duration::from_micros((len as f32 * 13333.333333) as u64), - None => { - let tagged_file = match lofty::read_from_path(&audio_location) { - Ok(tagged_file) => tagged_file, - - Err(_) => match Probe::open(&audio_location)?.read() { + let duration = match next_track.next() { + Some(future) => match future.indices.get(0) { + Some(val) => val.1 - start, + None => Duration::from_secs(0) + } + None => { + let tagged_file = match lofty::read_from_path(&audio_location) { Ok(tagged_file) => tagged_file, - Err(error) => return Err(error.into()), - }, - }; + Err(_) => match Probe::open(&audio_location)?.read() { + Ok(tagged_file) => tagged_file, - tagged_file.properties().duration() - start - } - }; - let end = start + duration + postgap; + Err(error) => return Err(error.into()), + }, + }; - // Get the format as a string - let format: Option = match FileFormat::from_file(&audio_location) { - Ok(fmt) => Some(fmt), - Err(_) => None, - }; - - // Get some useful tags - let mut tags: BTreeMap = BTreeMap::new(); - tags.insert(Tag::Album, album_title.clone()); - tags.insert(Tag::Key("AlbumArtist".to_string()), album_artist.clone()); - tags.insert(Tag::Track, (i + 1).to_string()); - match track.get_cdtext().read(cue::cd_text::PTI::Title) { - Some(title) => tags.insert(Tag::Title, title), - None => match track.get_cdtext().read(cue::cd_text::PTI::UPC_ISRC) { - Some(title) => tags.insert(Tag::Title, title), - None => { - let namestr = format!("{} - {}", i, track.get_filename()); - tags.insert(Tag::Title, namestr) + tagged_file.properties().duration() - start } - }, - }; - match track.get_cdtext().read(cue::cd_text::PTI::Performer) { - Some(artist) => tags.insert(Tag::Artist, artist), - None => None, - }; - match track.get_cdtext().read(cue::cd_text::PTI::Genre) { - Some(genre) => tags.insert(Tag::Genre, genre), - None => None, - }; - match track.get_cdtext().read(cue::cd_text::PTI::Message) { - Some(comment) => tags.insert(Tag::Comment, comment), - None => None, - }; + }; + let end = start + duration + postgap; - let album_art = Vec::new(); + // Get the format as a string + let format: Option = match FileFormat::from_file(&audio_location) { + Ok(fmt) => Some(fmt), + Err(_) => None, + }; - let new_song = Song { - location: URI::Cue { - location: audio_location, - index: i, - start, - end, - }, - plays: 0, - skips: 0, - favorited: false, - rating: None, - format, - duration, - play_time: Duration::from_secs(0), - last_played: None, - date_added: Some(chrono::offset::Utc::now()), - date_modified: Some(chrono::offset::Utc::now()), - tags, - album_art, - }; - - match self.add_song(new_song) { - Ok(_) => tracks_added += 1, - Err(_error) => { - //println!("{}", _error); - continue; + // Get some useful tags + let mut tags: BTreeMap = BTreeMap::new(); + match album_title { + Some(title) => {tags.insert(Tag::Album, title.clone());}, + None => (), } - }; + match album_artist { + Some(artist) => {tags.insert(Tag::Album, artist.clone());}, + None => (), + } + tags.insert(Tag::Track, track.no.parse().unwrap_or((i + 1).to_string())); + match track.title.clone() { + Some(title) => tags.insert(Tag::Title, title), + None => match track.isrc.clone() { + Some(title) => tags.insert(Tag::Title, title), + None => { + let namestr = format!("{} - {}", i, file.file.clone()); + tags.insert(Tag::Title, namestr) + } + }, + }; + match track.performer.clone() { + Some(artist) => tags.insert(Tag::Artist, artist), + None => None, + }; + + // Find images around the music file that can be used + let album_art = find_images(&audio_location.to_path_buf()).unwrap(); + + let new_song = Song { + location: URI::Cue { + location: audio_location.clone(), + index: i, + start, + end, + }, + plays: 0, + skips: 0, + favorited: false, + rating: None, + format, + duration, + play_time: Duration::from_secs(0), + last_played: None, + date_added: Some(chrono::offset::Utc::now()), + date_modified: Some(chrono::offset::Utc::now()), + tags, + album_art, + }; + + match self.add_song(new_song) { + Ok(_) => tracks_added += 1, + Err(_error) => { + //println!("{}", _error); + continue; + } + }; + } } Ok(tracks_added)