From 7f57367aadb0fb7d8445830272745edbf854a71b Mon Sep 17 00:00:00 2001 From: G2-Games <ke0bhogsg@gmail.com> Date: Fri, 29 Sep 2023 23:28:12 -0500 Subject: [PATCH 001/136] 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<String>, - pub album: Option<String>, - pub tracknum: Option<usize>, - pub artist: Option<String>, - pub date: Option<Date>, - pub genre: Option<String>, - pub plays: Option<usize>, - pub favorited: Option<bool>, - pub format: Option<FileFormat>, // TODO: Make this a proper FileFormat eventually - pub duration: Option<Duration>, - pub custom_tags: Option<Vec<Tag>>, +pub struct AlbumArt { + pub path: Option<URI>; } -#[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<FileFormat>, + pub duration: Duration, + pub play_time: Duration, + #[serde(with = "ts_seconds_option")] + pub last_played: Option<DateTime<Utc>>, + #[serde(with = "ts_seconds_option")] + pub date_added: Option<DateTime<Utc>>, + pub tags: Vec<(String, String)>, +} + +impl Song { + pub fn get_tag(&self, target_key: String) -> Option<String> { + for tag in self.tags { + if tag.0 == target_key { + return Some(tag.1) + } + } + None + } + + pub fn get_tags(&self, target_keys: Vec<String>) -> Vec<Option<String>> { + 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<Path>, } -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<Vec<Song>, Box<dyn Error>> { + let mut library: Vec<Song> = 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::<usize, bool>(0).unwrap(), - None => false - } +fn path_in_db(query_path: &Path, library: &Vec<Song>) -> bool { + unimplemented!() } - pub fn find_all_music( config: &Config, target_path: &str, ) -> Result<(), Box<dyn std::error::Error>> { - 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<String>, // The tags to search + sort_by: &Vec<String>, // Tags to sort the resulting data by ) -> Option<Vec<MusicObject>> { - 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<MusicObject> = vec![]; - - while let Some(row) = rows.next().unwrap() { - let custom_tags: Vec<Tag> = match row.get::<usize, String>(11) { - Ok(result) => serde_json::from_str(&result).unwrap_or(vec![]), - Err(_) => vec![] - }; - - let file_format: FileFormat = FileFormat::from(row.get::<usize, String>(9).unwrap().as_bytes()); - - let new_song = Song { - // TODO: Implement proper errors here - path: URI::Local(String::from("URI")), - title: row.get::<usize, String>(1).ok(), - album: row.get::<usize, String>(2).ok(), - tracknum: row.get::<usize, usize>(3).ok(), - artist: row.get::<usize, String>(4).ok(), - date: Date::from_calendar_date(row.get::<usize, i32>(5).unwrap_or(0), time::Month::January, 1).ok(), // TODO: Fix this to get the actual date - genre: row.get::<usize, String>(6).ok(), - plays: row.get::<usize, usize>(7).ok(), - favorited: row.get::<usize, bool>(8).ok(), - format: Some(file_format), - duration: Some(Duration::from_secs(row.get::<usize, u64>(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 <ke0bhogsg@gmail.com> Date: Sat, 30 Sep 2023 23:31:42 -0500 Subject: [PATCH 002/136] 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<URI>; + pub index: u16, + pub path: Option<URI>, } /// 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<u8>, pub format: Option<FileFormat>, pub duration: Duration, pub play_time: Duration, @@ -33,25 +35,26 @@ pub struct Song { pub last_played: Option<DateTime<Utc>>, #[serde(with = "ts_seconds_option")] pub date_added: Option<DateTime<Utc>>, + pub album_art: Vec<AlbumArt>, pub tags: Vec<(String, String)>, } impl Song { - pub fn get_tag(&self, target_key: String) -> Option<String> { - 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<String>) -> Vec<Option<String>> { + pub fn get_tags(&self, target_keys: &Vec<String>) -> Vec<Option<String>> { 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<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<Vec<Song>, Box<dyn Error>> { - let mut library: Vec<Song> = 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<Song>) -> bool { - unimplemented!() -} - -pub fn find_all_music( - config: &Config, - target_path: &str, -) -> Result<(), Box<dyn std::error::Error>> { - - 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<String> = 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<Song>, } -/// Query the database, returning a list of items -pub fn query ( - query_string: &String, // The query itself - target_tags: &Vec<String>, // The tags to search - sort_by: &Vec<String>, // Tags to sort the resulting data by -) -> Option<Vec<MusicObject>> { - 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<Self, Box<dyn Error>> { + let mut library: Vec<Song> = 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<dyn Error>> { + 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<Song> { + 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<dyn std::error::Error>> { + 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<dyn std::error::Error>> { + // 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<AlbumArt> = 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<FileFormat> = 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<dyn std::error::Error>> { + 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<dyn std::error::Error>> { + 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<String>, // The tags to search + sort_by: &Vec<String>, // Tags to sort the resulting data by + ) -> Option<Vec<MusicObject>> { + 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 <ke0bhogsg@gmail.com> Date: Mon, 2 Oct 2023 01:26:36 -0500 Subject: [PATCH 003/136] 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<URI>, } +#[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<DateTime<Utc>>, #[serde(with = "ts_seconds_option")] pub date_added: Option<DateTime<Utc>>, + #[serde(with = "ts_seconds_option")] + pub date_modified: Option<DateTime<Utc>>, pub album_art: Vec<AlbumArt>, - 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<Song>, } +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<String>, // The tags to search - sort_by: &Vec<String>, // Tags to sort the resulting data by - ) -> Option<Vec<MusicObject>> { - unimplemented!() + query_string: &String, // The query itself + target_tags: &Vec<Tag>, // The tags to search + sort_by: &Vec<Tag>, // Tags to sort the resulting data by + ) -> Option<Vec<Song>> { + 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::<f64>(), tag_b.parse::<f64>()) { + // 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 <ke0bhogsg@gmail.com> Date: Mon, 2 Oct 2023 20:27:59 -0500 Subject: [PATCH 004/136] 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<RwLock<Config>>, + pub library: MusicLibrary, music_player: MusicPlayer, } impl MusicController { /// Creates new MusicController with config at given path - pub fn new(config_path: &PathBuf) -> Result<MusicController, std::io::Error>{ + pub fn new(config_path: &PathBuf) -> Result<MusicController, Box<dyn std::error::Error>>{ let config = Arc::new(RwLock::new(Config::new(config_path)?)); let 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<MusicController, toml::de::Error> { + pub fn from(config_path: &PathBuf) -> Result<MusicController, Box<dyn std::error::Error>> { let config = Arc::new(RwLock::new(Config::from(config_path)?)); let 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<Song>` + pub fn query_library( + &self, + query_string: &String, + target_tags: Vec<Tag>, + sort_by: Vec<Tag> + ) -> Option<Vec<&Song>> { + self.library.query(query_string, &target_tags, &sort_by) + } } 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<Self, Box<dyn Error>> { + /// the [MusicLibrary] Vec + pub fn init(config: Arc<RwLock<Config>>) -> Result<Self, Box<dyn Error>> { + let global_config = &*config.read().unwrap(); let mut library: Vec<Song> = Vec::new(); - let mut 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<dyn std::error::Error>> { + pub fn find_all_music(&mut self, target_path: &str, config: &Config) -> Result<(), Box<dyn std::error::Error>> { let mut current_dir = PathBuf::new(); - 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<Tag>, // The tags to search sort_by: &Vec<Tag>, // Tags to sort the resulting data by - ) -> Option<Vec<Song>> { - let mut songs = Vec::new(); + ) -> Option<Vec<&Song>> { + 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 <ke0bhogsg@gmail.com> Date: Tue, 3 Oct 2023 20:00:13 -0500 Subject: [PATCH 005/136] 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<Tag>, + search_location: bool, sort_by: Vec<Tag> ) -> Option<Vec<&Song>> { - 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<String>) -> Vec<Option<String>> { - 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<dyn Any> { + 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<Song> { + /// Queries for a [Song] by its [URI], returning a single Song + /// with the URI that matches + fn query_by_uri(&self, path: &URI) -> Option<Song> { 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<dyn std::error::Error>> { - 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<dyn std::error::Error>> { - 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<Tag>, // The tags to search + search_location: bool, // Whether to search the location field or not sort_by: &Vec<Tag>, // Tags to sort the resulting data by ) -> Option<Vec<&Song>> { 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 <ke0bhogsg@gmail.com> Date: Tue, 3 Oct 2023 21:15:56 -0500 Subject: [PATCH 006/136] 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<dyn Any> { + pub fn get_field(&self, target_field: &str) -> Option<String> { 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::<f64>(), tag_b.parse::<f64>()) { + // Try to parse the tags as f64 + if let (Ok(num_a), Ok(num_b)) = (tag_a.parse::<i32>(), tag_b.parse::<i32>()) { // 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 <ke0bhogsg@gmail.com> Date: Fri, 6 Oct 2023 01:22:55 -0500 Subject: [PATCH 007/136] 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<Song>` + /// Queries the [MusicLibrary], returning a `Vec<Song>` 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 <ke0bhogsg@gmail.com> Date: Wed, 1 Nov 2023 01:31:58 -0500 Subject: [PATCH 008/136] 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<String> { 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<dyn Error>> { 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<dyn Error>> { + 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<dyn Error>> { 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<Song> { - 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<dyn std::error::Error>> { - 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<Song>` + /// with matching `PathBuf`s + fn query_path(&self, path: &PathBuf) -> Option<Vec<&Song>> { + let result: Arc<Mutex<Vec<&Song>>> = 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<usize, Box<dyn std::error::Error>> { + 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<dyn std::error::Error>> { + pub fn add_file(&mut self, target_file: &Path) -> Result<(), Box<dyn Error>> { // 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<dyn std::error::Error>> { - 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<usize, Box<dyn Error>> { + 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<FileFormat> = 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<dyn Error>> { + 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<dyn std::error::Error>> { - 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<usize, Box<dyn Error>> { + 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<dyn std::error::Error>> { + 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 <ke0bhogsg@gmail.com> Date: Wed, 1 Nov 2023 10:57:15 -0500 Subject: [PATCH 009/136] 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<FileFormat> = 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 <ke0bhogsg@gmail.com> Date: Wed, 1 Nov 2023 14:13:25 -0500 Subject: [PATCH 010/136] 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 <ke0bhogsg@gmail.com> Date: Wed, 1 Nov 2023 14:14:28 -0500 Subject: [PATCH 011/136] `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<Config> { + pub fn new(config_file: &PathBuf) -> std::io::Result<Config> { 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<Config, toml::de::Error> { - 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<MusicController, Box<dyn std::error::Error>>{ + pub fn new(config_path: &PathBuf) -> Result<MusicController, Box<dyn std::error::Error>> { let config = Arc::new(RwLock::new(Config::new(config_path)?)); let 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<MusicController, Box<dyn std::error::Error>> { 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<Song> { 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<Song>` pub fn query_library( &self, query_string: &String, target_tags: Vec<Tag>, search_location: bool, - sort_by: Vec<Tag> + sort_by: Vec<Tag>, ) -> Option<Vec<&Song>> { - 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<T> = result::Result<T, AudioOutputError>; -pub trait OutputSample: SizedSample + FromSample<f32> + IntoSample<f32> +cpal::Sample + ConvertibleSample + RawSample + std::marker::Send + 'static {} +pub trait OutputSample: + SizedSample + + FromSample<f32> + + IntoSample<f32> + + cpal::Sample + + ConvertibleSample + + RawSample + + std::marker::Send + + 'static +{ +} pub struct AudioOutput<T> -where T: OutputSample, +where + T: OutputSample, { ring_buf_producer: rb::Producer<T>, sample_buf: SampleBuffer<T>, @@ -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<Box<dyn AudioStream>> { - 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::<i8>::create_stream(spec, &device, &config.into(), duration), - cpal::SampleFormat::I16 => AudioOutput::<i16>::create_stream(spec, &device, &config.into(), duration), - cpal::SampleFormat::I32 => AudioOutput::<i32>::create_stream(spec, &device, &config.into(), duration), - //cpal::SampleFormat::I64 => AudioOutput::<i64>::create_stream(spec, &device, &config.into(), duration), - cpal::SampleFormat::U8 => AudioOutput::<u8>::create_stream(spec, &device, &config.into(), duration), - cpal::SampleFormat::U16 => AudioOutput::<u16>::create_stream(spec, &device, &config.into(), duration), - cpal::SampleFormat::U32 => AudioOutput::<u32>::create_stream(spec, &device, &config.into(), duration), - //cpal::SampleFormat::U64 => AudioOutput::<u64>::create_stream(spec, &device, &config.into(), duration), - cpal::SampleFormat::F32 => AudioOutput::<f32>::create_stream(spec, &device, &config.into(), duration), - cpal::SampleFormat::F64 => AudioOutput::<f64>::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::<i8>::create_stream(spec, &device, &config.into(), duration) + } + cpal::SampleFormat::I16 => { + AudioOutput::<i16>::create_stream(spec, &device, &config.into(), duration) + } + cpal::SampleFormat::I32 => { + AudioOutput::<i32>::create_stream(spec, &device, &config.into(), duration) + } + //cpal::SampleFormat::I64 => AudioOutput::<i64>::create_stream(spec, &device, &config.into(), duration), + cpal::SampleFormat::U8 => { + AudioOutput::<u8>::create_stream(spec, &device, &config.into(), duration) + } + cpal::SampleFormat::U16 => { + AudioOutput::<u16>::create_stream(spec, &device, &config.into(), duration) + } + cpal::SampleFormat::U32 => { + AudioOutput::<u32>::create_stream(spec, &device, &config.into(), duration) + } + //cpal::SampleFormat::U64 => AudioOutput::<u64>::create_stream(spec, &device, &config.into(), duration), + cpal::SampleFormat::F32 => { + AudioOutput::<f32>::create_stream(spec, &device, &config.into(), duration) + } + cpal::SampleFormat::F64 => { + AudioOutput::<f64>::create_stream(spec, &device, &config.into(), duration) + } + _ => todo!(), + }; } impl<T: OutputSample> AudioOutput<T> { // Creates the stream (TODO: Merge w/open_stream?) - fn create_stream(spec: SignalSpec, device: &cpal::Device, config: &cpal::StreamConfig, duration: Duration) -> Result<Box<dyn AudioStream>> { + fn create_stream( + spec: SignalSpec, + device: &cpal::Device, + config: &cpal::StreamConfig, + duration: Duration, + ) -> Result<Box<dyn AudioStream>> { 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::<T>::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<T: OutputSample> AudioStream for AudioOutput<T> { 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<T: OutputSample> AudioStream for AudioOutput<T> { 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<MusicProcessor>) + UpdateProcessor(Box<MusicProcessor>), } // Holds a song decoder reader, etc @@ -75,49 +77,59 @@ struct SongHandler { impl SongHandler { pub fn new(uri: &URI) -> Result<Self, ()> { // 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<dyn MediaSource> = 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<Result<(), TrackerError>>, tracker_receiver: Receiver<TrackerMessage>, config: Arc<RwLock<Config>>) { + + fn start_tracker( + status_sender: Sender<Result<(), TrackerError>>, + tracker_receiver: Receiver<TrackerMessage>, + config: Arc<RwLock<Config>>, + ) { 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<Box<dyn MusicTracker>> = Vec::new(); // Updates local trackers to the music controller config // TODO: refactor - let update_trackers = |trackers: &mut Vec<Box<dyn MusicTracker>>|{ + let update_trackers = |trackers: &mut Vec<Box<dyn MusicTracker>>| { 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<DecoderMessage>, status_sender: Sender<PlayerStatus>, config: Arc<RwLock<Config>>, current_song: Arc<RwLock<Option<Song>>>) { + fn start_player( + message_receiver: Receiver<DecoderMessage>, + status_sender: Sender<PlayerStatus>, + config: Arc<RwLock<Config>>, + current_song: Arc<RwLock<Option<Song>>>, + ) { // 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<u64> = None; - + let mut audio_output: Option<Box<dyn AudioStream>> = None; - + let mut music_processor = MusicProcessor::new(); - - let (tracker_sender, tracker_receiver): (Sender<TrackerMessage>, Receiver<TrackerMessage>) = mpsc::channel(); - let (tracker_status_sender, tracker_status_receiver): (Sender<Result<(), TrackerError>>, Receiver<Result<(), TrackerError>>) = mpsc::channel(); - + + let (tracker_sender, tracker_receiver): ( + Sender<TrackerMessage>, + Receiver<TrackerMessage>, + ) = mpsc::channel(); + let (tracker_status_sender, tracker_status_receiver): ( + Sender<Result<(), TrackerError>>, + Receiver<Result<(), TrackerError>>, + ) = 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<Song>{ + + pub fn get_current_song(&self) -> Option<Song> { 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<Self, surf::Error> { - 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<u64> { 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<u32, TrackerError>; } @@ -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<u32, TrackerError> { 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<String, surf::Error> { 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<String, surf::Error> { + pub async fn get_session_key( + api_key: &String, + shared_secret: &String, + auth_token: &String, + ) -> Result<String, surf::Error> { 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<surf::Response, surf::Error> { + pub async fn api_request( + &self, + mut params: BTreeMap<&str, &str>, + ) -> Result<surf::Response, surf::Error> { 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<u32, TrackerError> { 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<surf::Response, surf::Error> { - 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<surf::Response, surf::Error> { + 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 <ke0bhogsg@gmail.com> Date: Fri, 3 Nov 2023 08:39:19 -0500 Subject: [PATCH 012/136] 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<Tag>, ) -> Option<Vec<&Song>> { 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<dyn Error>> { + 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<dyn Error>> { @@ -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<Path>, +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<Song>, } 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<Song> = 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<Tag>, // The tags to search - search_location: bool, // Whether to search the location field or not sort_by: &Vec<Tag>, // Tags to sort the resulting data by ) -> Option<Vec<&Song>> { 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<Vec<Album>, Box<dyn Error>> { + let mut albums: Vec<Album> = 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 <ke0bhogsg@gmail.com> Date: Fri, 3 Nov 2023 09:49:53 -0500 Subject: [PATCH 013/136] 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<Tag>, ) -> Option<Vec<&Song>> { 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<Tag>, // 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 <ke0bhogsg@gmail.com> Date: Fri, 3 Nov 2023 10:30:22 -0500 Subject: [PATCH 014/136] 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<Song>, } -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 <ke0bhogsg@gmail.com> Date: Fri, 3 Nov 2023 12:29:04 -0500 Subject: [PATCH 015/136] 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<Vec<Album>, Box<dyn Error>> { - let mut albums: Vec<Album> = 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 <ke0bhogsg@gmail.com> Date: Sat, 4 Nov 2023 04:57:20 -0500 Subject: [PATCH 016/136] 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<URI>, } -#[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<DateTime<Utc>>, pub album_art: Vec<AlbumArt>, - pub tags: Vec<(Tag, String)>, + pub tags: BTreeMap<Tag, String>, } 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<String> { @@ -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<usize, Vec<&'a Song>>, +} + +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<usize, Vec<&Song>> { + &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<Song>, } -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<Song> = 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<dyn Error>> { 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<Tag, String> = 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<Tag, String> = 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<Vec<Album>, Box<dyn Error>> { - let mut albums: BTreeMap<&String, Album> = BTreeMap::new(); + /// Generates all albums from the track list + pub fn albums(&self) -> BTreeMap<String, Album> { + let mut albums: BTreeMap<String, Album> = 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::<usize>().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::<i32>(), + b.get_tag(&Tag::Title).unwrap_or(&blank).parse::<i32>() + ) { + // 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<Vec<Album>, Box<dyn Error>> { + let all_albums = self.albums(); + + let normalized_query = normalize(query_string); + let albums: Vec<Album> = 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<Vec<Song>, Box<dyn Error>> { + 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<Song> = 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<Song>, path: PathBuf, take_backup: bool) -> Result<(), Box<dyn Error>> { + 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 <ke0bhogsg@gmail.com> Date: Sat, 4 Nov 2023 18:40:05 -0500 Subject: [PATCH 017/136] 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::<i32>(), tag_b.parse::<i32>()) { // 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<String, Album> { let mut albums: BTreeMap<String, Album> = 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::<usize>().unwrap_or(1); - let norm_title = normalize(title); + + let disc_num = result.get_tag(&Tag::Disk).unwrap_or(&"".to_string()).parse::<usize>().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::<i32>(), - b.get_tag(&Tag::Title).unwrap_or(&blank).parse::<i32>() - ) { - // 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::<i32>(), b_track.parse::<i32>()) { + // 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<Vec<Song>, Box<dyn Error>> { + + // 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<Song> = 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<Song>, path: PathBuf, take_backup: bool) -> Result<(), Box<dyn Error>> { + + // 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 <ke0bhogsg@gmail.com> Date: Sat, 4 Nov 2023 18:42:21 -0500 Subject: [PATCH 018/136] `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::<usize>().unwrap_or(1); + let disc_num = result + .get_tag(&Tag::Disk) + .unwrap_or(&"".to_string()) + .parse::<usize>() + .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::<i32>(), b_track.parse::<i32>()) { + if let (Ok(num_a), Ok(num_b)) = (a_track.parse::<i32>(), b_track.parse::<i32>()) + { // 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<Vec<Album>, Box<dyn Error>> { let all_albums = self.albums(); let normalized_query = normalize(query_string); - let albums: Vec<Album> = all_albums.par_iter().filter_map(|album| - if normalize(album.0).contains(&normalized_query) { - Some(album.1.clone()) - } else { - None - } - ).collect(); + let albums: Vec<Album> = 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<Vec<Song>, Box<dyn Error>> { - // 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<Song> = bincode::serde::decode_from_std_read(&mut d, bincode::config::standard().with_little_endian().with_variable_int_encoding())?; + let library: Vec<Song> = 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<Song>, path: PathBuf, take_backup: bool) -> Result<(), Box<dyn Error>> { - +pub fn write_library( + library: &Vec<Song>, + path: PathBuf, + take_backup: bool, +) -> Result<(), Box<dyn Error>> { // 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<Song>, 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 <ke0bhogsg@gmail.com> Date: Mon, 6 Nov 2023 02:39:09 -0600 Subject: [PATCH 019/136] 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<Song>` 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<Box<dyn MusicTracker + Send>>, current_song: Arc<RwLock<Option<Song>>>, @@ -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<MusicProcessor>), -} - // Holds a song decoder reader, etc struct SongHandler { pub reader: Box<dyn FormatReader>, @@ -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<Box<dyn AudioStream>> = None; - let mut music_processor = MusicProcessor::new(); - let (tracker_sender, tracker_receiver): ( Sender<TrackerMessage>, Receiver<TrackerMessage>, @@ -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<f32>, - 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<URI>, +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<Song>, @@ -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<dyn Error>> { 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<Vec<&Song>> { let result: Arc<Mutex<Vec<&Song>>> = 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<usize, Box<dyn std::error::Error>> { 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<AlbumArt> = 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<FileFormat> = 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<Tag>, // Tags to sort the resulting data by ) -> Option<Vec<&Song>> { 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::<usize>() .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<Vec<Song>, Box<dyn Error>> { +pub(super) fn read_library(path: PathBuf) -> Result<Vec<Song>, Box<dyn Error>> { // 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<Vec<Song>, Box<dyn Error>> { Ok(library) } -pub fn write_library( +pub(super) fn write_library( library: &Vec<Song>, path: PathBuf, take_backup: bool, @@ -63,3 +64,34 @@ pub fn write_library( Ok(()) } + +pub fn find_images(song_path: &PathBuf) -> Result<Vec<AlbumArt>, Box<dyn Error>> { + let mut images: Vec<AlbumArt> = 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 <ke0bhogsg@gmail.com> Date: Mon, 6 Nov 2023 16:41:28 -0600 Subject: [PATCH 020/136] 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<usize, Box<dyn Error>> { 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<FileFormat> = match FileFormat::from_file(&audio_location) { - Ok(fmt) => Some(fmt), - Err(_) => None, - }; - - // Get some useful tags - let mut tags: BTreeMap<Tag, String> = 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<FileFormat> = 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<Tag, String> = 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) From 8fec560bcfdf44d520ef2ac3e11b7010941783ba Mon Sep 17 00:00:00 2001 From: G2-Games <ke0bhogsg@gmail.com> Date: Fri, 19 Jan 2024 15:37:19 -0600 Subject: [PATCH 021/136] Restructured repository as a workspace --- Cargo.toml | 34 ++ src/bus_control.rs | 0 src/lib.rs | 24 ++ src/music_controller/config.rs | 54 +++ src/music_controller/init.rs | 19 + src/music_controller/music_controller.rs | 68 ++++ src/music_player/music_output.rs | 167 +++++++++ src/music_player/music_player.rs | 364 ++++++++++++++++++ src/music_player/music_resampler.rs | 147 ++++++++ src/music_processor/music_processor.rs | 35 ++ src/music_storage/music_db.rs | 456 +++++++++++++++++++++++ src/music_storage/playlist.rs | 27 ++ src/music_tracker/music_tracker.rs | 186 +++++++++ 13 files changed, 1581 insertions(+) create mode 100644 Cargo.toml create mode 100644 src/bus_control.rs create mode 100644 src/lib.rs create mode 100644 src/music_controller/config.rs create mode 100644 src/music_controller/init.rs create mode 100644 src/music_controller/music_controller.rs create mode 100644 src/music_player/music_output.rs create mode 100644 src/music_player/music_player.rs create mode 100644 src/music_player/music_resampler.rs create mode 100644 src/music_processor/music_processor.rs create mode 100644 src/music_storage/music_db.rs create mode 100644 src/music_storage/playlist.rs create mode 100644 src/music_tracker/music_tracker.rs diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..4a63b78 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "dango-core" +version = "0.1.1" +edition = "2021" +license = "AGPL-3.0-only" +description = "A music backend that manages storage, querying, and playback of remote and local songs." +homepage = "https://dangoware.com/dango-music-player" +documentation = "https://docs.rs/dango-core" +readme = "README.md" +repository = "https://github.com/DangoWare/dango-music-player" +keywords = ["audio", "music"] +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" +walkdir = "2.3.3" +cpal = "0.15.2" +heapless = "0.7.16" +rb = "0.4.1" +symphonia = { version = "0.5.3", features = ["all-codecs"] } +serde_json = "1.0.104" +cue = "2.0.0" +async-std = "1.12.0" +async-trait = "0.1.73" +md-5 = "0.10.5" +surf = "2.3.2" +futures = "0.3.28" +rubato = "0.12.0" +arrayvec = "0.7.4" diff --git a/src/bus_control.rs b/src/bus_control.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..3a25b3d --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,24 @@ +pub mod music_tracker { + pub mod music_tracker; +} + +pub mod music_storage { + pub mod music_db; + pub mod playlist; +} + +pub mod music_processor { + pub mod music_processor; +} + +pub mod music_player { + pub mod music_player; + pub mod music_output; + pub mod music_resampler; +} + +pub mod music_controller { + pub mod music_controller; + pub mod config; + pub mod init; +} \ No newline at end of file diff --git a/src/music_controller/config.rs b/src/music_controller/config.rs new file mode 100644 index 0000000..70ee273 --- /dev/null +++ b/src/music_controller/config.rs @@ -0,0 +1,54 @@ +use std::path::PathBuf; +use std::fs::read_to_string; +use std::fs; + +use serde::{Deserialize, Serialize}; + +use crate::music_tracker::music_tracker::LastFM; + +#[derive(Serialize, Deserialize)] +pub struct Config { + pub db_path: Box<PathBuf>, + pub lastfm: Option<LastFM>, +} + +impl Config { + // Creates and saves a new config with default values + pub fn new(config_file: &PathBuf) -> std::io::Result<Config> { + let path = PathBuf::from("./music_database.db3"); + + let config = Config { + db_path: Box::new(path), + lastfm: None, + }; + config.save(config_file)?; + + Ok(config) + } + + // Loads config from given file path + pub fn from(config_file: &PathBuf) -> std::result::Result<Config, toml::de::Error> { + 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(_) => {}, + } + + fs::rename(temp_file, config_file)?; + Ok(()) + } +} diff --git a/src/music_controller/init.rs b/src/music_controller/init.rs new file mode 100644 index 0000000..80b91bf --- /dev/null +++ b/src/music_controller/init.rs @@ -0,0 +1,19 @@ +use std::path::Path; +use std::fs::File; + +pub fn init() { + +} + +fn init_config() { + let config_path = "./config.toml"; + + if !Path::new(config_path).try_exists().unwrap() { + File::create("./config.toml").unwrap(); + } +} + +fn init_db() { + +} + diff --git a/src/music_controller/music_controller.rs b/src/music_controller/music_controller.rs new file mode 100644 index 0000000..bb1ab66 --- /dev/null +++ b/src/music_controller/music_controller.rs @@ -0,0 +1,68 @@ +use std::path::PathBuf; + +use rusqlite::Result; + +use crate::music_controller::config::Config; +use crate::music_player::music_player::{MusicPlayer, PlayerStatus, PlayerMessage, DSPMessage}; +use crate::music_processor::music_processor::MusicProcessor; +use crate::music_storage::music_db::URI; + +pub struct MusicController { + pub config: Config, + music_player: MusicPlayer, +} + +impl MusicController { + /// Creates new MusicController with config at given path + pub fn new(config_path: &PathBuf) -> Result<MusicController, std::io::Error>{ + let config = Config::new(config_path)?; + let music_player = MusicPlayer::new(); + + let controller = MusicController { + config, + music_player, + }; + + return Ok(controller) + } + + /// Creates new music controller from a config at given path + pub fn from(config_path: &PathBuf) -> std::result::Result<MusicController, toml::de::Error> { + let config = Config::from(config_path)?; + let music_player = MusicPlayer::new(); + + let controller = MusicController { + config, + music_player, + }; + + return Ok(controller) + } + + /// Opens and plays song at given URI + pub fn open_song(&mut self, uri: &URI) { + self.music_player.open_song(uri); + } + + /// Sends given message to music player + pub fn song_control(&mut self, message: PlayerMessage) { + 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 audio playback volume + pub fn get_vol(&mut 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(PlayerMessage::DSP(DSPMessage::UpdateProcessor(Box::new(self.music_player.music_processor.clone())))); + } + +} diff --git a/src/music_player/music_output.rs b/src/music_player/music_output.rs new file mode 100644 index 0000000..b774b0f --- /dev/null +++ b/src/music_player/music_output.rs @@ -0,0 +1,167 @@ +use std::{result, thread}; + +use symphonia::core::audio::{AudioBufferRef, SignalSpec, RawSample, SampleBuffer}; +use symphonia::core::conv::{ConvertibleSample, IntoSample, FromSample}; +use symphonia::core::units::Duration; + +use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; +use cpal::{self, SizedSample}; + +use rb::*; + +use crate::music_player::music_resampler::Resampler; + +pub trait AudioStream { + fn write(&mut self, decoded: AudioBufferRef<'_>) -> Result<()>; + fn flush(&mut self); +} + +#[derive(Debug)] +pub enum AudioOutputError { + OpenStreamError, + PlayStreamError, + StreamClosedError, +} + +pub type Result<T> = result::Result<T, AudioOutputError>; + +pub trait OutputSample: SizedSample + FromSample<f32> + IntoSample<f32> +cpal::Sample + ConvertibleSample + RawSample + std::marker::Send + 'static {} + +pub struct AudioOutput<T> +where T: OutputSample, +{ + ring_buf_producer: rb::Producer<T>, + sample_buf: SampleBuffer<T>, + stream: cpal::Stream, + resampler: Option<Resampler<T>>, +} +impl OutputSample for i8 {} +impl OutputSample for i16 {} +impl OutputSample for i32 {} +//impl OutputSample for i64 {} +impl OutputSample for u8 {} +impl OutputSample for u16 {} +impl OutputSample for u32 {} +//impl OutputSample for u64 {} +impl OutputSample for f32 {} +impl OutputSample for f64 {} +//create a new trait with functions, then impl that somehow + +pub fn open_stream(spec: SignalSpec, duration: Duration) -> Result<Box<dyn AudioStream>> { + 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::<i8>::create_stream(spec, &device, &config.into(), duration), + cpal::SampleFormat::I16 => AudioOutput::<i16>::create_stream(spec, &device, &config.into(), duration), + cpal::SampleFormat::I32 => AudioOutput::<i32>::create_stream(spec, &device, &config.into(), duration), + //cpal::SampleFormat::I64 => AudioOutput::<i64>::create_stream(spec, &device, &config.into(), duration), + cpal::SampleFormat::U8 => AudioOutput::<u8>::create_stream(spec, &device, &config.into(), duration), + cpal::SampleFormat::U16 => AudioOutput::<u16>::create_stream(spec, &device, &config.into(), duration), + cpal::SampleFormat::U32 => AudioOutput::<u32>::create_stream(spec, &device, &config.into(), duration), + //cpal::SampleFormat::U64 => AudioOutput::<u64>::create_stream(spec, &device, &config.into(), duration), + cpal::SampleFormat::F32 => AudioOutput::<f32>::create_stream(spec, &device, &config.into(), duration), + cpal::SampleFormat::F64 => AudioOutput::<f64>::create_stream(spec, &device, &config.into(), duration), + _ => todo!(), + }; + } + + +impl<T: OutputSample> AudioOutput<T> { + // Creates the stream (TODO: Merge w/open_stream?) + fn create_stream(spec: SignalSpec, device: &cpal::Device, config: &cpal::StreamConfig, duration: Duration) -> Result<Box<dyn AudioStream>> { + let num_channels = config.channels as usize; + + // Ring buffer is created with 200ms audio capacity + let ring_len = ((200 * config.sample_rate.0 as usize) / 1000) * num_channels; + 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, + 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); + }, + //TODO: Handle error here properly + move |err| println!("Yeah we erroring out here"), + 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::<T>::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)) + } + + Ok(Box::new(AudioOutput { ring_buf_producer, sample_buf, stream, resampler})) + } +} + +impl<T: OutputSample> AudioStream for AudioOutput<T> { + // Writes given samples to ring buffer + fn write(&mut self, decoded: AudioBufferRef<'_>) -> Result<()> { + if decoded.frames() == 0 { + return Ok(()); + } + + let mut samples: &[T] = if let Some(resampler) = &mut self.resampler { + // Resamples if required + match resampler.resample(decoded) { + Some(resampled) => resampled, + None => return Ok(()), + } + } else { + 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(); + + 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 new file mode 100644 index 0000000..1996240 --- /dev/null +++ b/src/music_player/music_player.rs @@ -0,0 +1,364 @@ +use std::sync::mpsc::{self, Sender, Receiver}; +use std::thread; +use std::io::SeekFrom; + +use async_std::io::ReadExt; +use async_std::task; + +use symphonia::core::codecs::{CODEC_TYPE_NULL, DecoderOptions, Decoder}; +use symphonia::core::formats::{FormatOptions, FormatReader, SeekMode, SeekTo}; +use symphonia::core::io::{MediaSourceStream, MediaSource}; +use symphonia::core::meta::MetadataOptions; +use symphonia::core::probe::Hint; +use symphonia::core::errors::Error; +use symphonia::core::units::Time; + +use futures::AsyncBufRead; + +use crate::music_player::music_output::AudioStream; +use crate::music_processor::music_processor::MusicProcessor; +use crate::music_storage::music_db::URI; + +// Struct that controls playback of music +pub struct MusicPlayer { + pub music_processor: MusicProcessor, + player_status: PlayerStatus, + message_sender: Option<Sender<PlayerMessage>>, + status_receiver: Option<Receiver<PlayerStatus>>, +} + +#[derive(Clone, Copy)] +pub enum PlayerStatus { + Playing, + Paused, + Stopped, + Error, +} + +pub enum PlayerMessage { + Play, + Pause, + Stop, + SeekTo(u64), + DSP(DSPMessage) +} + +pub enum DSPMessage { + UpdateProcessor(Box<MusicProcessor>) +} + +impl MusicPlayer { + pub fn new() -> Self { + MusicPlayer { + music_processor: MusicProcessor::new(), + player_status: PlayerStatus::Stopped, + message_sender: None, + status_receiver: None, + } + } + + // Opens and plays song with given path in separate thread + pub fn open_song(&mut self, uri: &URI) { + // Creates mpsc channels to communicate with thread + let (message_sender, message_receiver) = mpsc::channel(); + let (status_sender, status_receiver) = mpsc::channel(); + self.message_sender = Some(message_sender); + self.status_receiver = Some(status_receiver); + + let owned_uri = uri.clone(); + + // Creates thread that audio is decoded in + thread::spawn(move || { + let (mut reader, mut decoder) = MusicPlayer::get_reader_and_dec(&owned_uri); + + let mut seek_time: Option<u64> = None; + + let mut audio_output: Option<Box<dyn AudioStream>> = None; + + let mut music_processor = MusicProcessor::new(); + + 'main_decode: loop { + // Handles message received from the MusicPlayer if there is one // TODO: Refactor + let received_message = message_receiver.try_recv(); + match received_message { + Ok(PlayerMessage::Pause) => { + status_sender.send(PlayerStatus::Paused).unwrap(); + // Loops on a blocking message receiver to wait for a play/stop message + 'inner_pause: loop { + let message = message_receiver.try_recv(); + match message { + Ok(PlayerMessage::Play) => { + status_sender.send(PlayerStatus::Playing).unwrap(); + break 'inner_pause + }, + Ok(PlayerMessage::Stop) => { + status_sender.send(PlayerStatus::Stopped).unwrap(); + break 'main_decode + }, + _ => {}, + } + } + }, + // Exits main decode loop and subsequently ends thread (?) + Ok(PlayerMessage::Stop) => { + status_sender.send(PlayerStatus::Stopped).unwrap(); + break 'main_decode + }, + Ok(PlayerMessage::SeekTo(time)) => seek_time = Some(time), + Ok(PlayerMessage::DSP(dsp_message)) => { + match dsp_message { + DSPMessage::UpdateProcessor(new_processor) => music_processor = *new_processor, + } + } + _ => {}, + } + + match seek_time { + Some(time) => { + let seek_to = SeekTo::Time { time: Time::from(time), track_id: Some(0) }; + reader.seek(SeekMode::Accurate, seek_to).unwrap(); + seek_time = None; + } + None => {} //Nothing to do! + } + + let packet = match reader.next_packet() { + Ok(packet) => packet, + Err(Error::ResetRequired) => panic!(), //TODO, + Err(err) => { + //Unrecoverable? + panic!("{}", err); + } + }; + + match 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()); + } + + // 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 + continue; + }, + Err(Error::DecodeError(_)) => { + // may you one day be decoded + continue; + }, + Err(err) => { + // Unrecoverable, though shouldn't panic here + panic!("{}", err); + } + } + } + }); + } + + fn get_reader_and_dec(uri: &URI) -> (Box<dyn FormatReader>, Box<dyn Decoder>) { + // Opens remote/local source and creates MediaSource for symphonia + let config = RemoteOptions { media_buffer_len: 10000, forward_buffer_len: 10000}; + let src: Box<dyn MediaSource> = match uri { + URI::Local(path) => Box::new(std::fs::File::open(path).expect("Failed to open file")), + URI::Remote(_, location) => Box::new(RemoteSource::new(location.as_ref(), &config).unwrap()), + }; + + 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 mut 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 track = reader.tracks() + .iter() + .find(|t| t.codec_params.codec != CODEC_TYPE_NULL) + .expect("no supported audio tracks"); + + let dec_opts: DecoderOptions = Default::default(); + + let mut decoder = symphonia::default::get_codecs().make(&track.codec_params, &dec_opts) + .expect("unsupported codec"); + + return (reader, decoder); + } + + // Updates status by checking on messages from spawned thread + fn update_status(&mut self) { + let status = self.status_receiver.as_mut().unwrap().try_recv(); + if status.is_ok() { + self.player_status = status.unwrap(); + match status.unwrap() { + // Removes receiver and sender since spawned thread no longer exists + PlayerStatus::Stopped => { + self.status_receiver = None; + self.message_sender = None; + } + _ => {} + } + } + } + + // Sends message to spawned thread + pub fn send_message(&mut self, message: PlayerMessage) { + self.update_status(); + // Checks that message sender exists before sending a message off + if self.message_sender.is_some() { + self.message_sender.as_mut().unwrap().send(message).unwrap(); + } + } + + pub fn get_status(&mut self) -> PlayerStatus { + self.update_status(); + return self.player_status; + } +} + +// TODO: Make the buffer length do anything +/// Options for remote sources +/// +/// media_buffer_len is how many bytes are to be buffered in totala +/// +/// forward_buffer is how many bytes can ahead of the seek position without the remote source being read from +pub struct RemoteOptions { + media_buffer_len: u64, + forward_buffer_len: u64, +} + +impl Default for RemoteOptions { + fn default() -> Self { + RemoteOptions { + media_buffer_len: 100000, + forward_buffer_len: 1024, + } + } +} + +/// A remote source of media +struct RemoteSource { + reader: Box<dyn AsyncBufRead + Send + Sync + Unpin>, + media_buffer: Vec<u8>, + forward_buffer_len: u64, + offset: u64, +} + +impl RemoteSource { + /// Creates a new RemoteSource with given uri and configuration + pub fn new(uri: &str, config: &RemoteOptions) -> Result<Self, surf::Error> { + 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(), + forward_buffer_len: config.forward_buffer_len, + offset: 0, + }) + } +} +// TODO: refactor this + buffer into the buffer passed into the function, not a newly allocated one +impl std::io::Read for RemoteSource { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> { + // Reads bytes into the media buffer if the offset is within the specified distance from the end of the buffer + if self.media_buffer.len() as u64 - self.offset < self.forward_buffer_len { + let mut buffer = [0; 1024]; + let read_bytes = task::block_on(async { + match self.reader.read_exact(&mut buffer).await { + 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 + let mut bytes_read = 0; + for location in 0..1024 { + if (location + self.offset as usize) < self.media_buffer.len() { + buf[location] = self.media_buffer[location + self.offset as usize]; + bytes_read += 1; + } + } + + self.offset += bytes_read; + return Ok(bytes_read as usize); + } +} + +impl std::io::Seek for RemoteSource { + // Seeks to a given position + // Seeking past the internal buffer's length results in the seeking to the end of content + fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> { + match pos { + // Offset is set to given position + SeekFrom::Start(pos) => { + 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 { + self.offset = self.media_buffer.len() as u64; + } else { + 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{ + self.offset = self.media_buffer.len() as u64; + } else { + self.offset += pos as u64 + } + return Ok(self.offset); + }, + } + } +} + +impl MediaSource for RemoteSource { + fn is_seekable(&self) -> bool { + return true; + } + + fn byte_len(&self) -> Option<u64> { + return None; + } +} \ No newline at end of file diff --git a/src/music_player/music_resampler.rs b/src/music_player/music_resampler.rs new file mode 100644 index 0000000..f654a17 --- /dev/null +++ b/src/music_player/music_resampler.rs @@ -0,0 +1,147 @@ +// Symphonia +// Copyright (c) 2019-2022 The Project Symphonia Developers. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use symphonia::core::audio::{AudioBuffer, AudioBufferRef, Signal, SignalSpec}; +use symphonia::core::conv::{FromSample, IntoSample}; +use symphonia::core::sample::Sample; + +pub struct Resampler<T> { + resampler: rubato::FftFixedIn<f32>, + input: Vec<Vec<f32>>, + output: Vec<Vec<f32>>, + interleaved: Vec<T>, + duration: usize, +} + +impl<T> Resampler<T> +where + T: Sample + FromSample<f32> + IntoSample<f32>, +{ + fn resample_inner(&mut self) -> &[T] { + { + //let mut input = heapless::Vec::<f32, 32>::new(); + let mut input: arrayvec::ArrayVec<&[f32], 32> = Default::default(); + + for channel in self.input.iter() { + input.push(&channel[..self.duration]); + } + + // Resample. + rubato::Resampler::process_into_buffer( + &mut self.resampler, + &input, + &mut self.output, + None, + ) + .unwrap(); + } + + // Remove consumed samples from the input buffer. + for channel in self.input.iter_mut() { + channel.drain(0..self.duration); + } + + // Interleave the planar samples from Rubato. + let num_channels = self.output.len(); + + 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() { + *s = self.output[ch][i].into_sample(); + } + } + + &self.interleaved + } +} + +impl<T> Resampler<T> +where + T: Sample + FromSample<f32> + IntoSample<f32>, +{ + pub fn new(spec: SignalSpec, to_sample_rate: usize, duration: u64) -> Self { + let duration = duration as usize; + let num_channels = spec.channels.count(); + + let resampler = rubato::FftFixedIn::<f32>::new( + spec.rate as usize, + to_sample_rate, + duration, + 2, + num_channels, + ) + .unwrap(); + + let output = rubato::Resampler::output_buffer_allocate(&resampler); + + let input = vec![Vec::with_capacity(duration); num_channels]; + + Self { resampler, input, output, duration, interleaved: Default::default() } + } + + /// Resamples a planar/non-interleaved input. + /// + /// Returns the resampled samples in an interleaved format. + pub fn resample(&mut self, input: AudioBufferRef<'_>) -> Option<&[T]> { + // Copy and convert samples into input buffer. + convert_samples_any(&input, &mut self.input); + + // Check if more samples are required. + if self.input[0].len() < self.duration { + return None; + } + + Some(self.resample_inner()) + } + + /// Resample any remaining samples in the resample buffer. + pub fn flush(&mut self) -> Option<&[T]> { + let len = self.input[0].len(); + + if len == 0 { + return None; + } + + let partial_len = len % self.duration; + + if partial_len != 0 { + // Fill each input channel buffer with silence to the next multiple of the resampler + // duration. + for channel in self.input.iter_mut() { + channel.resize(len + (self.duration - partial_len), f32::MID); + } + } + + Some(self.resample_inner()) + } +} + +fn convert_samples_any(input: &AudioBufferRef<'_>, output: &mut [Vec<f32>]) { + match input { + AudioBufferRef::U8(input) => convert_samples(input, output), + AudioBufferRef::U16(input) => convert_samples(input, output), + AudioBufferRef::U24(input) => convert_samples(input, output), + AudioBufferRef::U32(input) => convert_samples(input, output), + AudioBufferRef::S8(input) => convert_samples(input, output), + AudioBufferRef::S16(input) => convert_samples(input, output), + AudioBufferRef::S24(input) => convert_samples(input, output), + AudioBufferRef::S32(input) => convert_samples(input, output), + AudioBufferRef::F32(input) => convert_samples(input, output), + AudioBufferRef::F64(input) => convert_samples(input, output), + } +} + +fn convert_samples<S>(input: &AudioBuffer<S>, output: &mut [Vec<f32>]) +where + S: Sample + IntoSample<f32>, +{ + for (c, dst) in output.iter_mut().enumerate() { + 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 new file mode 100644 index 0000000..dd7effc --- /dev/null +++ b/src/music_processor/music_processor.rs @@ -0,0 +1,35 @@ +use symphonia::core::audio::{AudioBuffer, AudioBufferRef, Signal, AsAudioBufferRef, SignalSpec}; + +#[derive(Clone)] +pub struct MusicProcessor { + pub audio_buffer: AudioBuffer<f32>, + pub audio_volume: f32, +} + +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); + } +} \ No newline at end of file diff --git a/src/music_storage/music_db.rs b/src/music_storage/music_db.rs new file mode 100644 index 0000000..72d3053 --- /dev/null +++ b/src/music_storage/music_db.rs @@ -0,0 +1,456 @@ +use file_format::{FileFormat, Kind}; +use serde::Deserialize; +use lofty::{Accessor, AudioFile, Probe, TaggedFileExt, ItemKey, ItemValue, TagType}; +use rusqlite::{params, Connection}; +use cue::{cd_text::PTI, cd::CD}; +use std::fs; +use std::path::{Path, PathBuf}; +use std::time::Duration; +use time::Date; +use walkdir::WalkDir; + +use crate::music_controller::config::Config; + +#[derive(Debug)] +pub struct Song { + pub path: Box<Path>, + pub title: Option<String>, + pub album: Option<String>, + tracknum: Option<usize>, + pub artist: Option<String>, + date: Option<Date>, + genre: Option<String>, + plays: Option<usize>, + favorited: Option<bool>, + format: Option<FileFormat>, // TODO: Make this a proper FileFormat eventually + duration: Option<Duration>, + pub custom_tags: Option<Vec<Tag>>, +} +#[derive(Clone)] +pub enum URI{ + Local(String), + Remote(Service, String), +} + +#[derive(Clone, Copy)] +pub enum Service { + InternetRadio, + Spotify, + Youtube, +} + +#[derive(Debug)] +pub struct Playlist { + title: String, + cover_art: Box<Path>, +} + +pub fn create_db() -> Result<(), rusqlite::Error> { + let path = "./music_database.db3"; + let db_connection = Connection::open(path)?; + + db_connection.pragma_update(None, "synchronous", "0")?; + db_connection.pragma_update(None, "journal_mode", "WAL")?; + + // 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(()) +} + +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::<usize, bool>(0).unwrap(), + None => false + } +} + +/// Parse a cuesheet given a path and a directory it is located in, +/// returning a Vec of Song objects +fn parse_cuesheet( + cuesheet_path: &Path, + current_dir: &PathBuf +) -> Result<Vec<Song>, Box<dyn std::error::Error>>{ + let cuesheet = CD::parse_file(cuesheet_path.to_path_buf())?; + + let album = cuesheet.get_cdtext().read(PTI::Title); + + let mut song_list:Vec<Song> = vec![]; + + for (index, track) in cuesheet.tracks().iter().enumerate() { + let track_string_path = format!("{}/{}", current_dir.to_string_lossy(), track.get_filename()); + let track_path = Path::new(&track_string_path); + + if !track_path.exists() {continue}; + + // Get the format as a string + let short_format = match FileFormat::from_file(track_path) { + Ok(fmt) => Some(fmt), + Err(_) => None + }; + + let duration = Duration::from_secs(track.get_length().unwrap_or(-1) as u64); + + let custom_index_start = Tag::Custom{ + tag: String::from("dango_cue_index_start"), + tag_value: track.get_index(0).unwrap_or(-1).to_string() + }; + let custom_index_end = Tag::Custom{ + tag: String::from("dango_cue_index_end"), + tag_value: track.get_index(0).unwrap_or(-1).to_string() + }; + + let custom_tags: Vec<Tag> = vec![custom_index_start, custom_index_end]; + + let tags = track.get_cdtext(); + let cue_song = Song { + path: track_path.into(), + title: tags.read(PTI::Title), + album: album.clone(), + tracknum: Some(index + 1), + artist: tags.read(PTI::Performer), + date: None, + genre: tags.read(PTI::Genre), + plays: Some(0), + favorited: Some(false), + format: short_format, + duration: Some(duration), + custom_tags: Some(custom_tags) + }; + + song_list.push(cue_song); + } + + Ok(song_list) +} + +pub fn find_all_music( + config: &Config, + target_path: &str, +) -> Result<(), Box<dyn std::error::Error>> { + 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()) { + 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(), &db_connection) + } else if extension.to_ascii_lowercase() == "cue" { + // TODO: implement cuesheet support + parse_cuesheet(target_file.path(), ¤t_dir); + } + } + + // 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) { + // 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 item in tag.items() { + 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(", "); + } + + custom_insert.push_str(&format!(" (?1, '{}', '{}')", custom_key.replace("\'", "''"), custom_value.replace("\'", "''"))); + + loops += 1; + } + + // Get the format as a string + let short_format: Option<String> = 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(); + + // 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)] +pub enum Tag { + SongPath, + Title, + Album, + TrackNum, + Artist, + Date, + Genre, + Plays, + Favorited, + Format, + Duration, + Custom{tag: String, tag_value: String}, +} + +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)] +pub enum MusicObject { + Song(Song), + Album(Playlist), + Playlist(Playlist), +} + +impl MusicObject { + pub fn as_song(&self) -> Option<&Song> { + match self { + MusicObject::Song(data) => Some(data), + _ => None + } + } +} + +/// 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>, +) -> Option<Vec<MusicObject>> { + 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<MusicObject> = vec![]; + + while let Some(row) = rows.next().unwrap() { + let custom_tags: Vec<Tag> = match row.get::<usize, String>(11) { + Ok(result) => serde_json::from_str(&result).unwrap_or(vec![]), + Err(_) => vec![] + }; + + let file_format: FileFormat = FileFormat::from(row.get::<usize, String>(9).unwrap().as_bytes()); + + let new_song = Song { + // TODO: Implement proper errors here + path: Path::new(&row.get::<usize, String>(0).unwrap_or("".to_owned())).into(), + title: row.get::<usize, String>(1).ok(), + album: row.get::<usize, String>(2).ok(), + tracknum: row.get::<usize, usize>(3).ok(), + artist: row.get::<usize, String>(4).ok(), + date: Date::from_calendar_date(row.get::<usize, i32>(5).unwrap_or(0), time::Month::January, 1).ok(), // TODO: Fix this to get the actual date + genre: row.get::<usize, String>(6).ok(), + plays: row.get::<usize, usize>(7).ok(), + favorited: row.get::<usize, bool>(8).ok(), + format: Some(file_format), + duration: Some(Duration::from_secs(row.get::<usize, u64>(10).unwrap_or(0))), + custom_tags: Some(custom_tags), + }; + + final_result.push(MusicObject::Song(new_song)); + }; + + Some(final_result) +} diff --git a/src/music_storage/playlist.rs b/src/music_storage/playlist.rs new file mode 100644 index 0000000..f6fc9b7 --- /dev/null +++ b/src/music_storage/playlist.rs @@ -0,0 +1,27 @@ +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(); + } +} diff --git a/src/music_tracker/music_tracker.rs b/src/music_tracker/music_tracker.rs new file mode 100644 index 0000000..93fb5fb --- /dev/null +++ b/src/music_tracker/music_tracker.rs @@ -0,0 +1,186 @@ +use std::time::{SystemTime, UNIX_EPOCH}; +use std::collections::BTreeMap; + +use async_trait::async_trait; +use serde::{Serialize, Deserialize}; +use md5::{Md5, Digest}; + +#[async_trait] +pub trait MusicTracker { + /// Adds one listen to a song halfway through playback + async fn track_song(&self, song: &String) -> Result<(), surf::Error>; + + /// Adds a 'listening' status to the music tracker service of choice + async fn track_now(&self, song: &String) -> Result<(), surf::Error>; + + /// Reads config files, and attempts authentication with service + async fn test_tracker(&self) -> Result<(), surf::Error>; + + /// Returns plays for a given song according to tracker service + async fn get_times_tracked(&self, song: &String) -> Result<u32, surf::Error>; +} + +#[derive(Serialize, Deserialize)] +pub struct LastFM { + dango_api_key: String, + auth_token: Option<String>, + shared_secret: Option<String>, + session_key: Option<String>, +} + +#[async_trait] +impl MusicTracker for LastFM { + async fn track_song(&self, song: &String) -> Result<(), surf::Error> { + 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 string_timestamp = timestamp.to_string(); + params.insert("method", "track.scrobble"); + params.insert("artist", "Kikuo"); + params.insert("track", "A Happy Death - Again"); + params.insert("timestamp", &string_timestamp); + + self.api_request(params).await?; + Ok(()) + } + + async fn track_now(&self, song: &String) -> Result<(), surf::Error> { + let mut params: BTreeMap<&str, &str> = BTreeMap::new(); + params.insert("method", "track.updateNowPlaying"); + params.insert("artist", "Kikuo"); + params.insert("track", "A Happy Death - Again"); + self.api_request(params).await?; + Ok(()) + } + + async fn test_tracker(&self) -> Result<(), surf::Error> { + let mut params: BTreeMap<&str, &str> = BTreeMap::new(); + params.insert("method", "chart.getTopArtists"); + self.api_request(params).await?; + Ok(()) + } + + async fn get_times_tracked(&self, song: &String) -> Result<u32, surf::Error> { + todo!(); + } +} + +#[derive(Deserialize, Serialize)] +struct AuthToken { + token: String +} + +#[derive(Deserialize, Serialize, Debug)] +struct SessionResponse { + name: String, + key: String, + subscriber: i32, +} + +#[derive(Deserialize, Serialize, Debug)] +struct Session { + session: SessionResponse +} + +impl LastFM { + // Returns a url to be accessed by the user + pub async fn get_auth_url(&mut self) -> Result<String, surf::Error> { + let method = String::from("auth.gettoken"); + let api_key = self.dango_api_key.clone(); + 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?; + self.auth_token = Some(auth_token.token.clone()); + + let auth_url = format!("http://www.last.fm/api/auth/?api_key={api_key}&token={}", auth_token.token); + + return Ok(auth_url); + } + + pub async fn set_session(&mut self) { + let method = String::from("auth.getSession"); + let api_key = self.dango_api_key.clone(); + let auth_token = self.auth_token.clone().unwrap(); + let shared_secret = self.shared_secret.clone().unwrap(); + + // Creates api_sig as defined in last.fm documentation + 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.unwrap(); + + // Sets session key from received response + let session_response: Session = serde_json::from_str(&response).unwrap(); + self.session_key = Some(session_response.session.key.clone()); + } + + // Creates a new LastFM struct + pub fn new() -> LastFM { + let last_fm = LastFM { + // Grab this from config in future + dango_api_key: String::from("29a071e3113ab8ed36f069a2d3e20593"), + auth_token: None, + // Also grab from config in future + shared_secret: Some(String::from("5400c554430de5c5002d5e4bcc295b3d")), + session_key: None, + }; + return last_fm; + } + + // Creates an api request with the given parameters + pub async fn api_request(&self, mut params: BTreeMap<&str, &str>) -> Result<surf::Response, surf::Error> { + params.insert("api_key", &self.dango_api_key); + params.insert("sk", &self.session_key.as_ref().unwrap()); + + // Creates and sets api call signature + let api_sig = LastFM::request_sig(¶ms, &self.shared_secret.as_ref().unwrap()); + params.insert("api_sig", &api_sig); + let mut string_params = String::from(""); + + // Creates method call string + 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; + } + + // Returns an api signature as defined in the last.fm api documentation + fn request_sig(params: &BTreeMap<&str, &str>, shared_secret: &str) -> String { + let mut sig_string = String::new(); + // Appends keys and values of parameters to the unhashed sig + for key in params.keys() { + let param_value = params.get(*key); + 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!(); + } +} \ No newline at end of file From c8f16f6b3581f8a710a417de7964ced14feb8b72 Mon Sep 17 00:00:00 2001 From: G2-Games <ke0bhogsg@gmail.com> Date: Fri, 19 Jan 2024 17:49:05 -0600 Subject: [PATCH 022/136] Added preliminary Discord RPC support, updated configuration and error handling for music trackers. Closes #2 --- Cargo.toml | 1 + src/music_controller/config.rs | 38 +++++-- src/music_tracker/music_tracker.rs | 174 +++++++++++++++++++++++------ 3 files changed, 168 insertions(+), 45 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4a63b78..b312946 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,3 +32,4 @@ surf = "2.3.2" futures = "0.3.28" rubato = "0.12.0" arrayvec = "0.7.4" +discord-presence = "0.5.18" diff --git a/src/music_controller/config.rs b/src/music_controller/config.rs index 70ee273..d018088 100644 --- a/src/music_controller/config.rs +++ b/src/music_controller/config.rs @@ -4,23 +4,43 @@ use std::fs; use serde::{Deserialize, Serialize}; -use crate::music_tracker::music_tracker::LastFM; +use crate::music_tracker::music_tracker::{LastFM, LastFMConfig, DiscordRPCConfig}; #[derive(Serialize, Deserialize)] pub struct Config { pub db_path: Box<PathBuf>, - pub lastfm: Option<LastFM>, + pub lastfm: Option<LastFMConfig>, + pub discord: Option<DiscordRPCConfig>, +} + +impl Default for Config { + fn default() -> Self { + let path = PathBuf::from("./music_database.db3"); + + return Config { + db_path: Box::new(path), + + lastfm: Some (LastFMConfig { + enabled: true, + dango_api_key: String::from("29a071e3113ab8ed36f069a2d3e20593"), + auth_token: None, + shared_secret: Some(String::from("5400c554430de5c5002d5e4bcc295b3d")), + session_key: None, + }), + + discord: Some(DiscordRPCConfig { + enabled: true, + dango_client_id: 1144475145864499240, + dango_icon: String::from("flat"), + }), + }; + } } impl Config { // Creates and saves a new config with default values - pub fn new(config_file: &PathBuf) -> std::io::Result<Config> { - let path = PathBuf::from("./music_database.db3"); - - let config = Config { - db_path: Box::new(path), - lastfm: None, - }; + pub fn new(config_file: &PathBuf) -> std::io::Result<Config> { + let config = Config::default(); config.save(config_file)?; Ok(config) diff --git a/src/music_tracker/music_tracker.rs b/src/music_tracker/music_tracker.rs index 93fb5fb..9568d85 100644 --- a/src/music_tracker/music_tracker.rs +++ b/src/music_tracker/music_tracker.rs @@ -2,35 +2,72 @@ use std::time::{SystemTime, UNIX_EPOCH}; use std::collections::BTreeMap; use async_trait::async_trait; -use serde::{Serialize, Deserialize}; +use serde::{Serialize, Deserialize, Serializer}; use md5::{Md5, Digest}; +use discord_presence::{ Event, DiscordError}; +use surf::StatusCode; #[async_trait] pub trait MusicTracker { /// Adds one listen to a song halfway through playback - async fn track_song(&self, song: &String) -> Result<(), surf::Error>; + async fn track_song(&mut self, song: &String) -> Result<(), TrackerError>; /// Adds a 'listening' status to the music tracker service of choice - async fn track_now(&self, song: &String) -> Result<(), surf::Error>; + async fn track_now(&mut self, song: &String) -> Result<(), TrackerError>; /// Reads config files, and attempts authentication with service - async fn test_tracker(&self) -> Result<(), surf::Error>; + async fn test_tracker(&mut self) -> Result<(), TrackerError>; /// Returns plays for a given song according to tracker service - async fn get_times_tracked(&self, song: &String) -> Result<u32, surf::Error>; + async fn get_times_tracked(&mut self, song: &String) -> Result<u32, TrackerError>; +} + +#[derive(Debug)] +pub enum TrackerError { + /// Tracker does not accept the song's format/content + InvalidSong, + /// Tracker requires authentication + InvalidAuth, + /// Tracker request was malformed + InvalidRequest, + /// Tracker is unavailable + ServiceUnavailable, + /// Unknown tracker error + Unknown, +} + + +impl TrackerError { + pub fn from_surf_error(error: surf::Error) -> TrackerError { + return match error.status() { + StatusCode::Forbidden => TrackerError::InvalidAuth, + StatusCode::Unauthorized => TrackerError::InvalidAuth, + StatusCode::NetworkAuthenticationRequired => TrackerError::InvalidAuth, + StatusCode::BadRequest => TrackerError::InvalidRequest, + StatusCode::BadGateway => TrackerError::ServiceUnavailable, + StatusCode::ServiceUnavailable => TrackerError::ServiceUnavailable, + StatusCode::NotFound => TrackerError::ServiceUnavailable, + _ => TrackerError::Unknown, + } + } +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct LastFMConfig { + pub enabled: bool, + pub dango_api_key: String, + pub auth_token: Option<String>, + pub shared_secret: Option<String>, + pub session_key: Option<String>, } -#[derive(Serialize, Deserialize)] pub struct LastFM { - dango_api_key: String, - auth_token: Option<String>, - shared_secret: Option<String>, - session_key: Option<String>, + config: LastFMConfig } #[async_trait] impl MusicTracker for LastFM { - async fn track_song(&self, song: &String) -> Result<(), surf::Error> { + async fn track_song(&mut self, song: &String) -> Result<(), TrackerError> { let mut params: BTreeMap<&str, &str> = BTreeMap::new(); // Sets timestamp of song beginning play time @@ -41,27 +78,35 @@ impl MusicTracker for LastFM { params.insert("track", "A Happy Death - Again"); params.insert("timestamp", &string_timestamp); - self.api_request(params).await?; - Ok(()) + return match self.api_request(params).await { + Ok(_) => Ok(()), + Err(err) => Err(TrackerError::from_surf_error(err)), + } } - async fn track_now(&self, song: &String) -> Result<(), surf::Error> { + async fn track_now(&mut self, song: &String) -> Result<(), TrackerError> { let mut params: BTreeMap<&str, &str> = BTreeMap::new(); params.insert("method", "track.updateNowPlaying"); params.insert("artist", "Kikuo"); params.insert("track", "A Happy Death - Again"); - self.api_request(params).await?; - Ok(()) + + return match self.api_request(params).await { + Ok(_) => Ok(()), + Err(err) => Err(TrackerError::from_surf_error(err)), + } } - async fn test_tracker(&self) -> Result<(), surf::Error> { + async fn test_tracker(&mut self) -> Result<(), TrackerError> { let mut params: BTreeMap<&str, &str> = BTreeMap::new(); params.insert("method", "chart.getTopArtists"); - self.api_request(params).await?; - Ok(()) + + return match self.api_request(params).await { + Ok(_) => Ok(()), + Err(err) => Err(TrackerError::from_surf_error(err)), + } } - async fn get_times_tracked(&self, song: &String) -> Result<u32, surf::Error> { + async fn get_times_tracked(&mut self, song: &String) -> Result<u32, TrackerError> { todo!(); } } @@ -87,11 +132,11 @@ impl LastFM { // Returns a url to be accessed by the user pub async fn get_auth_url(&mut self) -> Result<String, surf::Error> { let method = String::from("auth.gettoken"); - let api_key = self.dango_api_key.clone(); + let api_key = self.config.dango_api_key.clone(); 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?; - self.auth_token = Some(auth_token.token.clone()); + self.config.auth_token = Some(auth_token.token.clone()); let auth_url = format!("http://www.last.fm/api/auth/?api_key={api_key}&token={}", auth_token.token); @@ -100,9 +145,9 @@ impl LastFM { pub async fn set_session(&mut self) { let method = String::from("auth.getSession"); - let api_key = self.dango_api_key.clone(); - let auth_token = self.auth_token.clone().unwrap(); - let shared_secret = self.shared_secret.clone().unwrap(); + let api_key = self.config.dango_api_key.clone(); + let auth_token = self.config.auth_token.clone().unwrap(); + let shared_secret = self.config.shared_secret.clone().unwrap(); // Creates api_sig as defined in last.fm documentation let api_sig = format!("api_key{api_key}methodauth.getSessiontoken{auth_token}{shared_secret}"); @@ -119,33 +164,29 @@ impl LastFM { // Sets session key from received response let session_response: Session = serde_json::from_str(&response).unwrap(); - self.session_key = Some(session_response.session.key.clone()); + self.config.session_key = Some(session_response.session.key.clone()); } // Creates a new LastFM struct - pub fn new() -> LastFM { + pub fn new(config: &LastFMConfig) -> LastFM { let last_fm = LastFM { - // Grab this from config in future - dango_api_key: String::from("29a071e3113ab8ed36f069a2d3e20593"), - auth_token: None, - // Also grab from config in future - shared_secret: Some(String::from("5400c554430de5c5002d5e4bcc295b3d")), - session_key: None, + 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<surf::Response, surf::Error> { - params.insert("api_key", &self.dango_api_key); - params.insert("sk", &self.session_key.as_ref().unwrap()); + params.insert("api_key", &self.config.dango_api_key); + params.insert("sk", &self.config.session_key.as_ref().unwrap()); // Creates and sets api call signature - let api_sig = LastFM::request_sig(¶ms, &self.shared_secret.as_ref().unwrap()); + let api_sig = LastFM::request_sig(¶ms, &self.config.shared_secret.as_ref().unwrap()); 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}&")); @@ -183,4 +224,65 @@ impl LastFM { pub fn reset_account() { todo!(); } +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct DiscordRPCConfig { + pub enabled: bool, + pub dango_client_id: u64, + pub dango_icon: String, +} + +pub struct DiscordRPC { + config: DiscordRPCConfig, + pub client: discord_presence::client::Client +} + +impl DiscordRPC { + pub fn new(config: &DiscordRPCConfig) -> Self { + let rpc = DiscordRPC { + client: discord_presence::client::Client::new(config.dango_client_id), + config: config.clone(), + }; + return rpc; + } +} + +#[async_trait] +impl MusicTracker for DiscordRPC { + async fn track_now(&mut self, song: &String) -> Result<(), TrackerError> { + 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; + // Sets discord account activity to current playing song + let send_activity = self.client.set_activity(|activity| { + activity + .state(song) + .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: &String) -> Result<(), TrackerError> { + return Ok(()) + } + + async fn test_tracker(&mut self) -> Result<(), TrackerError> { + return Ok(()) + } + + async fn get_times_tracked(&mut self, song: &String) -> Result<u32, TrackerError> { + return Ok(0); + } } \ No newline at end of file From 14edbe7c465af3d6e64a477f8f1d28b08239ba01 Mon Sep 17 00:00:00 2001 From: G2-Games <ke0bhogsg@gmail.com> Date: Fri, 19 Jan 2024 18:22:36 -0600 Subject: [PATCH 023/136] Refactored music_player, integrated music_tracker into it. Closes #7 --- src/music_controller/config.rs | 10 +- src/music_controller/music_controller.rs | 33 +- src/music_player/music_player.rs | 443 +++++++++++++++-------- src/music_processor/music_processor.rs | 8 + src/music_storage/music_db.rs | 31 +- src/music_tracker/music_tracker.rs | 83 +++-- 6 files changed, 378 insertions(+), 230 deletions(-) diff --git a/src/music_controller/config.rs b/src/music_controller/config.rs index d018088..c77a02a 100644 --- a/src/music_controller/config.rs +++ b/src/music_controller/config.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; use crate::music_tracker::music_tracker::{LastFM, LastFMConfig, DiscordRPCConfig}; -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, PartialEq, Eq)] pub struct Config { pub db_path: Box<PathBuf>, pub lastfm: Option<LastFMConfig>, @@ -20,13 +20,7 @@ impl Default for Config { return Config { db_path: Box::new(path), - lastfm: Some (LastFMConfig { - enabled: true, - dango_api_key: String::from("29a071e3113ab8ed36f069a2d3e20593"), - auth_token: None, - shared_secret: Some(String::from("5400c554430de5c5002d5e4bcc295b3d")), - session_key: None, - }), + lastfm: None, discord: Some(DiscordRPCConfig { enabled: true, diff --git a/src/music_controller/music_controller.rs b/src/music_controller/music_controller.rs index bb1ab66..29f4fb7 100644 --- a/src/music_controller/music_controller.rs +++ b/src/music_controller/music_controller.rs @@ -1,22 +1,23 @@ use std::path::PathBuf; +use std::sync::{RwLock, Arc, Mutex}; use rusqlite::Result; use crate::music_controller::config::Config; -use crate::music_player::music_player::{MusicPlayer, PlayerStatus, PlayerMessage, DSPMessage}; +use crate::music_player::music_player::{MusicPlayer, PlayerStatus, DecoderMessage, DSPMessage}; use crate::music_processor::music_processor::MusicProcessor; -use crate::music_storage::music_db::URI; +use crate::music_storage::music_db::{URI, Song}; pub struct MusicController { - pub config: Config, + pub config: Arc<RwLock<Config>>, music_player: MusicPlayer, } impl MusicController { /// Creates new MusicController with config at given path pub fn new(config_path: &PathBuf) -> Result<MusicController, std::io::Error>{ - let config = Config::new(config_path)?; - let music_player = MusicPlayer::new(); + let config = Arc::new(RwLock::new(Config::new(config_path)?)); + let music_player = MusicPlayer::new(config.clone()); let controller = MusicController { config, @@ -28,8 +29,8 @@ impl MusicController { /// Creates new music controller from a config at given path pub fn from(config_path: &PathBuf) -> std::result::Result<MusicController, toml::de::Error> { - let config = Config::from(config_path)?; - let music_player = MusicPlayer::new(); + let config = Arc::new(RwLock::new(Config::from(config_path)?)); + let music_player = MusicPlayer::new(config.clone()); let controller = MusicController { config, @@ -39,13 +40,8 @@ impl MusicController { return Ok(controller) } - /// Opens and plays song at given URI - pub fn open_song(&mut self, uri: &URI) { - self.music_player.open_song(uri); - } - - /// Sends given message to music player - pub fn song_control(&mut self, message: PlayerMessage) { + /// Sends given message to control music player + pub fn song_control(&mut self, message: DecoderMessage) { self.music_player.send_message(message); } @@ -54,15 +50,20 @@ impl MusicController { return self.music_player.get_status(); } + /// Gets current song being controlled, if any + pub fn get_current_song(&self) -> Option<Song> { + return self.music_player.get_current_song(); + } + /// Gets audio playback volume - pub fn get_vol(&mut self) -> f32 { + 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(PlayerMessage::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())))); } } diff --git a/src/music_player/music_player.rs b/src/music_player/music_player.rs index 1996240..2944788 100644 --- a/src/music_player/music_player.rs +++ b/src/music_player/music_player.rs @@ -1,41 +1,50 @@ use std::sync::mpsc::{self, Sender, Receiver}; +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::formats::{FormatOptions, FormatReader, SeekMode, SeekTo}; use symphonia::core::io::{MediaSourceStream, MediaSource}; use symphonia::core::meta::MetadataOptions; use symphonia::core::probe::Hint; use symphonia::core::errors::Error; -use symphonia::core::units::Time; +use symphonia::core::units::{Time, TimeBase}; 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; +use crate::music_storage::music_db::{URI, Song}; +use crate::music_tracker::music_tracker::{MusicTracker, LastFM, DiscordRPCConfig, DiscordRPC, TrackerError}; // Struct that controls playback of music pub struct MusicPlayer { pub music_processor: MusicProcessor, player_status: PlayerStatus, - message_sender: Option<Sender<PlayerMessage>>, - status_receiver: Option<Receiver<PlayerStatus>>, + music_trackers: Vec<Box<dyn MusicTracker + Send>>, + current_song: Arc<RwLock<Option<Song>>>, + message_sender: Sender<DecoderMessage>, + status_receiver: Receiver<PlayerStatus>, + config: Arc<RwLock<Config>>, } -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug)] pub enum PlayerStatus { - Playing, + Playing(f64), Paused, Stopped, Error, } -pub enum PlayerMessage { +#[derive(Debug, Clone)] +pub enum DecoderMessage { + OpenSong(Song), Play, Pause, Stop, @@ -43,143 +52,43 @@ pub enum PlayerMessage { DSP(DSPMessage) } +#[derive(Clone)] +pub enum TrackerMessage { + Track(Song), + TrackNow(Song) +} + +#[derive(Debug, Clone)] pub enum DSPMessage { UpdateProcessor(Box<MusicProcessor>) } -impl MusicPlayer { - pub fn new() -> Self { - MusicPlayer { - music_processor: MusicProcessor::new(), - player_status: PlayerStatus::Stopped, - message_sender: None, - status_receiver: None, - } - } - - // Opens and plays song with given path in separate thread - pub fn open_song(&mut self, uri: &URI) { - // Creates mpsc channels to communicate with thread - let (message_sender, message_receiver) = mpsc::channel(); - let (status_sender, status_receiver) = mpsc::channel(); - self.message_sender = Some(message_sender); - self.status_receiver = Some(status_receiver); - - let owned_uri = uri.clone(); +// Holds a song decoder reader, etc +struct SongHandler { + pub reader: Box<dyn FormatReader>, + pub decoder: Box<dyn Decoder>, + pub time_base: Option<TimeBase>, + pub duration: Option<u64>, +} - // Creates thread that audio is decoded in - thread::spawn(move || { - let (mut reader, mut decoder) = MusicPlayer::get_reader_and_dec(&owned_uri); - - let mut seek_time: Option<u64> = None; - - let mut audio_output: Option<Box<dyn AudioStream>> = None; - - let mut music_processor = MusicProcessor::new(); - - 'main_decode: loop { - // Handles message received from the MusicPlayer if there is one // TODO: Refactor - let received_message = message_receiver.try_recv(); - match received_message { - Ok(PlayerMessage::Pause) => { - status_sender.send(PlayerStatus::Paused).unwrap(); - // Loops on a blocking message receiver to wait for a play/stop message - 'inner_pause: loop { - let message = message_receiver.try_recv(); - match message { - Ok(PlayerMessage::Play) => { - status_sender.send(PlayerStatus::Playing).unwrap(); - break 'inner_pause - }, - Ok(PlayerMessage::Stop) => { - status_sender.send(PlayerStatus::Stopped).unwrap(); - break 'main_decode - }, - _ => {}, - } - } - }, - // Exits main decode loop and subsequently ends thread (?) - Ok(PlayerMessage::Stop) => { - status_sender.send(PlayerStatus::Stopped).unwrap(); - break 'main_decode - }, - Ok(PlayerMessage::SeekTo(time)) => seek_time = Some(time), - Ok(PlayerMessage::DSP(dsp_message)) => { - match dsp_message { - DSPMessage::UpdateProcessor(new_processor) => music_processor = *new_processor, - } - } - _ => {}, - } - - match seek_time { - Some(time) => { - let seek_to = SeekTo::Time { time: Time::from(time), track_id: Some(0) }; - reader.seek(SeekMode::Accurate, seek_to).unwrap(); - seek_time = None; - } - None => {} //Nothing to do! - } - - let packet = match reader.next_packet() { - Ok(packet) => packet, - Err(Error::ResetRequired) => panic!(), //TODO, - Err(err) => { - //Unrecoverable? - panic!("{}", err); - } - }; - - match 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()); - } - - // 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 - continue; - }, - Err(Error::DecodeError(_)) => { - // may you one day be decoded - continue; - }, - Err(err) => { - // Unrecoverable, though shouldn't panic here - panic!("{}", err); - } - } - } - }); - } - - fn get_reader_and_dec(uri: &URI) -> (Box<dyn FormatReader>, Box<dyn Decoder>) { +// TODO: actual error handling here +impl SongHandler { + pub fn new(uri: &URI) -> Result<Self, ()> { // 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<dyn MediaSource> = match uri { - URI::Local(path) => Box::new(std::fs::File::open(path).expect("Failed to open file")), - URI::Remote(_, location) => Box::new(RemoteSource::new(location.as_ref(), &config).unwrap()), + URI::Local(path) => { + match std::fs::File::open(path) { + Ok(file) => Box::new(file), + Err(_) => return Err(()), + } + }, + URI::Remote(_, location) => { + match RemoteSource::new(location.as_ref(), &config) { + Ok(remote_source) => Box::new(remote_source), + Err(_) => return Err(()), + } + }, }; let mss = MediaSourceStream::new(src, Default::default()); @@ -198,42 +107,262 @@ impl MusicPlayer { .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 mut decoder = symphonia::default::get_codecs().make(&track.codec_params, &dec_opts) .expect("unsupported codec"); - return (reader, decoder); + return Ok(SongHandler {reader, decoder, time_base, duration}); + } +} + +impl MusicPlayer { + pub fn new(config: Arc<RwLock<Config>>) -> Self { + // Creates mpsc channels to communicate with music player threads + 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 { + music_processor: MusicProcessor::new(), + music_trackers: Vec::new(), + player_status: PlayerStatus::Stopped, + current_song, + message_sender, + status_receiver, + config, + } + } + + fn start_tracker(status_sender: Sender<Result<(), TrackerError>>, tracker_receiver: Receiver<TrackerMessage>, config: Arc<RwLock<Config>>) { + 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<Box<dyn MusicTracker>> = Vec::new(); + // Updates local trackers to the music controller config + let update_trackers = |trackers: &mut Vec<Box<dyn MusicTracker>>|{ + if let Some(lastfm_config) = global_config.lastfm.clone() { + trackers.push(Box::new(LastFM::new(&lastfm_config))); + } + if let Some(discord_config) = global_config.discord.clone() { + trackers.push(Box::new(DiscordRPC::new(&discord_config))); + } + }; + update_trackers(&mut trackers); + loop { + if let message = tracker_receiver.recv() { + if local_config != global_config { + update_trackers(&mut trackers); + } + + let mut results = Vec::new(); + task::block_on(async { + 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(_) => {}, + } + } + results = join_all(futures).await; + }); + + for result in results { + status_sender.send(result).unwrap_or_default() + } + } + } + }); + } + + // Opens and plays song with given path in separate thread + fn start_player(message_receiver: Receiver<DecoderMessage>, status_sender: Sender<PlayerStatus>, config: Arc<RwLock<Config>>, current_song: Arc<RwLock<Option<Song>>>) { + // 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<u64> = None; + + let mut audio_output: Option<Box<dyn AudioStream>> = None; + + let mut music_processor = MusicProcessor::new(); + + let (tracker_sender, tracker_receiver): (Sender<TrackerMessage>, Receiver<TrackerMessage>) = mpsc::channel(); + let (tracker_status_sender, tracker_status_receiver): (Sender<Result<(), TrackerError>>, Receiver<Result<(), TrackerError>>) = 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 { + 'handle_message: loop { + let message = if paused { + // Pauses playback by blocking on waiting for new player messages + match message_receiver.recv() { + Ok(message) => Some(message), + Err(_) => None, + } + } else { + // Resumes playback by not blocking + match message_receiver.try_recv() { + Ok(message) => Some(message), + Err(_) => break 'handle_message, + } + }; + // Handles message received from MusicPlayer struct + match message { + Some(DecoderMessage::OpenSong(song)) => { + let song_uri = song.path.clone(); + match SongHandler::new(&song_uri) { + Ok(new_handler) => { + song_handler = Some(new_handler); + *current_song.write().unwrap() = Some(song); + paused = false; + song_tracked = false; + } + Err(_) => status_sender.send(PlayerStatus::Error).unwrap(), + } + } + Some(DecoderMessage::Play) => { + if song_handler.is_some() { + paused = false; + } + } + Some(DecoderMessage::Pause) => { + paused = true; + 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(); + break 'main_decode + } + 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()) { + 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(); + seek_time = None; + } + None => {} //Nothing to do! + } + let packet = match song_handler.reader.next_packet() { + Ok(packet) => packet, + Err(Error::ResetRequired) => panic!(), //TODO, + Err(err) => { + // Unrecoverable? + 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(); + } + + 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; + // 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 { + song_tracked = true; + tracker_sender.send(TrackerMessage::Track(song.clone())).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()); + } + // 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 + continue; + }, + Err(Error::DecodeError(_)) => { + // may you one day be decoded + continue; + }, + Err(err) => { + // Unrecoverable, though shouldn't panic here + panic!("{}", err); + } + } + } + } + }); } // Updates status by checking on messages from spawned thread - fn update_status(&mut self) { - let status = self.status_receiver.as_mut().unwrap().try_recv(); - if status.is_ok() { - self.player_status = status.unwrap(); - match status.unwrap() { - // Removes receiver and sender since spawned thread no longer exists - PlayerStatus::Stopped => { - self.status_receiver = None; - self.message_sender = None; - } - _ => {} - } + fn update_player(&mut self) { + for message in self.status_receiver.try_recv() { + self.player_status = message; + } + } + + pub fn get_current_song(&self) -> Option<Song>{ + 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: PlayerMessage) { - self.update_status(); + pub fn send_message(&mut self, message: DecoderMessage) { + self.update_player(); // Checks that message sender exists before sending a message off - if self.message_sender.is_some() { - self.message_sender.as_mut().unwrap().send(message).unwrap(); - } + self.message_sender.send(message).unwrap(); } pub fn get_status(&mut self) -> PlayerStatus { - self.update_status(); + self.update_player(); return self.player_status; } } @@ -361,4 +490,4 @@ impl MediaSource for RemoteSource { fn byte_len(&self) -> Option<u64> { return None; } -} \ No newline at end of file +} diff --git a/src/music_processor/music_processor.rs b/src/music_processor/music_processor.rs index dd7effc..e6d6608 100644 --- a/src/music_processor/music_processor.rs +++ b/src/music_processor/music_processor.rs @@ -1,3 +1,5 @@ +use std::fmt::Debug; + use symphonia::core::audio::{AudioBuffer, AudioBufferRef, Signal, AsAudioBufferRef, SignalSpec}; #[derive(Clone)] @@ -6,6 +8,12 @@ pub struct MusicProcessor { 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 { diff --git a/src/music_storage/music_db.rs b/src/music_storage/music_db.rs index 72d3053..7ed59fe 100644 --- a/src/music_storage/music_db.rs +++ b/src/music_storage/music_db.rs @@ -11,28 +11,29 @@ use walkdir::WalkDir; use crate::music_controller::config::Config; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Song { - pub path: Box<Path>, + pub path: URI, pub title: Option<String>, pub album: Option<String>, - tracknum: Option<usize>, + pub tracknum: Option<usize>, pub artist: Option<String>, - date: Option<Date>, - genre: Option<String>, - plays: Option<usize>, - favorited: Option<bool>, - format: Option<FileFormat>, // TODO: Make this a proper FileFormat eventually - duration: Option<Duration>, + pub date: Option<Date>, + pub genre: Option<String>, + pub plays: Option<usize>, + pub favorited: Option<bool>, + pub format: Option<FileFormat>, // TODO: Make this a proper FileFormat eventually + pub duration: Option<Duration>, pub custom_tags: Option<Vec<Tag>>, } -#[derive(Clone)] + +#[derive(Clone, Debug)] pub enum URI{ Local(String), Remote(Service, String), } -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug)] pub enum Service { InternetRadio, Spotify, @@ -143,7 +144,7 @@ fn parse_cuesheet( let tags = track.get_cdtext(); let cue_song = Song { - path: track_path.into(), + path: URI::Local(String::from("URI")), title: tags.read(PTI::Title), album: album.clone(), tracknum: Some(index + 1), @@ -311,7 +312,7 @@ pub fn add_file_to_db(target_file: &Path, connection: &Connection) { } } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Clone)] pub enum Tag { SongPath, Title, @@ -363,7 +364,7 @@ impl MusicObject { } /// Query the database, returning a list of items -pub fn query( +pub fn query ( config: &Config, text_input: &String, queried_tags: &Vec<&Tag>, @@ -435,7 +436,7 @@ pub fn query( let new_song = Song { // TODO: Implement proper errors here - path: Path::new(&row.get::<usize, String>(0).unwrap_or("".to_owned())).into(), + path: URI::Local(String::from("URI")), title: row.get::<usize, String>(1).ok(), album: row.get::<usize, String>(2).ok(), tracknum: row.get::<usize, usize>(3).ok(), diff --git a/src/music_tracker/music_tracker.rs b/src/music_tracker/music_tracker.rs index 9568d85..7dd1d8a 100644 --- a/src/music_tracker/music_tracker.rs +++ b/src/music_tracker/music_tracker.rs @@ -7,19 +7,21 @@ use md5::{Md5, Digest}; use discord_presence::{ Event, DiscordError}; use surf::StatusCode; +use crate::music_storage::music_db::Song; + #[async_trait] pub trait MusicTracker { /// Adds one listen to a song halfway through playback - async fn track_song(&mut self, song: &String) -> Result<(), TrackerError>; + 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: &String) -> Result<(), TrackerError>; + 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: &String) -> Result<u32, TrackerError>; + async fn get_times_tracked(&mut self, song: &Song) -> Result<u32, TrackerError>; } #[derive(Debug)] @@ -52,13 +54,12 @@ impl TrackerError { } } -#[derive(Serialize, Deserialize, Clone)] +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct LastFMConfig { pub enabled: bool, pub dango_api_key: String, - pub auth_token: Option<String>, - pub shared_secret: Option<String>, - pub session_key: Option<String>, + pub shared_secret: String, + pub session_key: String, } pub struct LastFM { @@ -67,15 +68,21 @@ pub struct LastFM { #[async_trait] impl MusicTracker for LastFM { - async fn track_song(&mut self, song: &String) -> Result<(), TrackerError> { + 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 string_timestamp = timestamp.to_string(); + + let (artist, track) = match (song.artist, song.title) { + (Some(artist), Some(track)) => (artist, track), + _ => return Err(TrackerError::InvalidSong) + }; + params.insert("method", "track.scrobble"); - params.insert("artist", "Kikuo"); - params.insert("track", "A Happy Death - Again"); + params.insert("artist", &artist); + params.insert("track", &track); params.insert("timestamp", &string_timestamp); return match self.api_request(params).await { @@ -84,11 +91,17 @@ impl MusicTracker for LastFM { } } - async fn track_now(&mut self, song: &String) -> Result<(), TrackerError> { + 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) { + (Some(artist), Some(track)) => (artist, track), + _ => return Err(TrackerError::InvalidSong) + }; + params.insert("method", "track.updateNowPlaying"); - params.insert("artist", "Kikuo"); - params.insert("track", "A Happy Death - Again"); + params.insert("artist", &artist); + params.insert("track", &track); return match self.api_request(params).await { Ok(_) => Ok(()), @@ -106,7 +119,7 @@ impl MusicTracker for LastFM { } } - async fn get_times_tracked(&mut self, song: &String) -> Result<u32, TrackerError> { + async fn get_times_tracked(&mut self, song: &Song) -> Result<u32, TrackerError> { todo!(); } } @@ -129,26 +142,21 @@ struct Session { } impl LastFM { - // Returns a url to be accessed by the user - pub async fn get_auth_url(&mut self) -> Result<String, surf::Error> { + /// Returns a url to be approved by the user along with the auth token + pub async fn get_auth(api_key: &String) -> Result<String, surf::Error> { let method = String::from("auth.gettoken"); - let api_key = self.config.dango_api_key.clone(); 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?; - self.config.auth_token = Some(auth_token.token.clone()); let auth_url = format!("http://www.last.fm/api/auth/?api_key={api_key}&token={}", auth_token.token); return Ok(auth_url); } - pub async fn set_session(&mut self) { + /// Returns a LastFM session key + pub async fn get_session_key(api_key: &String, shared_secret: &String, auth_token: &String) -> Result<String, surf::Error> { let method = String::from("auth.getSession"); - let api_key = self.config.dango_api_key.clone(); - let auth_token = self.config.auth_token.clone().unwrap(); - let shared_secret = self.config.shared_secret.clone().unwrap(); - // Creates api_sig as defined in last.fm documentation let api_sig = format!("api_key{api_key}methodauth.getSessiontoken{auth_token}{shared_secret}"); @@ -160,14 +168,14 @@ impl LastFM { 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.unwrap(); + 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).unwrap(); - self.config.session_key = Some(session_response.session.key.clone()); + let session_response: Session = serde_json::from_str(&response)?; + return Ok(session_response.session.key); } - // Creates a new LastFM struct + /// Creates a new LastFM struct with a given config pub fn new(config: &LastFMConfig) -> LastFM { let last_fm = LastFM { config: config.clone() @@ -178,10 +186,10 @@ impl LastFM { // Creates an api request with the given parameters pub async fn api_request(&self, mut params: BTreeMap<&str, &str>) -> Result<surf::Response, surf::Error> { params.insert("api_key", &self.config.dango_api_key); - params.insert("sk", &self.config.session_key.as_ref().unwrap()); + params.insert("sk", &self.config.session_key); // Creates and sets api call signature - let api_sig = LastFM::request_sig(¶ms, &self.config.shared_secret.as_ref().unwrap()); + let api_sig = LastFM::request_sig(¶ms, &self.config.shared_secret); params.insert("api_sig", &api_sig); let mut string_params = String::from(""); @@ -226,7 +234,7 @@ impl LastFM { } } -#[derive(Serialize, Deserialize, Clone)] +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct DiscordRPCConfig { pub enabled: bool, pub dango_client_id: u64, @@ -250,7 +258,14 @@ impl DiscordRPC { #[async_trait] impl MusicTracker for DiscordRPC { - async fn track_now(&mut self, song: &String) -> Result<(), TrackerError> { + async fn track_now(&mut self, song: Song) -> Result<(), TrackerError> { + // Sets song title + let song_name = if let Some(song_name) = song.title { + song_name + } else { + String::from("Unknown") + }; + let _client_thread = self.client.start(); // Blocks thread execution until it has connected to local discord client @@ -263,7 +278,7 @@ impl MusicTracker for DiscordRPC { // Sets discord account activity to current playing song let send_activity = self.client.set_activity(|activity| { activity - .state(song) + .state(format!("Listening to: {}", song_name)) .assets(|assets| assets.large_image(&self.config.dango_icon)) .timestamps(|time| time.start(start_time)) }); @@ -274,7 +289,7 @@ impl MusicTracker for DiscordRPC { } } - async fn track_song(&mut self, song: &String) -> Result<(), TrackerError> { + async fn track_song(&mut self, song: Song) -> Result<(), TrackerError> { return Ok(()) } @@ -282,7 +297,7 @@ impl MusicTracker for DiscordRPC { return Ok(()) } - async fn get_times_tracked(&mut self, song: &String) -> Result<u32, TrackerError> { + async fn get_times_tracked(&mut self, song: &Song) -> Result<u32, TrackerError> { return Ok(0); } } \ No newline at end of file From 7e64c67b46055e88ff9b2b26ba5ed5e0a8b179fc Mon Sep 17 00:00:00 2001 From: G2-Games <ke0bhogsg@gmail.com> Date: Fri, 19 Jan 2024 17:45:57 -0600 Subject: [PATCH 024/136] Added preliminary listenbrainz scrobbling, removed cue support. Closes #1 --- Cargo.toml | 1 - src/music_controller/config.rs | 19 +++-- src/music_controller/music_controller.rs | 5 +- src/music_player/music_output.rs | 5 +- src/music_player/music_player.rs | 17 +++-- src/music_storage/music_db.rs | 60 ---------------- src/music_tracker/music_tracker.rs | 92 ++++++++++++++++++++++-- 7 files changed, 118 insertions(+), 81 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b312946..3ee99fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,6 @@ heapless = "0.7.16" rb = "0.4.1" symphonia = { version = "0.5.3", features = ["all-codecs"] } serde_json = "1.0.104" -cue = "2.0.0" async-std = "1.12.0" async-trait = "0.1.73" md-5 = "0.10.5" diff --git a/src/music_controller/config.rs b/src/music_controller/config.rs index c77a02a..034f12d 100644 --- a/src/music_controller/config.rs +++ b/src/music_controller/config.rs @@ -4,13 +4,14 @@ use std::fs; use serde::{Deserialize, Serialize}; -use crate::music_tracker::music_tracker::{LastFM, LastFMConfig, DiscordRPCConfig}; +use crate::music_tracker::music_tracker::{LastFMConfig, DiscordRPCConfig, ListenBrainzConfig}; #[derive(Serialize, Deserialize, PartialEq, Eq)] pub struct Config { pub db_path: Box<PathBuf>, pub lastfm: Option<LastFMConfig>, pub discord: Option<DiscordRPCConfig>, + pub listenbrainz: Option<ListenBrainzConfig>, } impl Default for Config { @@ -21,18 +22,24 @@ 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 + /// Creates and saves a new config with default values pub fn new(config_file: &PathBuf) -> std::io::Result<Config> { let config = Config::default(); config.save(config_file)?; @@ -40,14 +47,14 @@ impl Config { Ok(config) } - // Loads config from given file path + /// Loads config from given file path pub fn from(config_file: &PathBuf) -> std::result::Result<Config, toml::de::Error> { 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 + /// 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(); diff --git a/src/music_controller/music_controller.rs b/src/music_controller/music_controller.rs index 29f4fb7..da4f676 100644 --- a/src/music_controller/music_controller.rs +++ b/src/music_controller/music_controller.rs @@ -1,12 +1,11 @@ use std::path::PathBuf; -use std::sync::{RwLock, Arc, Mutex}; +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_processor::music_processor::MusicProcessor; -use crate::music_storage::music_db::{URI, Song}; +use crate::music_storage::music_db::Song; pub struct MusicController { pub config: Arc<RwLock<Config>>, diff --git a/src/music_player/music_output.rs b/src/music_player/music_output.rs index b774b0f..795e9e9 100644 --- a/src/music_player/music_output.rs +++ b/src/music_player/music_output.rs @@ -74,8 +74,7 @@ pub fn open_stream(spec: SignalSpec, duration: Duration) -> Result<Box<dyn Audio cpal::SampleFormat::F64 => AudioOutput::<f64>::create_stream(spec, &device, &config.into(), duration), _ => todo!(), }; - } - +} impl<T: OutputSample> AudioOutput<T> { // Creates the stream (TODO: Merge w/open_stream?) @@ -83,7 +82,7 @@ impl<T: OutputSample> AudioOutput<T> { let num_channels = config.channels as usize; // Ring buffer is created with 200ms audio capacity - let ring_len = ((200 * config.sample_rate.0 as usize) / 1000) * num_channels; + let ring_len = ((50 * config.sample_rate.0 as usize) / 1000) * num_channels; let ring_buf= rb::SpscRb::new(ring_len); let ring_buf_producer = ring_buf.producer(); diff --git a/src/music_player/music_player.rs b/src/music_player/music_player.rs index 2944788..5ab0ad9 100644 --- a/src/music_player/music_player.rs +++ b/src/music_player/music_player.rs @@ -21,7 +21,7 @@ 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, LastFM, DiscordRPCConfig, DiscordRPC, TrackerError}; +use crate::music_tracker::music_tracker::{MusicTracker, TrackerError, LastFM, DiscordRPC, ListenBrainz}; // Struct that controls playback of music pub struct MusicPlayer { @@ -146,13 +146,22 @@ impl MusicPlayer { // Sets local config for trackers to detect changes let local_config = global_config.clone(); let mut trackers: Vec<Box<dyn MusicTracker>> = Vec::new(); - // Updates local trackers to the music controller config + // Updates local trackers to the music controller config // TODO: refactor let update_trackers = |trackers: &mut Vec<Box<dyn MusicTracker>>|{ if let Some(lastfm_config) = global_config.lastfm.clone() { - trackers.push(Box::new(LastFM::new(&lastfm_config))); + if lastfm_config.enabled { + trackers.push(Box::new(LastFM::new(&lastfm_config))); + } } if let Some(discord_config) = global_config.discord.clone() { - trackers.push(Box::new(DiscordRPC::new(&discord_config))); + if discord_config.enabled { + trackers.push(Box::new(DiscordRPC::new(&discord_config))); + } + } + if let Some(listenbz_config) = global_config.listenbrainz.clone() { + if listenbz_config.enabled { + trackers.push(Box::new(ListenBrainz::new(&listenbz_config))); + } } }; update_trackers(&mut trackers); diff --git a/src/music_storage/music_db.rs b/src/music_storage/music_db.rs index 7ed59fe..093d7c5 100644 --- a/src/music_storage/music_db.rs +++ b/src/music_storage/music_db.rs @@ -2,7 +2,6 @@ use file_format::{FileFormat, Kind}; use serde::Deserialize; use lofty::{Accessor, AudioFile, Probe, TaggedFileExt, ItemKey, ItemValue, TagType}; use rusqlite::{params, Connection}; -use cue::{cd_text::PTI, cd::CD}; use std::fs; use std::path::{Path, PathBuf}; use std::time::Duration; @@ -105,64 +104,6 @@ fn path_in_db(query_path: &Path, connection: &Connection) -> bool { } } -/// Parse a cuesheet given a path and a directory it is located in, -/// returning a Vec of Song objects -fn parse_cuesheet( - cuesheet_path: &Path, - current_dir: &PathBuf -) -> Result<Vec<Song>, Box<dyn std::error::Error>>{ - let cuesheet = CD::parse_file(cuesheet_path.to_path_buf())?; - - let album = cuesheet.get_cdtext().read(PTI::Title); - - let mut song_list:Vec<Song> = vec![]; - - for (index, track) in cuesheet.tracks().iter().enumerate() { - let track_string_path = format!("{}/{}", current_dir.to_string_lossy(), track.get_filename()); - let track_path = Path::new(&track_string_path); - - if !track_path.exists() {continue}; - - // Get the format as a string - let short_format = match FileFormat::from_file(track_path) { - Ok(fmt) => Some(fmt), - Err(_) => None - }; - - let duration = Duration::from_secs(track.get_length().unwrap_or(-1) as u64); - - let custom_index_start = Tag::Custom{ - tag: String::from("dango_cue_index_start"), - tag_value: track.get_index(0).unwrap_or(-1).to_string() - }; - let custom_index_end = Tag::Custom{ - tag: String::from("dango_cue_index_end"), - tag_value: track.get_index(0).unwrap_or(-1).to_string() - }; - - let custom_tags: Vec<Tag> = vec![custom_index_start, custom_index_end]; - - let tags = track.get_cdtext(); - let cue_song = Song { - path: URI::Local(String::from("URI")), - title: tags.read(PTI::Title), - album: album.clone(), - tracknum: Some(index + 1), - artist: tags.read(PTI::Performer), - date: None, - genre: tags.read(PTI::Genre), - plays: Some(0), - favorited: Some(false), - format: short_format, - duration: Some(duration), - custom_tags: Some(custom_tags) - }; - - song_list.push(cue_song); - } - - Ok(song_list) -} pub fn find_all_music( config: &Config, @@ -196,7 +137,6 @@ pub fn find_all_music( add_file_to_db(target_file.path(), &db_connection) } else if extension.to_ascii_lowercase() == "cue" { // TODO: implement cuesheet support - parse_cuesheet(target_file.path(), ¤t_dir); } } diff --git a/src/music_tracker/music_tracker.rs b/src/music_tracker/music_tracker.rs index 7dd1d8a..213a5f1 100644 --- a/src/music_tracker/music_tracker.rs +++ b/src/music_tracker/music_tracker.rs @@ -1,10 +1,11 @@ use std::time::{SystemTime, UNIX_EPOCH}; use std::collections::BTreeMap; +use serde_json::json; use async_trait::async_trait; -use serde::{Serialize, Deserialize, Serializer}; +use serde::{Serialize, Deserialize}; use md5::{Md5, Digest}; -use discord_presence::{ Event, DiscordError}; +use discord_presence::{Event}; use surf::StatusCode; use crate::music_storage::music_db::Song; @@ -38,7 +39,6 @@ pub enum TrackerError { Unknown, } - impl TrackerError { pub fn from_surf_error(error: surf::Error) -> TrackerError { return match error.status() { @@ -300,4 +300,88 @@ impl MusicTracker for DiscordRPC { async fn get_times_tracked(&mut self, song: &Song) -> Result<u32, TrackerError> { return Ok(0); } -} \ No newline at end of file +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)] +pub struct ListenBrainzConfig { + pub enabled: bool, + pub api_url: String, + pub auth_token: String, +} + +pub struct ListenBrainz { + config: ListenBrainzConfig, +} + +#[async_trait] +impl MusicTracker for ListenBrainz { + async fn track_now(&mut self, song: Song) -> Result<(), TrackerError> { + let (artist, track) = match (song.artist, song.title) { + (Some(artist), Some(track)) => (artist, track), + _ => return Err(TrackerError::InvalidSong) + }; + // Creates a json to submit a single song as defined in the listenbrainz documentation + let json_req = json!({ + "listen_type": "playing_now", + "payload": [ + { + "track_metadata": { + "artist_name": artist, + "track_name": track, + } + } + ] + }); + + 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)) + } + } + + 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) { + (Some(artist), Some(track)) => (artist, track), + _ => return Err(TrackerError::InvalidSong) + }; + + let json_req = json!({ + "listen_type": "single", + "payload": [ + { + "listened_at": timestamp, + "track_metadata": { + "artist_name": artist, + "track_name": track, + } + } + ] + }); + + 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)) + } + } + async fn test_tracker(&mut self) -> Result<(), TrackerError> { + todo!() + } + async fn get_times_tracked(&mut self, song: &Song) -> Result<u32, TrackerError> { + todo!() + } +} + +impl ListenBrainz { + pub fn new(config: &ListenBrainzConfig) -> Self { + ListenBrainz { + config: config.clone() + } + } + // Makes an api request to configured url with given json + pub async fn api_request(&self, request: &String, endpoint: &String) -> Result<surf::Response, surf::Error> { + 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 12aaab9174b2e1a0b4ff6ff4bac81a50332353e1 Mon Sep 17 00:00:00 2001 From: G2-Games <ke0bhogsg@gmail.com> Date: Sun, 19 Nov 2023 19:22:11 -0600 Subject: [PATCH 025/136] Updated `lofty` to `0.17.0`, fixing several issues --- Cargo.toml | 2 +- src/music_storage/music_db.rs | 51 ++++++++++++++++++++--------------- 2 files changed, 31 insertions(+), 22 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d30fe73..cfde222 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ categories = ["multimedia::audio"] [dependencies] file-format = { version = "0.22.0", features = ["reader-asf", "reader-ebml", "reader-mp4", "reader-rm", "reader-txt", "reader-xml", "serde"] } -lofty = "0.16.1" +lofty = "0.17.0" serde = { version = "1.0.191", features = ["derive"] } time = "0.3.22" toml = "0.7.5" diff --git a/src/music_storage/music_db.rs b/src/music_storage/music_db.rs index 3372323..093a7c1 100644 --- a/src/music_storage/music_db.rs +++ b/src/music_storage/music_db.rs @@ -11,7 +11,7 @@ use std::ops::ControlFlow::{Break, Continue}; use rcue::parser::parse_from_file; use file_format::{FileFormat, Kind}; use walkdir::WalkDir; -use lofty::{AudioFile, ItemKey, ItemValue, Probe, TagType, TaggedFileExt}; +use lofty::{AudioFile, ItemKey, ItemValue, Probe, TagType, TaggedFileExt, ParseOptions}; use std::fs; use std::path::{Path, PathBuf}; @@ -246,7 +246,7 @@ impl Album<'_> { } } -const BLOCKED_EXTENSIONS: [&str; 3] = ["vob", "log", "txt"]; +const BLOCKED_EXTENSIONS: [&str; 5] = ["vob", "log", "txt", "sf2", "mid"]; #[derive(Debug)] pub struct MusicLibrary { @@ -341,6 +341,7 @@ impl MusicLibrary { config: &Config, ) -> Result<usize, Box<dyn std::error::Error>> { let mut total = 0; + let mut errors = 0; for target_file in WalkDir::new(target_path) .follow_links(true) .into_iter() @@ -381,6 +382,7 @@ impl MusicLibrary { match self.add_file(&target_file.path()) { Ok(_) => total += 1, Err(_error) => { + errors += 1; println!("{}, {:?}: {}", format, target_file.file_name(), _error) } // TODO: Handle more of these errors }; @@ -388,6 +390,7 @@ impl MusicLibrary { total += match self.add_cuesheet(&target_file.path().to_path_buf()) { Ok(added) => added, Err(error) => { + errors += 1; println!("{}", error); 0 } @@ -398,19 +401,19 @@ impl MusicLibrary { // Save the database after scanning finishes self.save(&config).unwrap(); + println!("ERRORS: {}", errors); + Ok(total) } pub fn add_file(&mut self, target_file: &Path) -> Result<(), Box<dyn Error>> { + let normal_options = ParseOptions::new().parsing_mode(lofty::ParsingMode::Relaxed); + // TODO: Fix error handling here - let tagged_file = match lofty::read_from_path(target_file) { + let tagged_file = match Probe::open(target_file)?.options(normal_options).read() { Ok(tagged_file) => tagged_file, - Err(_) => match Probe::open(target_file)?.read() { - Ok(tagged_file) => tagged_file, - - Err(error) => return Err(error.into()), - }, + Err(error) => return Err(error.into()), }; // Ensure the tags exist, if not, insert blank data @@ -435,7 +438,7 @@ impl MusicLibrary { ItemKey::Comment => Tag::Comment, ItemKey::AlbumTitle => Tag::Album, ItemKey::DiscNumber => Tag::Disk, - ItemKey::Unknown(unknown) if unknown == "ACOUSTID_FINGERPRINT" => continue, + ItemKey::Unknown(unknown) if unknown == "ACOUSTID_FINGERPRINT" || unknown == "Acoustid Fingerprint" => continue, ItemKey::Unknown(unknown) => Tag::Key(unknown.to_string()), custom => Tag::Key(format!("{:?}", custom)), }; @@ -490,7 +493,9 @@ impl MusicLibrary { match self.add_song(new_song) { Ok(_) => (), - Err(error) => return Err(error), + Err(_) => { + //return Err(error) + }, }; Ok(()) @@ -532,7 +537,9 @@ impl MusicLibrary { None => Duration::from_secs(0), }; let mut start = track.indices[0].1; - start -= pregap; + if !start.is_zero() { + start -= pregap; + } let duration = match next_track.next() { Some(future) => match future.indices.get(0) { @@ -540,17 +547,15 @@ impl MusicLibrary { None => Duration::from_secs(0) } None => { - let tagged_file = match lofty::read_from_path(&audio_location) { - Ok(tagged_file) => tagged_file, + match lofty::read_from_path(&audio_location) { + Ok(tagged_file) => tagged_file.properties().duration() - start, Err(_) => match Probe::open(&audio_location)?.read() { - Ok(tagged_file) => tagged_file, + Ok(tagged_file) => tagged_file.properties().duration() - start, - Err(error) => return Err(error.into()), + Err(_) => Duration::from_secs(0), }, - }; - - tagged_file.properties().duration() - start + } } }; let end = start + duration + postgap; @@ -656,18 +661,22 @@ impl MusicLibrary { } /// Scan the song by a location and update its tags - pub fn update_by_file(&mut self, new_tags: Song) -> Result<(), Box<dyn std::error::Error>> { - match self.query_uri(&new_tags.location) { + pub fn update_uri(&mut self, target_uri: &URI, new_tags: Vec<Tag>) -> Result<(), Box<dyn std::error::Error>> { + match self.query_uri(target_uri) { Some(_) => (), None => return Err(format!("URI not in database!").into()), } + for tag in new_tags { + println!("{:?}", tag); + } + todo!() } /// Query the database, returning a list of [Song]s /// - /// The order in which the sort by Vec is arranged + /// The order in which the `sort by` Vec is arranged /// determines the output sorting. /// /// Example: From ba4ed346ce018a7dd808ce4837aea8d95891edc2 Mon Sep 17 00:00:00 2001 From: G2-Games <ke0bhogsg@gmail.com> Date: Fri, 24 Nov 2023 14:22:20 -0600 Subject: [PATCH 026/136] Removed music_player in preparation for GStreamer based backend --- src/lib.rs | 7 - src/music_controller/init.rs | 14 - src/music_controller/music_controller.rs | 23 +- src/music_player/music_output.rs | 209 --------- src/music_player/music_player.rs | 529 ----------------------- src/music_player/music_resampler.rs | 154 ------- src/music_storage/playlist.rs | 2 +- src/music_tracker/music_tracker.rs | 8 +- 8 files changed, 6 insertions(+), 940 deletions(-) delete mode 100644 src/music_controller/init.rs delete mode 100644 src/music_player/music_output.rs delete mode 100644 src/music_player/music_player.rs delete mode 100644 src/music_player/music_resampler.rs diff --git a/src/lib.rs b/src/lib.rs index 7aa37eb..a40622d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,14 +8,7 @@ pub mod music_storage { mod utils; } -pub mod music_player { - pub mod music_output; - pub mod music_player; - pub mod music_resampler; -} - pub mod music_controller { pub mod config; - pub mod init; pub mod music_controller; } diff --git a/src/music_controller/init.rs b/src/music_controller/init.rs deleted file mode 100644 index 38b101f..0000000 --- a/src/music_controller/init.rs +++ /dev/null @@ -1,14 +0,0 @@ -use std::fs::File; -use std::path::Path; - -pub fn init() {} - -fn init_config() { - let config_path = "./config.toml"; - - if !Path::new(config_path).try_exists().unwrap() { - File::create("./config.toml").unwrap(); - } -} - -fn init_db() {} diff --git a/src/music_controller/music_controller.rs b/src/music_controller/music_controller.rs index 51be432..8a86205 100644 --- a/src/music_controller/music_controller.rs +++ b/src/music_controller/music_controller.rs @@ -2,20 +2,17 @@ use std::path::PathBuf; use std::sync::{Arc, RwLock}; use crate::music_controller::config::Config; -use crate::music_player::music_player::{DecoderMessage, MusicPlayer, PlayerStatus}; use crate::music_storage::music_db::{MusicLibrary, Song, Tag}; pub struct MusicController { pub config: Arc<RwLock<Config>>, pub library: MusicLibrary, - music_player: MusicPlayer, } impl MusicController { /// Creates new MusicController with config at given path pub fn new(config_path: &PathBuf) -> Result<MusicController, Box<dyn std::error::Error>> { 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), @@ -24,7 +21,6 @@ impl MusicController { let controller = MusicController { config, library, - music_player, }; return Ok(controller); @@ -33,7 +29,6 @@ impl MusicController { /// Creates new music controller from a config at given path pub fn from(config_path: &PathBuf) -> Result<MusicController, Box<dyn std::error::Error>> { 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), @@ -42,33 +37,17 @@ impl MusicController { let controller = MusicController { config, library, - music_player, }; 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<Song> { - return self.music_player.get_current_song(); - } - /// Queries the [MusicLibrary], returning a `Vec<Song>` pub fn query_library( &self, query_string: &String, target_tags: Vec<Tag>, - search_location: bool, + _search_location: bool, sort_by: Vec<Tag>, ) -> Option<Vec<&Song>> { self.library diff --git a/src/music_player/music_output.rs b/src/music_player/music_output.rs deleted file mode 100644 index 76e6d3b..0000000 --- a/src/music_player/music_output.rs +++ /dev/null @@ -1,209 +0,0 @@ -use std::{result, thread}; - -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}; -use cpal::{self, SizedSample}; - -use rb::*; - -use crate::music_player::music_resampler::Resampler; - -pub trait AudioStream { - fn write(&mut self, decoded: AudioBufferRef<'_>) -> Result<()>; - fn flush(&mut self); -} - -#[derive(Debug)] -pub enum AudioOutputError { - OpenStreamError, - PlayStreamError, - StreamClosedError, -} - -pub type Result<T> = result::Result<T, AudioOutputError>; - -pub trait OutputSample: - SizedSample - + FromSample<f32> - + IntoSample<f32> - + cpal::Sample - + ConvertibleSample - + RawSample - + std::marker::Send - + 'static -{ -} - -pub struct AudioOutput<T> -where - T: OutputSample, -{ - ring_buf_producer: rb::Producer<T>, - sample_buf: SampleBuffer<T>, - stream: cpal::Stream, - resampler: Option<Resampler<T>>, -} -impl OutputSample for i8 {} -impl OutputSample for i16 {} -impl OutputSample for i32 {} -//impl OutputSample for i64 {} -impl OutputSample for u8 {} -impl OutputSample for u16 {} -impl OutputSample for u32 {} -//impl OutputSample for u64 {} -impl OutputSample for f32 {} -impl OutputSample for f64 {} -//create a new trait with functions, then impl that somehow - -pub fn open_stream(spec: SignalSpec, duration: Duration) -> Result<Box<dyn AudioStream>> { - 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::<i8>::create_stream(spec, &device, &config.into(), duration) - } - cpal::SampleFormat::I16 => { - AudioOutput::<i16>::create_stream(spec, &device, &config.into(), duration) - } - cpal::SampleFormat::I32 => { - AudioOutput::<i32>::create_stream(spec, &device, &config.into(), duration) - } - //cpal::SampleFormat::I64 => AudioOutput::<i64>::create_stream(spec, &device, &config.into(), duration), - cpal::SampleFormat::U8 => { - AudioOutput::<u8>::create_stream(spec, &device, &config.into(), duration) - } - cpal::SampleFormat::U16 => { - AudioOutput::<u16>::create_stream(spec, &device, &config.into(), duration) - } - cpal::SampleFormat::U32 => { - AudioOutput::<u32>::create_stream(spec, &device, &config.into(), duration) - } - //cpal::SampleFormat::U64 => AudioOutput::<u64>::create_stream(spec, &device, &config.into(), duration), - cpal::SampleFormat::F32 => { - AudioOutput::<f32>::create_stream(spec, &device, &config.into(), duration) - } - cpal::SampleFormat::F64 => { - AudioOutput::<f64>::create_stream(spec, &device, &config.into(), duration) - } - _ => todo!(), - }; -} - -impl<T: OutputSample> AudioOutput<T> { - // Creates the stream (TODO: Merge w/open_stream?) - fn create_stream( - spec: SignalSpec, - device: &cpal::Device, - config: &cpal::StreamConfig, - duration: Duration, - ) -> Result<Box<dyn AudioStream>> { - 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_producer = ring_buf.producer(); - let ring_buf_consumer = ring_buf.consumer(); - - let stream_result = device.build_output_stream( - 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); - }, - //TODO: Handle error here properly - move |err| println!("Yeah we erroring out here"), - 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::<T>::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, - )) - } - - Ok(Box::new(AudioOutput { - ring_buf_producer, - sample_buf, - stream, - resampler, - })) - } -} - -impl<T: OutputSample> AudioStream for AudioOutput<T> { - // Writes given samples to ring buffer - fn write(&mut self, decoded: AudioBufferRef<'_>) -> Result<()> { - if decoded.frames() == 0 { - return Ok(()); - } - - let mut samples: &[T] = if let Some(resampler) = &mut self.resampler { - // Resamples if required - match resampler.resample(decoded) { - Some(resampled) => resampled, - None => return Ok(()), - } - } else { - 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(); - - while let Some(written) = self.ring_buf_producer.write_blocking(stale_samples) { - stale_samples = &stale_samples[written..]; - } - } - - let _ = self.stream.pause(); - } -} diff --git a/src/music_player/music_player.rs b/src/music_player/music_player.rs deleted file mode 100644 index e3c43ad..0000000 --- a/src/music_player/music_player.rs +++ /dev/null @@ -1,529 +0,0 @@ -use std::io::SeekFrom; -use std::sync::mpsc::{self, Receiver, Sender}; -use std::sync::{Arc, RwLock}; -use std::thread; - -use async_std::io::ReadExt; -use async_std::task; - -use futures::future::join_all; -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::{MediaSource, MediaSourceStream}; -use symphonia::core::meta::MetadataOptions; -use symphonia::core::probe::Hint; -use symphonia::core::units::{Time, TimeBase}; - -use futures::AsyncBufRead; - -use crate::music_controller::config::Config; -use crate::music_player::music_output::AudioStream; -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 { - player_status: PlayerStatus, - music_trackers: Vec<Box<dyn MusicTracker + Send>>, - current_song: Arc<RwLock<Option<Song>>>, - message_sender: Sender<DecoderMessage>, - status_receiver: Receiver<PlayerStatus>, - config: Arc<RwLock<Config>>, -} - -#[derive(Clone, Copy, Debug)] -pub enum PlayerStatus { - Playing(f64), - Paused, - Stopped, - Error, -} - -#[derive(Debug, Clone)] -pub enum DecoderMessage { - OpenSong(Song), - Play, - Pause, - Stop, - SeekTo(u64), -} - -#[derive(Clone)] -pub enum TrackerMessage { - Track(Song), - TrackNow(Song), -} - -// Holds a song decoder reader, etc -struct SongHandler { - pub reader: Box<dyn FormatReader>, - pub decoder: Box<dyn Decoder>, - pub time_base: Option<TimeBase>, - pub duration: Option<u64>, -} - -// TODO: actual error handling here -impl SongHandler { - pub fn new(uri: &URI) -> Result<Self, ()> { - // Opens remote/local source and creates MediaSource for symphonia - let config = RemoteOptions { - media_buffer_len: 10000, - forward_buffer_len: 10000, - }; - let src: Box<dyn MediaSource> = match uri { - 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!(), - }; - - 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 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, - }); - } -} - -impl MusicPlayer { - pub fn new(config: Arc<RwLock<Config>>) -> Self { - // Creates mpsc channels to communicate with music player threads - 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 { - music_trackers: Vec::new(), - player_status: PlayerStatus::Stopped, - current_song, - message_sender, - status_receiver, - config, - } - } - - fn start_tracker( - status_sender: Sender<Result<(), TrackerError>>, - tracker_receiver: Receiver<TrackerMessage>, - config: Arc<RwLock<Config>>, - ) { - 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<Box<dyn MusicTracker>> = Vec::new(); - // Updates local trackers to the music controller config // TODO: refactor - let update_trackers = |trackers: &mut Vec<Box<dyn MusicTracker>>| { - if let Some(lastfm_config) = global_config.lastfm.clone() { - if lastfm_config.enabled { - trackers.push(Box::new(LastFM::new(&lastfm_config))); - } - } - if let Some(discord_config) = global_config.discord.clone() { - if discord_config.enabled { - trackers.push(Box::new(DiscordRPC::new(&discord_config))); - } - } - if let Some(listenbz_config) = global_config.listenbrainz.clone() { - if listenbz_config.enabled { - trackers.push(Box::new(ListenBrainz::new(&listenbz_config))); - } - } - }; - update_trackers(&mut trackers); - loop { - if let message = tracker_receiver.recv() { - if local_config != global_config { - update_trackers(&mut trackers); - } - - let mut results = Vec::new(); - task::block_on(async { - 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(_) => {} - } - } - results = join_all(futures).await; - }); - - for result in results { - status_sender.send(result).unwrap_or_default() - } - } - } - }); - } - - // Opens and plays song with given path in separate thread - fn start_player( - message_receiver: Receiver<DecoderMessage>, - status_sender: Sender<PlayerStatus>, - config: Arc<RwLock<Config>>, - current_song: Arc<RwLock<Option<Song>>>, - ) { - // 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<u64> = None; - - let mut audio_output: Option<Box<dyn AudioStream>> = None; - - let (tracker_sender, tracker_receiver): ( - Sender<TrackerMessage>, - Receiver<TrackerMessage>, - ) = mpsc::channel(); - let (tracker_status_sender, tracker_status_receiver): ( - Sender<Result<(), TrackerError>>, - Receiver<Result<(), TrackerError>>, - ) = 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 { - 'handle_message: loop { - let message = if paused { - // Pauses playback by blocking on waiting for new player messages - match message_receiver.recv() { - Ok(message) => Some(message), - Err(_) => None, - } - } else { - // Resumes playback by not blocking - match message_receiver.try_recv() { - Ok(message) => Some(message), - Err(_) => break 'handle_message, - } - }; - // Handles message received from MusicPlayer struct - match message { - Some(DecoderMessage::OpenSong(song)) => { - let song_uri = song.location.clone(); - match SongHandler::new(&song_uri) { - Ok(new_handler) => { - song_handler = Some(new_handler); - *current_song.write().unwrap() = Some(song); - paused = false; - song_tracked = false; - } - Err(_) => status_sender.send(PlayerStatus::Error).unwrap(), - } - } - Some(DecoderMessage::Play) => { - if song_handler.is_some() { - paused = false; - } - } - Some(DecoderMessage::Pause) => { - paused = true; - status_sender.send(PlayerStatus::Paused).unwrap(); - } - Some(DecoderMessage::SeekTo(time)) => seek_time = Some(time), - // Exits main decode loop and subsequently ends thread - Some(DecoderMessage::Stop) => { - status_sender.send(PlayerStatus::Stopped).unwrap(); - break 'main_decode; - } - 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()) - { - 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(); - seek_time = None; - } - None => {} //Nothing to do! - } - let packet = match song_handler.reader.next_packet() { - Ok(packet) => packet, - Err(Error::ResetRequired) => panic!(), //TODO, - Err(err) => { - // Unrecoverable? - 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(); - } - - 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; - // 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 - { - song_tracked = true; - tracker_sender - .send(TrackerMessage::Track(song.clone())) - .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(), - ); - } - } - 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); - } - } - } - } - }); - } - - // 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<Song> { - 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; - } -} - -// TODO: Make the buffer length do anything -/// Options for remote sources -/// -/// media_buffer_len is how many bytes are to be buffered in totala -/// -/// forward_buffer is how many bytes can ahead of the seek position without the remote source being read from -pub struct RemoteOptions { - media_buffer_len: u64, - forward_buffer_len: u64, -} - -impl Default for RemoteOptions { - fn default() -> Self { - RemoteOptions { - media_buffer_len: 100000, - forward_buffer_len: 1024, - } - } -} - -/// A remote source of media -struct RemoteSource { - reader: Box<dyn AsyncBufRead + Send + Sync + Unpin>, - media_buffer: Vec<u8>, - forward_buffer_len: u64, - offset: u64, -} - -impl RemoteSource { - /// Creates a new RemoteSource with given uri and configuration - pub fn new(uri: &str, config: &RemoteOptions) -> Result<Self, surf::Error> { - 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(), - forward_buffer_len: config.forward_buffer_len, - offset: 0, - }) - } -} -// TODO: refactor this + buffer into the buffer passed into the function, not a newly allocated one -impl std::io::Read for RemoteSource { - fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> { - // Reads bytes into the media buffer if the offset is within the specified distance from the end of the buffer - if self.media_buffer.len() as u64 - self.offset < self.forward_buffer_len { - let mut buffer = [0; 1024]; - let read_bytes = task::block_on(async { - match self.reader.read_exact(&mut buffer).await { - 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 - let mut bytes_read = 0; - for location in 0..1024 { - if (location + self.offset as usize) < self.media_buffer.len() { - buf[location] = self.media_buffer[location + self.offset as usize]; - bytes_read += 1; - } - } - - self.offset += bytes_read; - return Ok(bytes_read as usize); - } -} - -impl std::io::Seek for RemoteSource { - // Seeks to a given position - // Seeking past the internal buffer's length results in the seeking to the end of content - fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> { - match pos { - // Offset is set to given position - SeekFrom::Start(pos) => { - 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 { - self.offset = self.media_buffer.len() as u64; - } else { - 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 { - self.offset = self.media_buffer.len() as u64; - } else { - self.offset += pos as u64 - } - return Ok(self.offset); - } - } - } -} - -impl MediaSource for RemoteSource { - fn is_seekable(&self) -> bool { - return true; - } - - fn byte_len(&self) -> Option<u64> { - return None; - } -} diff --git a/src/music_player/music_resampler.rs b/src/music_player/music_resampler.rs deleted file mode 100644 index 4040835..0000000 --- a/src/music_player/music_resampler.rs +++ /dev/null @@ -1,154 +0,0 @@ -// Symphonia -// Copyright (c) 2019-2022 The Project Symphonia Developers. -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -use symphonia::core::audio::{AudioBuffer, AudioBufferRef, Signal, SignalSpec}; -use symphonia::core::conv::{FromSample, IntoSample}; -use symphonia::core::sample::Sample; - -pub struct Resampler<T> { - resampler: rubato::FftFixedIn<f32>, - input: Vec<Vec<f32>>, - output: Vec<Vec<f32>>, - interleaved: Vec<T>, - duration: usize, -} - -impl<T> Resampler<T> -where - T: Sample + FromSample<f32> + IntoSample<f32>, -{ - fn resample_inner(&mut self) -> &[T] { - { - //let mut input = heapless::Vec::<f32, 32>::new(); - let mut input: arrayvec::ArrayVec<&[f32], 32> = Default::default(); - - for channel in self.input.iter() { - input.push(&channel[..self.duration]); - } - - // Resample. - rubato::Resampler::process_into_buffer( - &mut self.resampler, - &input, - &mut self.output, - None, - ) - .unwrap(); - } - - // Remove consumed samples from the input buffer. - for channel in self.input.iter_mut() { - channel.drain(0..self.duration); - } - - // Interleave the planar samples from Rubato. - let num_channels = self.output.len(); - - 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() { - *s = self.output[ch][i].into_sample(); - } - } - - &self.interleaved - } -} - -impl<T> Resampler<T> -where - T: Sample + FromSample<f32> + IntoSample<f32>, -{ - pub fn new(spec: SignalSpec, to_sample_rate: usize, duration: u64) -> Self { - let duration = duration as usize; - let num_channels = spec.channels.count(); - - let resampler = rubato::FftFixedIn::<f32>::new( - spec.rate as usize, - to_sample_rate, - duration, - 2, - num_channels, - ) - .unwrap(); - - let output = rubato::Resampler::output_buffer_allocate(&resampler); - - let input = vec![Vec::with_capacity(duration); num_channels]; - - Self { - resampler, - input, - output, - duration, - interleaved: Default::default(), - } - } - - /// Resamples a planar/non-interleaved input. - /// - /// Returns the resampled samples in an interleaved format. - pub fn resample(&mut self, input: AudioBufferRef<'_>) -> Option<&[T]> { - // Copy and convert samples into input buffer. - convert_samples_any(&input, &mut self.input); - - // Check if more samples are required. - if self.input[0].len() < self.duration { - return None; - } - - Some(self.resample_inner()) - } - - /// Resample any remaining samples in the resample buffer. - pub fn flush(&mut self) -> Option<&[T]> { - let len = self.input[0].len(); - - if len == 0 { - return None; - } - - let partial_len = len % self.duration; - - if partial_len != 0 { - // Fill each input channel buffer with silence to the next multiple of the resampler - // duration. - for channel in self.input.iter_mut() { - channel.resize(len + (self.duration - partial_len), f32::MID); - } - } - - Some(self.resample_inner()) - } -} - -fn convert_samples_any(input: &AudioBufferRef<'_>, output: &mut [Vec<f32>]) { - match input { - AudioBufferRef::U8(input) => convert_samples(input, output), - AudioBufferRef::U16(input) => convert_samples(input, output), - AudioBufferRef::U24(input) => convert_samples(input, output), - AudioBufferRef::U32(input) => convert_samples(input, output), - AudioBufferRef::S8(input) => convert_samples(input, output), - AudioBufferRef::S16(input) => convert_samples(input, output), - AudioBufferRef::S24(input) => convert_samples(input, output), - AudioBufferRef::S32(input) => convert_samples(input, output), - AudioBufferRef::F32(input) => convert_samples(input, output), - AudioBufferRef::F64(input) => convert_samples(input, output), - } -} - -fn convert_samples<S>(input: &AudioBuffer<S>, output: &mut [Vec<f32>]) -where - S: Sample + IntoSample<f32>, -{ - for (c, dst) in output.iter_mut().enumerate() { - let src = input.chan(c); - dst.extend(src.iter().map(|&s| s.into_sample())); - } -} diff --git a/src/music_storage/playlist.rs b/src/music_storage/playlist.rs index 797d0d2..189b9e8 100644 --- a/src/music_storage/playlist.rs +++ b/src/music_storage/playlist.rs @@ -1,6 +1,6 @@ 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 9ffd73d..37a3142 100644 --- a/src/music_tracker/music_tracker.rs +++ b/src/music_tracker/music_tracker.rs @@ -123,7 +123,7 @@ impl MusicTracker for LastFM { }; } - async fn get_times_tracked(&mut self, song: &Song) -> Result<u32, TrackerError> { + async fn get_times_tracked(&mut self, _song: &Song) -> Result<u32, TrackerError> { todo!(); } } @@ -319,7 +319,7 @@ impl MusicTracker for DiscordRPC { } } - async fn track_song(&mut self, song: Song) -> Result<(), TrackerError> { + async fn track_song(&mut self, _song: Song) -> Result<(), TrackerError> { return Ok(()); } @@ -327,7 +327,7 @@ impl MusicTracker for DiscordRPC { return Ok(()); } - async fn get_times_tracked(&mut self, song: &Song) -> Result<u32, TrackerError> { + async fn get_times_tracked(&mut self, _song: &Song) -> Result<u32, TrackerError> { return Ok(0); } } @@ -408,7 +408,7 @@ impl MusicTracker for ListenBrainz { async fn test_tracker(&mut self) -> Result<(), TrackerError> { todo!() } - async fn get_times_tracked(&mut self, song: &Song) -> Result<u32, TrackerError> { + async fn get_times_tracked(&mut self, _song: &Song) -> Result<u32, TrackerError> { todo!() } } From 048254150c8488b1cb5d627e42f65af6a34446ad Mon Sep 17 00:00:00 2001 From: G2-Games <ke0bhogsg@gmail.com> Date: Fri, 24 Nov 2023 14:25:31 -0600 Subject: [PATCH 027/136] Ran `cargo clippy` --- src/music_controller/config.rs | 8 ++--- src/music_controller/music_controller.rs | 4 +-- src/music_storage/music_db.rs | 38 +++++++++----------- src/music_storage/utils.rs | 6 ++-- src/music_tracker/music_tracker.rs | 44 ++++++++++++------------ 5 files changed, 48 insertions(+), 52 deletions(-) diff --git a/src/music_controller/config.rs b/src/music_controller/config.rs index 82cbd22..f450c9d 100644 --- a/src/music_controller/config.rs +++ b/src/music_controller/config.rs @@ -18,7 +18,7 @@ impl Default for Config { fn default() -> Self { let path = PathBuf::from("./music_database"); - return Config { + Config { db_path: Box::new(path), lastfm: None, @@ -34,7 +34,7 @@ impl Default for Config { api_url: String::from("https://api.listenbrainz.org"), auth_token: String::from(""), }), - }; + } } } @@ -49,10 +49,10 @@ impl Config { /// Loads config from given file path pub fn from(config_file: &PathBuf) -> std::result::Result<Config, toml::de::Error> { - return toml::from_str( + toml::from_str( &read_to_string(config_file) .expect("Failed to initalize music config: File not found!"), - ); + ) } /// Saves config to given path diff --git a/src/music_controller/music_controller.rs b/src/music_controller/music_controller.rs index 8a86205..6f4881a 100644 --- a/src/music_controller/music_controller.rs +++ b/src/music_controller/music_controller.rs @@ -23,7 +23,7 @@ impl MusicController { library, }; - return Ok(controller); + Ok(controller) } /// Creates new music controller from a config at given path @@ -39,7 +39,7 @@ impl MusicController { library, }; - return Ok(controller); + Ok(controller) } /// Queries the [MusicLibrary], returning a `Vec<Song>` diff --git a/src/music_storage/music_db.rs b/src/music_storage/music_db.rs index 093a7c1..f9ef854 100644 --- a/src/music_storage/music_db.rs +++ b/src/music_storage/music_db.rs @@ -115,10 +115,7 @@ impl Song { "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, - }, + Some(format) => format.short_name().map(|short| short.to_string()), None => None, }, _ => todo!(), // Other field types are not yet supported @@ -321,10 +318,9 @@ impl MusicLibrary { /// with matching `PathBuf`s fn query_path(&self, path: &PathBuf) -> Option<Vec<&Song>> { let result: Arc<Mutex<Vec<&Song>>> = Arc::new(Mutex::new(Vec::new())); - let _ = self.library.par_iter().for_each(|track| { + self.library.par_iter().for_each(|track| { if path == track.location.path() { - result.clone().lock().unwrap().push(&track); - return; + result.clone().lock().unwrap().push(track); } }); if result.lock().unwrap().len() > 0 { @@ -368,7 +364,7 @@ impl MusicLibrary { } */ - let format = FileFormat::from_file(&path)?; + let format = FileFormat::from_file(path)?; let extension = match path.extension() { Some(ext) => ext.to_string_lossy().to_ascii_lowercase(), None => String::new(), @@ -379,7 +375,7 @@ impl MusicLibrary { if (format.kind() == Kind::Audio || format.kind() == Kind::Video) && !BLOCKED_EXTENSIONS.contains(&extension.as_str()) { - match self.add_file(&target_file.path()) { + match self.add_file(target_file.path()) { Ok(_) => total += 1, Err(_error) => { errors += 1; @@ -399,7 +395,7 @@ impl MusicLibrary { } // Save the database after scanning finishes - self.save(&config).unwrap(); + self.save(config).unwrap(); println!("ERRORS: {}", errors); @@ -455,7 +451,7 @@ impl MusicLibrary { // Get all the album artwork information from the file let mut album_art: Vec<AlbumArt> = Vec::new(); for (i, _art) in tag.pictures().iter().enumerate() { - let new_art = AlbumArt::Embedded(i as usize); + let new_art = AlbumArt::Embedded(i); album_art.push(new_art) } @@ -547,10 +543,10 @@ impl MusicLibrary { None => Duration::from_secs(0) } None => { - match lofty::read_from_path(&audio_location) { + match lofty::read_from_path(audio_location) { Ok(tagged_file) => tagged_file.properties().duration() - start, - Err(_) => match Probe::open(&audio_location)?.read() { + Err(_) => match Probe::open(audio_location)?.read() { Ok(tagged_file) => tagged_file.properties().duration() - start, Err(_) => Duration::from_secs(0), @@ -561,7 +557,7 @@ impl MusicLibrary { let end = start + duration + postgap; // Get the format as a string - let format: Option<FileFormat> = match FileFormat::from_file(&audio_location) { + let format: Option<FileFormat> = match FileFormat::from_file(audio_location) { Ok(fmt) => Some(fmt), Err(_) => None, }; @@ -637,7 +633,7 @@ impl MusicLibrary { None => (), } match new_song.location { - URI::Local(_) if self.query_path(&new_song.location.path()).is_some() => { + URI::Local(_) if self.query_path(new_song.location.path()).is_some() => { return Err(format!("Location exists for {:?}", new_song.location).into()) } _ => (), @@ -664,7 +660,7 @@ impl MusicLibrary { pub fn update_uri(&mut self, target_uri: &URI, new_tags: Vec<Tag>) -> Result<(), Box<dyn std::error::Error>> { match self.query_uri(target_uri) { Some(_) => (), - None => return Err(format!("URI not in database!").into()), + None => return Err("URI not in database!".to_string().into()), } for tag in new_tags { @@ -709,11 +705,11 @@ impl MusicLibrary { self.library.par_iter().for_each(|track| { for tag in target_tags { let track_result = match tag { - Tag::Field(target) => match track.get_field(&target) { + Tag::Field(target) => match track.get_field(target) { Some(value) => value, None => continue, }, - _ => match track.get_tag(&tag) { + _ => match track.get_tag(tag) { Some(value) => value.clone(), None => continue, }, @@ -749,7 +745,7 @@ impl MusicLibrary { Some(field_value) => field_value, None => continue, }, - _ => match a.get_tag(&sort_option) { + _ => match a.get_tag(sort_option) { Some(tag_value) => tag_value.to_owned(), None => continue, }, @@ -760,7 +756,7 @@ impl MusicLibrary { Some(field_value) => field_value, None => continue, }, - _ => match b.get_tag(&sort_option) { + _ => match b.get_tag(sort_option) { Some(tag_value) => tag_value.to_owned(), None => continue, }, @@ -782,7 +778,7 @@ impl MusicLibrary { path_a.file_name().cmp(&path_b.file_name()) }); - if new_songs.len() > 0 { + if !new_songs.is_empty() { Some(new_songs) } else { None diff --git a/src/music_storage/utils.rs b/src/music_storage/utils.rs index 0847ae0..f4a7947 100644 --- a/src/music_storage/utils.rs +++ b/src/music_storage/utils.rs @@ -45,12 +45,12 @@ pub(super) fn write_library( 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 writer = BufWriter::new(fs::File::create(&writer_name)?); let mut e = snap::write::FrameEncoder::new(writer); // Write out the data using bincode bincode::serde::encode_into_std_write( - &library, + library, &mut e, bincode::config::standard() .with_little_endian() @@ -83,7 +83,7 @@ pub fn find_images(song_path: &PathBuf) -> Result<Vec<AlbumArt>, Box<dyn Error>> continue; } - let format = FileFormat::from_file(&path)?.kind(); + let format = FileFormat::from_file(path)?.kind(); if format != Kind::Image { break } diff --git a/src/music_tracker/music_tracker.rs b/src/music_tracker/music_tracker.rs index 37a3142..f5bee70 100644 --- a/src/music_tracker/music_tracker.rs +++ b/src/music_tracker/music_tracker.rs @@ -41,7 +41,7 @@ pub enum TrackerError { impl TrackerError { pub fn from_surf_error(error: surf::Error) -> TrackerError { - return match error.status() { + match error.status() { StatusCode::Forbidden => TrackerError::InvalidAuth, StatusCode::Unauthorized => TrackerError::InvalidAuth, StatusCode::NetworkAuthenticationRequired => TrackerError::InvalidAuth, @@ -50,7 +50,7 @@ impl TrackerError { StatusCode::ServiceUnavailable => TrackerError::ServiceUnavailable, StatusCode::NotFound => TrackerError::ServiceUnavailable, _ => TrackerError::Unknown, - }; + } } } @@ -85,8 +85,8 @@ impl MusicTracker for LastFM { }; params.insert("method", "track.scrobble"); - params.insert("artist", &artist); - params.insert("track", &track); + params.insert("artist", artist); + params.insert("track", track); params.insert("timestamp", &string_timestamp); return match self.api_request(params).await { @@ -104,8 +104,8 @@ impl MusicTracker for LastFM { }; params.insert("method", "track.updateNowPlaying"); - params.insert("artist", &artist); - params.insert("track", &track); + params.insert("artist", artist); + params.insert("track", track); return match self.api_request(params).await { Ok(_) => Ok(()), @@ -160,7 +160,7 @@ impl LastFM { auth_token.token ); - return Ok(auth_url); + Ok(auth_url) } /// Returns a LastFM session key @@ -186,15 +186,15 @@ impl LastFM { // Sets session key from received response let session_response: Session = serde_json::from_str(&response)?; - return Ok(session_response.session.key); + Ok(session_response.session.key) } /// Creates a new LastFM struct with a given config pub fn new(config: &LastFMConfig) -> LastFM { - let last_fm = LastFM { + + LastFM { config: config.clone(), - }; - return last_fm; + } } // Creates an api request with the given parameters @@ -221,9 +221,9 @@ impl LastFM { let url = "http://ws.audioscrobbler.com/2.0/"; - let response = surf::post(url).body_string(string_params).await; + - return response; + surf::post(url).body_string(string_params).await } // Returns an api signature as defined in the last.fm api documentation @@ -242,7 +242,7 @@ impl LastFM { let hash_result = md5_hasher.finalize(); let hashed_sig = format!("{:#02x}", hash_result); - return hashed_sig; + hashed_sig } // Removes last.fm account from dango-music-player @@ -265,11 +265,11 @@ pub struct DiscordRPC { impl DiscordRPC { pub fn new(config: &DiscordRPCConfig) -> Self { - let rpc = DiscordRPC { + + DiscordRPC { client: discord_presence::client::Client::new(config.dango_client_id), config: config.clone(), - }; - return rpc; + } } } @@ -307,8 +307,8 @@ impl MusicTracker for DiscordRPC { // Sets discord account activity to current playing song let send_activity = self.client.set_activity(|activity| { activity - .state(format!("{}", album)) - .details(format!("{}", song_name)) + .state(album.to_string()) + .details(song_name.to_string()) .assets(|assets| assets.large_image(&self.config.dango_icon)) .timestamps(|time| time.start(start_time)) }); @@ -425,10 +425,10 @@ impl ListenBrainz { request: &String, endpoint: &String, ) -> Result<surf::Response, surf::Error> { - let reponse = surf::post(format!("{}{}", &self.config.api_url, endpoint)) + + surf::post(format!("{}{}", &self.config.api_url, endpoint)) .body_string(request.clone()) .header("Authorization", format!("Token {}", self.config.auth_token)) - .await; - return reponse; + .await } } From 6b58ac02cf81d6051d68f5fbd14678c188f33054 Mon Sep 17 00:00:00 2001 From: G2-Games <ke0bhogsg@gmail.com> Date: Fri, 24 Nov 2023 14:48:15 -0600 Subject: [PATCH 028/136] Fixed `cargo clippy` suggestions --- src/lib.rs | 6 ++--- src/music_controller/config.rs | 7 +++--- .../{music_controller.rs => controller.rs} | 0 src/music_storage/music_db.rs | 24 ++++++++----------- src/music_storage/playlist.rs | 2 +- src/music_storage/utils.rs | 7 +++--- src/{music_tracker => }/music_tracker.rs | 6 ++--- 7 files changed, 23 insertions(+), 29 deletions(-) rename src/music_controller/{music_controller.rs => controller.rs} (100%) rename src/{music_tracker => }/music_tracker.rs (99%) diff --git a/src/lib.rs b/src/lib.rs index a40622d..b676c9f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,4 @@ -pub mod music_tracker { - pub mod music_tracker; -} +pub mod music_tracker; pub mod music_storage { pub mod music_db; @@ -10,5 +8,5 @@ pub mod music_storage { pub mod music_controller { pub mod config; - pub mod music_controller; + pub mod controller; } diff --git a/src/music_controller/config.rs b/src/music_controller/config.rs index f450c9d..9b934d4 100644 --- a/src/music_controller/config.rs +++ b/src/music_controller/config.rs @@ -4,7 +4,7 @@ use std::path::PathBuf; use serde::{Deserialize, Serialize}; -use crate::music_tracker::music_tracker::{DiscordRPCConfig, LastFMConfig, ListenBrainzConfig}; +use crate::music_tracker::{DiscordRPCConfig, LastFMConfig, ListenBrainzConfig}; #[derive(Serialize, Deserialize, PartialEq, Eq)] pub struct Config { @@ -66,9 +66,8 @@ impl Config { fs::write(&temp_file, toml)?; // If configuration file already exists, delete it - match fs::metadata(config_file) { - Ok(_) => fs::remove_file(config_file)?, - Err(_) => {} + if fs::metadata(config_file).is_ok() { + fs::remove_file(config_file)? } fs::rename(temp_file, config_file)?; diff --git a/src/music_controller/music_controller.rs b/src/music_controller/controller.rs similarity index 100% rename from src/music_controller/music_controller.rs rename to src/music_controller/controller.rs diff --git a/src/music_storage/music_db.rs b/src/music_storage/music_db.rs index f9ef854..2bc3bd9 100644 --- a/src/music_storage/music_db.rs +++ b/src/music_storage/music_db.rs @@ -199,6 +199,7 @@ pub struct Album<'a> { discs: BTreeMap<usize, Vec<&'a Song>>, } +#[allow(clippy::len_without_is_empty)] impl Album<'_> { /// Returns the album title pub fn title(&self) -> &String { @@ -383,7 +384,7 @@ impl MusicLibrary { } // TODO: Handle more of these errors }; } else if extension == "cue" { - total += match self.add_cuesheet(&target_file.path().to_path_buf()) { + total += match self.add_cuesheet(target_file.path()) { Ok(added) => added, Err(error) => { errors += 1; @@ -457,7 +458,7 @@ impl MusicLibrary { } // Find images around the music file that can be used - let mut found_images = find_images(&target_file.to_path_buf()).unwrap(); + let mut found_images = find_images(target_file).unwrap(); album_art.append(&mut found_images); // Get the format as a string @@ -497,10 +498,10 @@ impl MusicLibrary { Ok(()) } - pub fn add_cuesheet(&mut self, cuesheet: &PathBuf) -> Result<usize, Box<dyn Error>> { + pub fn add_cuesheet(&mut self, cuesheet: &Path) -> Result<usize, Box<dyn Error>> { let mut tracks_added = 0; - let cue_data = parse_from_file(&cuesheet.as_path().to_string_lossy(), false).unwrap(); + let cue_data = parse_from_file(&cuesheet.to_string_lossy(), false).unwrap(); // Get album level information let album_title = &cue_data.title; @@ -515,10 +516,7 @@ impl MusicLibrary { } // Try to remove the original audio file from the db if it exists - match self.remove_uri(&URI::Local(audio_location.clone())) { - Ok(_) => tracks_added -= 1, - Err(_) => () - }; + if self.remove_uri(&URI::Local(audio_location.clone())).is_ok() { tracks_added -= 1 } let next_track = file.tracks.clone(); let mut next_track = next_track.iter().skip(1); @@ -626,12 +624,10 @@ impl MusicLibrary { } pub fn add_song(&mut self, new_song: Song) -> Result<(), Box<dyn Error>> { - match self.query_uri(&new_song.location) { - Some(_) => { - return Err(format!("URI already in database: {:?}", new_song.location).into()) - } - None => (), + if self.query_uri(&new_song.location).is_some() { + return Err(format!("URI already in database: {:?}", new_song.location).into()) } + 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()) @@ -854,7 +850,7 @@ impl MusicLibrary { /// Queries a list of albums by title pub fn query_albums( &self, - query_string: &String, // The query itself + query_string: &str, // The query itself ) -> Result<Vec<Album>, Box<dyn Error>> { let all_albums = self.albums(); diff --git a/src/music_storage/playlist.rs b/src/music_storage/playlist.rs index 189b9e8..4f8892d 100644 --- a/src/music_storage/playlist.rs +++ b/src/music_storage/playlist.rs @@ -1,6 +1,6 @@ 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: &[&Path]) { unimplemented!() } diff --git a/src/music_storage/utils.rs b/src/music_storage/utils.rs index f4a7947..0687db4 100644 --- a/src/music_storage/utils.rs +++ b/src/music_storage/utils.rs @@ -1,5 +1,6 @@ use std::io::{BufReader, BufWriter}; -use std::{error::Error, fs, path::PathBuf}; +use std::path::{Path, PathBuf}; +use std::{error::Error, fs}; use walkdir::WalkDir; use file_format::{FileFormat, Kind}; @@ -8,7 +9,7 @@ use snap; use super::music_db::{Song, AlbumArt, URI}; use unidecode::unidecode; -pub(super) fn normalize(input_string: &String) -> String { +pub(super) fn normalize(input_string: &str) -> String { let mut normalized = unidecode(input_string); // Remove non alphanumeric characters @@ -65,7 +66,7 @@ pub(super) fn write_library( Ok(()) } -pub fn find_images(song_path: &PathBuf) -> Result<Vec<AlbumArt>, Box<dyn Error>> { +pub fn find_images(song_path: &Path) -> Result<Vec<AlbumArt>, Box<dyn Error>> { let mut images: Vec<AlbumArt> = Vec::new(); let song_dir = song_path.parent().ok_or("")?; diff --git a/src/music_tracker/music_tracker.rs b/src/music_tracker.rs similarity index 99% rename from src/music_tracker/music_tracker.rs rename to src/music_tracker.rs index f5bee70..3899f44 100644 --- a/src/music_tracker/music_tracker.rs +++ b/src/music_tracker.rs @@ -422,12 +422,12 @@ impl ListenBrainz { // Makes an api request to configured url with given json pub async fn api_request( &self, - request: &String, + request: &str, endpoint: &String, ) -> Result<surf::Response, surf::Error> { - + surf::post(format!("{}{}", &self.config.api_url, endpoint)) - .body_string(request.clone()) + .body_string(request.to_owned()) .header("Authorization", format!("Token {}", self.config.auth_token)) .await } From e82476f2a6f646cbb6ebbc72548bca19ad20397e Mon Sep 17 00:00:00 2001 From: G2-Games <ke0bhogsg@gmail.com> Date: Fri, 24 Nov 2023 19:25:35 -0600 Subject: [PATCH 029/136] Removed unused dependencies --- Cargo.toml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index cfde222..9860318 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,25 +18,17 @@ serde = { version = "1.0.191", features = ["derive"] } time = "0.3.22" toml = "0.7.5" walkdir = "2.4.0" -cpal = "0.15.2" -heapless = "0.7.16" -rb = "0.4.1" -symphonia = { version = "0.5.3", features = ["all-codecs"] } serde_json = "1.0.104" async-std = "1.12.0" async-trait = "0.1.73" md-5 = "0.10.5" surf = "2.3.2" -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 = { version = "2.0.0-rc.3", features = ["serde"] } unidecode = "0.3.0" rayon = "1.8.0" log = "0.4" -pretty_env_logger = "0.4" base64 = "0.21.5" snap = "1.1.0" rcue = "0.1.3" From 64a3b67241a334cdc20ddb63fede8f75a11ae603 Mon Sep 17 00:00:00 2001 From: G2-Games <ke0bhogsg@gmail.com> Date: Sat, 25 Nov 2023 17:43:28 -0600 Subject: [PATCH 030/136] Removed `music_tracker.rs`, removed unused deps --- Cargo.toml | 7 - src/lib.rs | 2 - src/music_controller/config.rs | 19 -- src/music_tracker.rs | 434 --------------------------------- 4 files changed, 462 deletions(-) delete mode 100644 src/music_tracker.rs diff --git a/Cargo.toml b/Cargo.toml index 9860318..90ec0d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,15 +15,8 @@ categories = ["multimedia::audio"] file-format = { version = "0.22.0", features = ["reader-asf", "reader-ebml", "reader-mp4", "reader-rm", "reader-txt", "reader-xml", "serde"] } lofty = "0.17.0" serde = { version = "1.0.191", features = ["derive"] } -time = "0.3.22" toml = "0.7.5" walkdir = "2.4.0" -serde_json = "1.0.104" -async-std = "1.12.0" -async-trait = "0.1.73" -md-5 = "0.10.5" -surf = "2.3.2" -discord-presence = "0.5.18" chrono = { version = "0.4.31", features = ["serde"] } bincode = { version = "2.0.0-rc.3", features = ["serde"] } unidecode = "0.3.0" diff --git a/src/lib.rs b/src/lib.rs index b676c9f..d6c8bb0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,3 @@ -pub mod music_tracker; - pub mod music_storage { pub mod music_db; pub mod playlist; diff --git a/src/music_controller/config.rs b/src/music_controller/config.rs index 9b934d4..2014de7 100644 --- a/src/music_controller/config.rs +++ b/src/music_controller/config.rs @@ -4,14 +4,9 @@ use std::path::PathBuf; use serde::{Deserialize, Serialize}; -use crate::music_tracker::{DiscordRPCConfig, LastFMConfig, ListenBrainzConfig}; - #[derive(Serialize, Deserialize, PartialEq, Eq)] pub struct Config { pub db_path: Box<PathBuf>, - pub lastfm: Option<LastFMConfig>, - pub discord: Option<DiscordRPCConfig>, - pub listenbrainz: Option<ListenBrainzConfig>, } impl Default for Config { @@ -20,20 +15,6 @@ impl Default for Config { 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(""), - }), } } } diff --git a/src/music_tracker.rs b/src/music_tracker.rs deleted file mode 100644 index 3899f44..0000000 --- a/src/music_tracker.rs +++ /dev/null @@ -1,434 +0,0 @@ -use serde_json::json; -use std::collections::BTreeMap; -use std::time::{SystemTime, UNIX_EPOCH}; - -use async_trait::async_trait; -use discord_presence::Event; -use md5::{Digest, Md5}; -use serde::{Deserialize, Serialize}; -use surf::StatusCode; - -use crate::music_storage::music_db::{Song, Tag}; - -#[async_trait] -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<u32, TrackerError>; -} - -#[derive(Debug)] -pub enum TrackerError { - /// Tracker does not accept the song's format/content - InvalidSong, - /// Tracker requires authentication - InvalidAuth, - /// Tracker request was malformed - InvalidRequest, - /// Tracker is unavailable - ServiceUnavailable, - /// Unknown tracker error - Unknown, -} - -impl TrackerError { - pub fn from_surf_error(error: surf::Error) -> TrackerError { - match error.status() { - StatusCode::Forbidden => TrackerError::InvalidAuth, - StatusCode::Unauthorized => TrackerError::InvalidAuth, - StatusCode::NetworkAuthenticationRequired => TrackerError::InvalidAuth, - StatusCode::BadRequest => TrackerError::InvalidRequest, - StatusCode::BadGateway => TrackerError::ServiceUnavailable, - StatusCode::ServiceUnavailable => TrackerError::ServiceUnavailable, - StatusCode::NotFound => TrackerError::ServiceUnavailable, - _ => TrackerError::Unknown, - } - } -} - -#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)] -pub struct LastFMConfig { - pub enabled: bool, - pub dango_api_key: String, - pub shared_secret: String, - pub session_key: String, -} - -pub struct LastFM { - 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 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), - }; - - 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), - }; - - 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<u32, TrackerError> { - todo!(); - } -} - -#[derive(Deserialize, Serialize)] -struct AuthToken { - token: String, -} - -#[derive(Deserialize, Serialize, Debug)] -struct SessionResponse { - name: String, - key: String, - subscriber: i32, -} - -#[derive(Deserialize, Serialize, Debug)] -struct Session { - 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<String, surf::Error> { - 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 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 - ); - - Ok(auth_url) - } - - /// Returns a LastFM session key - pub async fn get_session_key( - api_key: &String, - shared_secret: &String, - auth_token: &String, - ) -> Result<String, surf::Error> { - 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}"); - - // 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)?; - Ok(session_response.session.key) - } - - /// Creates a new LastFM struct with a given config - pub fn new(config: &LastFMConfig) -> LastFM { - - LastFM { - config: config.clone(), - } - } - - // Creates an api request with the given parameters - pub async fn api_request( - &self, - mut params: BTreeMap<&str, &str>, - ) -> Result<surf::Response, surf::Error> { - 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/"; - - - - surf::post(url).body_string(string_params).await - } - - // Returns an api signature as defined in the last.fm api documentation - fn request_sig(params: &BTreeMap<&str, &str>, shared_secret: &str) -> String { - let mut sig_string = String::new(); - // Appends keys and values of parameters to the unhashed sig - for key in params.keys() { - let param_value = params.get(*key); - 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); - - hashed_sig - } - - // Removes last.fm account from dango-music-player - pub fn reset_account() { - todo!(); - } -} - -#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)] -pub struct DiscordRPCConfig { - pub enabled: bool, - pub dango_client_id: u64, - pub dango_icon: String, -} - -pub struct DiscordRPC { - config: DiscordRPCConfig, - pub client: discord_presence::client::Client, -} - -impl DiscordRPC { - pub fn new(config: &DiscordRPCConfig) -> Self { - - DiscordRPC { - client: discord_presence::client::Client::new(config.dango_client_id), - config: config.clone(), - } - } -} - -#[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(&Tag::Title) { - song_name - } else { - &unknown - }; - - // Sets album - let album = if let Some(album) = song.get_tag(&Tag::Album) { - album - } 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; - // Sets discord account activity to current playing song - let send_activity = self.client.set_activity(|activity| { - activity - .state(album.to_string()) - .details(song_name.to_string()) - .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(()); - } - - async fn test_tracker(&mut self) -> Result<(), TrackerError> { - return Ok(()); - } - - async fn get_times_tracked(&mut self, _song: &Song) -> Result<u32, TrackerError> { - return Ok(0); - } -} - -#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)] -pub struct ListenBrainzConfig { - pub enabled: bool, - pub api_url: String, - pub auth_token: String, -} - -pub struct ListenBrainz { - config: ListenBrainzConfig, -} - -#[async_trait] -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), - }; - // Creates a json to submit a single song as defined in the listenbrainz documentation - let json_req = json!({ - "listen_type": "playing_now", - "payload": [ - { - "track_metadata": { - "artist_name": artist, - "track_name": track, - } - } - ] - }); - - 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)), - }; - } - - 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(&Tag::Artist), song.get_tag(&Tag::Title)) { - (Some(artist), Some(track)) => (artist, track), - _ => return Err(TrackerError::InvalidSong), - }; - - let json_req = json!({ - "listen_type": "single", - "payload": [ - { - "listened_at": timestamp, - "track_metadata": { - "artist_name": artist, - "track_name": track, - } - } - ] - }); - - 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)), - }; - } - async fn test_tracker(&mut self) -> Result<(), TrackerError> { - todo!() - } - async fn get_times_tracked(&mut self, _song: &Song) -> Result<u32, TrackerError> { - todo!() - } -} - -impl ListenBrainz { - pub fn new(config: &ListenBrainzConfig) -> Self { - ListenBrainz { - config: config.clone(), - } - } - // Makes an api request to configured url with given json - pub async fn api_request( - &self, - request: &str, - endpoint: &String, - ) -> Result<surf::Response, surf::Error> { - - surf::post(format!("{}{}", &self.config.api_url, endpoint)) - .body_string(request.to_owned()) - .header("Authorization", format!("Token {}", self.config.auth_token)) - .await - } -} From 6845d34ce7f6611647228c649445f2b871da3523 Mon Sep 17 00:00:00 2001 From: G2-Games <ke0bhogsg@gmail.com> Date: Thu, 30 Nov 2023 02:32:06 -0600 Subject: [PATCH 031/136] Added enum for fields on `Song` structs, `Field` --- Cargo.toml | 4 +- src/music_storage/music_db.rs | 197 +++++++++++++++++++++++----------- src/music_storage/utils.rs | 1 + 3 files changed, 137 insertions(+), 65 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 90ec0d7..192d833 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,13 +7,13 @@ description = "A music backend that manages storage, querying, and playback of r homepage = "https://dangoware.com/dango-music-player" documentation = "https://docs.rs/dango-core" readme = "README.md" -repository = "https://github.com/DangoWare/dango-music-player" +repository = "https://github.com/Dangoware/dango-music-player" keywords = ["audio", "music"] categories = ["multimedia::audio"] [dependencies] file-format = { version = "0.22.0", features = ["reader-asf", "reader-ebml", "reader-mp4", "reader-rm", "reader-txt", "reader-xml", "serde"] } -lofty = "0.17.0" +lofty = "0.17.1" serde = { version = "1.0.191", features = ["derive"] } toml = "0.7.5" walkdir = "2.4.0" diff --git a/src/music_storage/music_db.rs b/src/music_storage/music_db.rs index 2bc3bd9..17275ae 100644 --- a/src/music_storage/music_db.rs +++ b/src/music_storage/music_db.rs @@ -1,5 +1,5 @@ // Crate things -use super::utils::{normalize, read_library, write_library, find_images}; +use super::utils::{find_images, normalize, read_library, write_library}; use crate::music_controller::config::Config; // Various std things @@ -8,15 +8,15 @@ use std::error::Error; use std::ops::ControlFlow::{Break, Continue}; // Files -use rcue::parser::parse_from_file; use file_format::{FileFormat, Kind}; -use walkdir::WalkDir; -use lofty::{AudioFile, ItemKey, ItemValue, Probe, TagType, TaggedFileExt, ParseOptions}; +use lofty::{AudioFile, ItemKey, ItemValue, ParseOptions, Probe, TagType, TaggedFileExt}; +use rcue::parser::parse_from_file; use std::fs; use std::path::{Path, PathBuf}; +use walkdir::WalkDir; // Time -use chrono::{serde::ts_seconds_option, DateTime, Utc}; +use chrono::{serde::ts_milliseconds_option, DateTime, Utc}; use std::time::Duration; // Serialization/Compression @@ -42,6 +42,8 @@ impl AlbumArt { } } +/// A tag for a song +#[non_exhaustive] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)] pub enum Tag { Title, @@ -73,6 +75,42 @@ impl ToString for Tag { } } +/// A field within a Song struct +pub enum Field { + Location(URI), + Plays(i32), + Skips(i32), + Favorited(bool), + Rating(u8), + Format(FileFormat), + Duration(Duration), + PlayTime(Duration), + LastPlayed(DateTime<Utc>), + DateAdded(DateTime<Utc>), + DateModified(DateTime<Utc>), +} + +impl ToString for Field { + fn to_string(&self) -> String { + match self { + Self::Location(location) => location.to_string(), + Self::Plays(plays) => plays.to_string(), + Self::Skips(skips) => skips.to_string(), + Self::Favorited(fav) => fav.to_string(), + Self::Rating(rating) => rating.to_string(), + Self::Format(format) => match format.short_name() { + Some(name) => name.to_string(), + None => format.to_string() + }, + Self::Duration(duration) => duration.as_millis().to_string(), + Self::PlayTime(time) => time.as_millis().to_string(), + Self::LastPlayed(last) => last.to_rfc2822(), + Self::DateAdded(added) => added.to_rfc2822(), + Self::DateModified(modified) => modified.to_rfc2822(), + } + } +} + /// Stores information about a single song #[derive(Debug, Clone, Deserialize, Serialize)] pub struct Song { @@ -84,43 +122,53 @@ pub struct Song { pub format: Option<FileFormat>, pub duration: Duration, pub play_time: Duration, - #[serde(with = "ts_seconds_option")] + #[serde(with = "ts_milliseconds_option")] pub last_played: Option<DateTime<Utc>>, - #[serde(with = "ts_seconds_option")] + #[serde(with = "ts_milliseconds_option")] pub date_added: Option<DateTime<Utc>>, - #[serde(with = "ts_seconds_option")] + #[serde(with = "ts_milliseconds_option")] pub date_modified: Option<DateTime<Utc>>, pub album_art: Vec<AlbumArt>, pub tags: BTreeMap<Tag, String>, } impl Song { - /** - * Get a tag's value - * - * ``` - * // Assuming an already created song: - * - * let tag = this_song.get_tag(Tag::Title); - * - * assert_eq!(tag, "Some Song Title"); - * ``` - **/ + /// Get a tag's value + /// + /// ``` + /// use dango_core::music_storage::music_db::Tag; + /// // Assuming an already created song: + /// + /// let tag = this_song.get_tag(Tag::Title); + /// + /// assert_eq!(tag, "Some Song Title"); + /// ``` pub fn get_tag(&self, target_key: &Tag) -> Option<&String> { self.tags.get(target_key) } - pub fn get_field(&self, target_field: &str) -> Option<String> { - match target_field { - "location" => Some(self.location.clone().path_string()), - "plays" => Some(self.plays.clone().to_string()), - "format" => match self.format { - Some(format) => format.short_name().map(|short| short.to_string()), - None => None, - }, + pub fn get_field(&self, target_field: &str) -> Option<Field> { + let lower_target = target_field.to_lowercase(); + match lower_target.as_str() { + "location" => Some(Field::Location(self.location.clone())), + "plays" => Some(Field::Plays(self.plays)), + "skips" => Some(Field::Skips(self.skips)), + "favorited" => Some(Field::Favorited(self.favorited)), + "rating" => self.rating.map(Field::Rating), + "duration" => Some(Field::Duration(self.duration)), + "play_time" => Some(Field::PlayTime(self.play_time)), + "format" => self.format.map(Field::Format), _ => todo!(), // Other field types are not yet supported } } + + pub fn set_tag(&mut self, target_key: Tag, new_value: String) { + self.tags.insert(target_key, new_value); + } + + pub fn remove_tag(&mut self, target_key: &Tag) { + self.tags.remove(target_key); + } } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] @@ -172,8 +220,10 @@ impl URI { URI::Remote(_, location) => location, } } +} - pub fn path_string(&self) -> String { +impl ToString for URI { + fn to_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(), @@ -302,12 +352,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 = self.library.par_iter().enumerate().try_for_each(|(i, track)| { - if path == &track.location { - return std::ops::ControlFlow::Break((track, i)); - } - Continue(()) - }); + let result = self + .library + .par_iter() + .enumerate() + .try_for_each(|(i, track)| { + if path == &track.location { + return std::ops::ControlFlow::Break((track, i)); + } + Continue(()) + }); match result { Break(song) => Some(song), @@ -435,7 +489,11 @@ impl MusicLibrary { ItemKey::Comment => Tag::Comment, ItemKey::AlbumTitle => Tag::Album, ItemKey::DiscNumber => Tag::Disk, - ItemKey::Unknown(unknown) if unknown == "ACOUSTID_FINGERPRINT" || unknown == "Acoustid Fingerprint" => continue, + ItemKey::Unknown(unknown) + if unknown == "ACOUSTID_FINGERPRINT" || unknown == "Acoustid Fingerprint" => + { + continue + } ItemKey::Unknown(unknown) => Tag::Key(unknown.to_string()), custom => Tag::Key(format!("{:?}", custom)), }; @@ -492,7 +550,7 @@ impl MusicLibrary { Ok(_) => (), Err(_) => { //return Err(error) - }, + } }; Ok(()) @@ -516,7 +574,9 @@ impl MusicLibrary { } // Try to remove the original audio file from the db if it exists - if self.remove_uri(&URI::Local(audio_location.clone())).is_ok() { tracks_added -= 1 } + if self.remove_uri(&URI::Local(audio_location.clone())).is_ok() { + tracks_added -= 1 + } let next_track = file.tracks.clone(); let mut next_track = next_track.iter().skip(1); @@ -538,19 +598,17 @@ impl MusicLibrary { let duration = match next_track.next() { Some(future) => match future.indices.get(0) { Some(val) => val.1 - start, - None => Duration::from_secs(0) - } - None => { - match lofty::read_from_path(audio_location) { + None => Duration::from_secs(0), + }, + None => match lofty::read_from_path(audio_location) { + Ok(tagged_file) => tagged_file.properties().duration() - start, + + Err(_) => match Probe::open(audio_location)?.read() { Ok(tagged_file) => tagged_file.properties().duration() - start, - Err(_) => match Probe::open(audio_location)?.read() { - Ok(tagged_file) => tagged_file.properties().duration() - start, - - Err(_) => Duration::from_secs(0), - }, - } - } + Err(_) => Duration::from_secs(0), + }, + }, }; let end = start + duration + postgap; @@ -563,11 +621,15 @@ impl MusicLibrary { // Get some useful tags let mut tags: BTreeMap<Tag, String> = BTreeMap::new(); match album_title { - Some(title) => {tags.insert(Tag::Album, title.clone());}, + Some(title) => { + tags.insert(Tag::Album, title.clone()); + } None => (), } match album_artist { - Some(artist) => {tags.insert(Tag::Album, artist.clone());}, + Some(artist) => { + tags.insert(Tag::Album, artist.clone()); + } None => (), } tags.insert(Tag::Track, track.no.parse().unwrap_or((i + 1).to_string())); @@ -625,7 +687,7 @@ impl MusicLibrary { pub fn add_song(&mut self, new_song: Song) -> Result<(), Box<dyn Error>> { if self.query_uri(&new_song.location).is_some() { - return Err(format!("URI already in database: {:?}", new_song.location).into()) + return Err(format!("URI already in database: {:?}", new_song.location).into()); } match new_song.location { @@ -653,11 +715,17 @@ impl MusicLibrary { } /// Scan the song by a location and update its tags - pub fn update_uri(&mut self, target_uri: &URI, new_tags: Vec<Tag>) -> Result<(), Box<dyn std::error::Error>> { - match self.query_uri(target_uri) { - Some(_) => (), + pub fn update_uri( + &mut self, + target_uri: &URI, + new_tags: Vec<Tag>, + ) -> Result<(), Box<dyn std::error::Error>> { + let target_song = match self.query_uri(target_uri) { + Some(song) => song, None => return Err("URI not in database!".to_string().into()), - } + }; + + println!("{:?}", target_song.0.location); for tag in new_tags { println!("{:?}", tag); @@ -673,6 +741,7 @@ impl MusicLibrary { /// /// Example: /// ``` + /// use dango_core::music_storage::music_db::Tag; /// query_tracks( /// &String::from("query"), /// &vec![ @@ -702,7 +771,7 @@ impl MusicLibrary { for tag in target_tags { let track_result = match tag { Tag::Field(target) => match track.get_field(target) { - Some(value) => value, + Some(value) => value.to_string(), None => continue, }, _ => match track.get_tag(tag) { @@ -723,7 +792,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; } @@ -738,7 +809,7 @@ impl MusicLibrary { 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, + Some(field_value) => field_value.to_string(), None => continue, }, _ => match a.get_tag(sort_option) { @@ -749,7 +820,7 @@ impl MusicLibrary { let tag_b = match sort_option { Tag::Field(field_selection) => match b.get_field(field_selection) { - Some(field_value) => field_value, + Some(field_value) => field_value.to_string(), None => continue, }, _ => match b.get_tag(sort_option) { @@ -768,8 +839,8 @@ impl MusicLibrary { } // If all tags are equal, sort by Track number - let path_a = PathBuf::from(a.get_field("location").unwrap()); - let path_b = PathBuf::from(b.get_field("location").unwrap()); + let path_a = PathBuf::from(a.get_field("location").unwrap().to_string()); + let path_b = PathBuf::from(b.get_field("location").unwrap().to_string()); path_a.file_name().cmp(&path_b.file_name()) }); @@ -834,8 +905,8 @@ impl MusicLibrary { 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()); + let path_a = PathBuf::from(a.get_field("location").unwrap().to_string()); + let path_b = PathBuf::from(b.get_field("location").unwrap().to_string()); path_a.file_name().cmp(&path_b.file_name()) } diff --git a/src/music_storage/utils.rs b/src/music_storage/utils.rs index 0687db4..5c4865a 100644 --- a/src/music_storage/utils.rs +++ b/src/music_storage/utils.rs @@ -14,6 +14,7 @@ pub(super) fn normalize(input_string: &str) -> String { // Remove non alphanumeric characters normalized.retain(|c| c.is_alphanumeric()); + normalized = normalized.to_ascii_lowercase(); normalized } From abfad7587d4e2360602fad186fc4344b4d4df956 Mon Sep 17 00:00:00 2001 From: G2-Games <ke0bhogsg@gmail.com> Date: Thu, 30 Nov 2023 14:35:40 -0600 Subject: [PATCH 032/136] Updated tag parse error handling, fixes #19 --- src/music_storage/music_db.rs | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/src/music_storage/music_db.rs b/src/music_storage/music_db.rs index 17275ae..47b1a9a 100644 --- a/src/music_storage/music_db.rs +++ b/src/music_storage/music_db.rs @@ -460,22 +460,27 @@ impl MusicLibrary { pub fn add_file(&mut self, target_file: &Path) -> Result<(), Box<dyn Error>> { let normal_options = ParseOptions::new().parsing_mode(lofty::ParsingMode::Relaxed); - // TODO: Fix error handling here - let tagged_file = match Probe::open(target_file)?.options(normal_options).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, + let tagged_file: lofty::TaggedFile; + let mut duration = Duration::from_secs(0); + let tag = match Probe::open(target_file)?.options(normal_options).read() { + Ok(file) => { + tagged_file = file; - None => match tagged_file.first_tag() { - Some(first_tag) => first_tag, - None => blank_tag, + duration = tagged_file.properties().duration(); + + // Ensure the tags exist, if not, insert blank data + match tagged_file.primary_tag() { + Some(primary_tag) => primary_tag, + + None => match tagged_file.first_tag() { + Some(first_tag) => first_tag, + None => blank_tag, + }, + } }, + + Err(_) => blank_tag, }; let mut tags: BTreeMap<Tag, String> = BTreeMap::new(); @@ -525,8 +530,6 @@ impl MusicLibrary { Err(_) => None, }; - let duration = tagged_file.properties().duration(); - // TODO: Fix error handling let binding = fs::canonicalize(target_file).unwrap(); From f9ca472db4e5beb8bda66d2bb52fd8163343121e Mon Sep 17 00:00:00 2001 From: G2-Games <ke0bhogsg@gmail.com> Date: Thu, 30 Nov 2023 14:38:54 -0600 Subject: [PATCH 033/136] Removed `.mid` from blocked extensions --- src/music_storage/music_db.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/music_storage/music_db.rs b/src/music_storage/music_db.rs index 47b1a9a..912070e 100644 --- a/src/music_storage/music_db.rs +++ b/src/music_storage/music_db.rs @@ -294,7 +294,7 @@ impl Album<'_> { } } -const BLOCKED_EXTENSIONS: [&str; 5] = ["vob", "log", "txt", "sf2", "mid"]; +const BLOCKED_EXTENSIONS: [&str; 5] = ["vob", "log", "txt", "sf2"]; #[derive(Debug)] pub struct MusicLibrary { From c0d7f01b38ebf887f5431eb5c45fcd499649a5b2 Mon Sep 17 00:00:00 2001 From: G2-Games <ke0bhogsg@gmail.com> Date: Thu, 30 Nov 2023 14:41:33 -0600 Subject: [PATCH 034/136] Fixed array length for blocked extensions --- src/music_storage/music_db.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/music_storage/music_db.rs b/src/music_storage/music_db.rs index 912070e..44939df 100644 --- a/src/music_storage/music_db.rs +++ b/src/music_storage/music_db.rs @@ -294,7 +294,7 @@ impl Album<'_> { } } -const BLOCKED_EXTENSIONS: [&str; 5] = ["vob", "log", "txt", "sf2"]; +const BLOCKED_EXTENSIONS: [&str; 4] = ["vob", "log", "txt", "sf2"]; #[derive(Debug)] pub struct MusicLibrary { From 8071a90dfebf3069e4a622452222c9380e49b435 Mon Sep 17 00:00:00 2001 From: G2-Games <ke0bhogsg@gmail.com> Date: Thu, 30 Nov 2023 23:34:09 -0600 Subject: [PATCH 035/136] Implemented GStreamer playback --- Cargo.toml | 2 + src/lib.rs | 2 + src/music_player.rs | 177 ++++++++++++++++++++++++++++++++++ src/music_storage/music_db.rs | 23 +++-- 4 files changed, 197 insertions(+), 7 deletions(-) create mode 100644 src/music_player.rs diff --git a/Cargo.toml b/Cargo.toml index 192d833..ba7c5fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,3 +25,5 @@ log = "0.4" base64 = "0.21.5" snap = "1.1.0" rcue = "0.1.3" +gstreamer = "0.21.2" +glib = "0.18.3" diff --git a/src/lib.rs b/src/lib.rs index d6c8bb0..d98209c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,3 +8,5 @@ pub mod music_controller { pub mod config; pub mod controller; } + +pub mod music_player; diff --git a/src/music_player.rs b/src/music_player.rs new file mode 100644 index 0000000..6ec9670 --- /dev/null +++ b/src/music_player.rs @@ -0,0 +1,177 @@ +// Crate things +use crate::music_controller::config::Config; +use crate::music_storage::music_db::URI; +use std::error::Error; +use std::sync::mpsc::{Sender, self, Receiver}; + +// GStreamer things +use gst::{ClockTime, Element}; +use gstreamer as gst; +use gstreamer::prelude::*; +use glib::FlagsClass; + +// Time things +use chrono::Duration; + +enum PlayerCmd { + Play, +} + +/// An instance of a music player with a GStreamer backend +pub struct Player { + source: Option<URI>, + events: Sender<PlayerCmd>, + playbin: Element, + position: Duration, + duration: Duration, + paused: bool, + volume: f64, + gapless: bool, +} + +impl Default for Player { + fn default() -> Self { + Self::new() + } +} + + +impl Player { + pub fn new() -> Self { + gst::init().unwrap(); + + let playbin = gst::ElementFactory::make("playbin") + .build() + .unwrap(); + + let flags = playbin.property_value("flags"); + let flags_class = FlagsClass::with_type(flags.type_()).unwrap(); + + let flags = flags_class + .builder_with_value(flags) + .unwrap() + .set_by_nick("audio") + .set_by_nick("download") + .unset_by_nick("video") + .unset_by_nick("text") + .build() + .unwrap(); + playbin.set_property_from_value("flags", &flags); + + playbin + .bus() + .expect("Failed to get GStreamer message bus"); + + let source = None; + let (tx, _): (Sender<PlayerCmd>, Receiver<PlayerCmd>) = mpsc::channel(); + Self { + source, + events: tx, + playbin, + paused: false, + volume: 0.5, + gapless: false, + position: Duration::seconds(0), + duration: Duration::seconds(0), + } + } + + pub fn enqueue_next(&mut self, next_track: URI) { + self.set_state(gst::State::Ready); + + self.playbin.set_property("uri", next_track.as_uri()); + + self.play(); + } + + + /// Set the playback volume, accepts a float from 0 to 1 + pub fn set_volume(&mut self, volume: f64) { + self.volume = volume.clamp(0.0, 1.0); + self.set_gstreamer_volume(self.volume); + } + + /// Set volume of the internal playbin player, can be + /// used to bypass the main volume control for seeking + fn set_gstreamer_volume(&mut self, volume: f64) { + self.playbin.set_property("volume", volume) + } + + /// Returns the current volume level, a float from 0 to 1 + pub fn volume(&mut self) -> f64 { + self.volume + } + + fn set_state(&mut self, state: gst::State) { + self.playbin + .set_state(state) + .expect("Unable to set the pipeline state"); + } + + /// If the player is paused or stopped, starts playback + pub fn play(&mut self) { + self.set_state(gst::State::Playing); + } + + /// Pause, if playing + pub fn pause(&mut self) { + self.paused = true; + self.set_state(gst::State::Paused); + } + + /// Resume from being paused + pub fn resume(&mut self) { + self.paused = false; + self.set_state(gst::State::Playing); + } + + /// Check if playback is paused + pub fn is_paused(&mut self) -> bool { + self.playbin.current_state() == gst::State::Paused + } + + /// Set the playback URI + pub fn set_source(&mut self, source: URI) { + self.source = Some(source.clone()); + self.playbin.set_property("uri", source.as_uri()) + } + + /// Get the current playback position of the player + pub fn position(&mut self) -> Option<Duration> { + self.playbin.query_position::<ClockTime>().map(|pos| Duration::nanoseconds(pos.nseconds() as i64)) + } + + /// Get the duration of the currently playing track + pub fn duration(&mut self) -> Option<Duration> { + self.playbin.query_duration::<ClockTime>().map(|pos| Duration::milliseconds(pos.mseconds() as i64)) + } + + /// Seek relative to the current position + pub fn seek_by(&mut self, seek_amount: Duration) -> Result<(), Box<dyn Error>> { + let time_pos = match self.position() { + Some(pos) => pos, + None => return Err("No position".into()) + }; + let seek_pos = time_pos + seek_amount; + + self.seek_to(seek_pos)?; + Ok(()) + } + + /// Seek absolutely + pub fn seek_to(&mut self, last_pos: Duration) -> Result<(), Box<dyn Error>> { + let duration = match self.duration() { + Some(dur) => dur, + None => return Err("No duration".into()) + }; + let seek_pos = last_pos.clamp(Duration::seconds(0), duration); + + let seek_pos_clock = ClockTime::from_mseconds(seek_pos.num_milliseconds() as u64); + self.set_gstreamer_volume(0.0); + self + .playbin + .seek_simple(gst::SeekFlags::FLUSH, seek_pos_clock)?; + self.set_gstreamer_volume(self.volume); + Ok(()) + } +} diff --git a/src/music_storage/music_db.rs b/src/music_storage/music_db.rs index 44939df..21df4f5 100644 --- a/src/music_storage/music_db.rs +++ b/src/music_storage/music_db.rs @@ -180,7 +180,7 @@ pub enum URI { start: Duration, end: Duration, }, - Remote(Service, PathBuf), + Remote(Service, String), } impl URI { @@ -213,13 +213,22 @@ impl URI { } /// Returns the location as a PathBuf - pub fn path(&self) -> &PathBuf { + pub fn path(&self) -> PathBuf { match self { - URI::Local(location) => location, - URI::Cue { location, .. } => location, - URI::Remote(_, location) => location, + URI::Local(location) => location.clone(), + URI::Cue { location, .. } => location.clone(), + URI::Remote(_, location) => PathBuf::from(location), } } + + pub fn as_uri(&self) -> String { + let path_str = match self { + URI::Local(location) => format!("file://{}", location.as_path().to_string_lossy()), + URI::Cue { location, .. } => format!("file://{}", location.as_path().to_string_lossy()), + URI::Remote(_, location) => location.clone(), + }; + path_str.to_string() + } } impl ToString for URI { @@ -227,7 +236,7 @@ impl ToString for URI { let path_str = match self { URI::Local(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(), + URI::Remote(_, location) => location.into(), }; path_str.to_string() } @@ -371,7 +380,7 @@ impl MusicLibrary { /// Queries for a [Song] by its [PathBuf], returning a `Vec<Song>` /// with matching `PathBuf`s - fn query_path(&self, path: &PathBuf) -> Option<Vec<&Song>> { + fn query_path(&self, path: PathBuf) -> Option<Vec<&Song>> { let result: Arc<Mutex<Vec<&Song>>> = Arc::new(Mutex::new(Vec::new())); self.library.par_iter().for_each(|track| { if path == track.location.path() { From 7dbed859d4a4d36ee09fd15f92af48ea838df6ab Mon Sep 17 00:00:00 2001 From: G2-Games <ke0bhogsg@gmail.com> Date: Sat, 2 Dec 2023 02:20:58 -0600 Subject: [PATCH 036/136] Gapless playback now possible, can play CUE tracks --- src/music_player.rs | 119 ++++++++++++++++++++++++++++---------------- 1 file changed, 75 insertions(+), 44 deletions(-) diff --git a/src/music_player.rs b/src/music_player.rs index 6ec9670..5fbe4d3 100644 --- a/src/music_player.rs +++ b/src/music_player.rs @@ -1,52 +1,52 @@ // Crate things -use crate::music_controller::config::Config; +//use crate::music_controller::config::Config; use crate::music_storage::music_db::URI; use std::error::Error; -use std::sync::mpsc::{Sender, self, Receiver}; +use std::sync::{Arc, Mutex}; +use std::sync::mpsc::{self, Receiver, Sender}; // GStreamer things +use glib::{FlagsClass, MainContext}; use gst::{ClockTime, Element}; use gstreamer as gst; use gstreamer::prelude::*; -use glib::FlagsClass; // Time things use chrono::Duration; -enum PlayerCmd { +#[derive(Debug)] +pub enum PlayerCmd { Play, + Pause, + Eos, + AboutToFinish, } /// An instance of a music player with a GStreamer backend pub struct Player { source: Option<URI>, - events: Sender<PlayerCmd>, + //pub message_tx: Sender<PlayerCmd>, + pub message_rx: Receiver<PlayerCmd>, playbin: Element, - position: Duration, - duration: Duration, paused: bool, volume: f64, + start: Option<Duration>, + end: Option<Duration>, + position: Option<Duration>, gapless: bool, } -impl Default for Player { - fn default() -> Self { - Self::new() - } -} - - impl Player { pub fn new() -> Self { + // Initialize GStreamer gst::init().unwrap(); - let playbin = gst::ElementFactory::make("playbin") - .build() - .unwrap(); + let playbin = gst::ElementFactory::make("playbin").build().unwrap(); let flags = playbin.property_value("flags"); let flags_class = FlagsClass::with_type(flags.type_()).unwrap(); + // Set up the Playbin flags to only play audio let flags = flags_class .builder_with_value(flags) .unwrap() @@ -58,32 +58,55 @@ impl Player { .unwrap(); playbin.set_property_from_value("flags", &flags); - playbin - .bus() - .expect("Failed to get GStreamer message bus"); + + let (message_tx, message_rx) = std::sync::mpsc::channel(); + playbin.connect("about-to-finish", false, move |_| { + println!("test"); + message_tx.send(PlayerCmd::AboutToFinish).unwrap(); + None + }); let source = None; - let (tx, _): (Sender<PlayerCmd>, Receiver<PlayerCmd>) = mpsc::channel(); Self { source, - events: tx, playbin, + message_rx, paused: false, volume: 0.5, gapless: false, - position: Duration::seconds(0), - duration: Duration::seconds(0), + start: None, + end: None, + position: None, } } + pub fn source(&self) -> &Option<URI> { + &self.source + } + pub fn enqueue_next(&mut self, next_track: URI) { self.set_state(gst::State::Ready); - self.playbin.set_property("uri", next_track.as_uri()); + self.set_source(next_track); self.play(); } + /// Set the playback URI + pub fn set_source(&mut self, source: URI) { + self.source = Some(source.clone()); + match source { + URI::Cue {start, ..} => { + self.playbin.set_property("uri", source.as_uri()); + self.play(); + while self.state() != gst::State::Playing { + std::thread::sleep(std::time::Duration::from_millis(10)); + }; + self.seek_to(Duration::from_std(start).unwrap()).unwrap(); + } + _ => self.playbin.set_property("uri", source.as_uri()), + } + } /// Set the playback volume, accepts a float from 0 to 1 pub fn set_volume(&mut self, volume: f64) { @@ -130,27 +153,29 @@ impl Player { self.playbin.current_state() == gst::State::Paused } - /// Set the playback URI - pub fn set_source(&mut self, source: URI) { - self.source = Some(source.clone()); - self.playbin.set_property("uri", source.as_uri()) - } - /// Get the current playback position of the player pub fn position(&mut self) -> Option<Duration> { - self.playbin.query_position::<ClockTime>().map(|pos| Duration::nanoseconds(pos.nseconds() as i64)) + self.position = self + .playbin + .query_position::<ClockTime>() + .map(|pos| Duration::nanoseconds(pos.nseconds() as i64)); + self.position } /// Get the duration of the currently playing track pub fn duration(&mut self) -> Option<Duration> { - self.playbin.query_duration::<ClockTime>().map(|pos| Duration::milliseconds(pos.mseconds() as i64)) + if self.end.is_some() && self.start.is_some() { + Some(self.end.unwrap() - self.start.unwrap()) + } else { + None + } } /// Seek relative to the current position pub fn seek_by(&mut self, seek_amount: Duration) -> Result<(), Box<dyn Error>> { let time_pos = match self.position() { Some(pos) => pos, - None => return Err("No position".into()) + None => return Err("No position".into()), }; let seek_pos = time_pos + seek_amount; @@ -159,19 +184,25 @@ impl Player { } /// Seek absolutely - pub fn seek_to(&mut self, last_pos: Duration) -> Result<(), Box<dyn Error>> { - let duration = match self.duration() { - Some(dur) => dur, - None => return Err("No duration".into()) - }; - let seek_pos = last_pos.clamp(Duration::seconds(0), duration); - - let seek_pos_clock = ClockTime::from_mseconds(seek_pos.num_milliseconds() as u64); + pub fn seek_to(&mut self, target_pos: Duration) -> Result<(), Box<dyn Error>> { + let seek_pos_clock = ClockTime::from_useconds(target_pos.num_microseconds().unwrap() as u64); self.set_gstreamer_volume(0.0); - self - .playbin + self.playbin .seek_simple(gst::SeekFlags::FLUSH, seek_pos_clock)?; self.set_gstreamer_volume(self.volume); Ok(()) } + + pub fn state(&mut self) -> gst::State { + self.playbin.current_state() + } +} + +impl Drop for Player { + /// Cleans up `GStreamer` pipeline when `Backend` is dropped. + fn drop(&mut self) { + self.playbin + .set_state(gst::State::Null) + .expect("Unable to set the pipeline to the `Null` state"); + } } From d12d65c518b985efedc9d8985791a07d01e632a2 Mon Sep 17 00:00:00 2001 From: G2-Games <ke0bhogsg@gmail.com> Date: Mon, 4 Dec 2023 21:09:10 -0600 Subject: [PATCH 037/136] Fixed "about to finish" message sending improperly --- Cargo.toml | 2 + src/music_player.rs | 174 +++++++++++++++++++++++++--------- src/music_storage/music_db.rs | 13 ++- 3 files changed, 142 insertions(+), 47 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ba7c5fd..a7eb36f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,3 +27,5 @@ snap = "1.1.0" rcue = "0.1.3" gstreamer = "0.21.2" glib = "0.18.3" +crossbeam-channel = "0.5.8" +crossbeam = "0.8.2" diff --git a/src/music_player.rs b/src/music_player.rs index 5fbe4d3..2ffbdd7 100644 --- a/src/music_player.rs +++ b/src/music_player.rs @@ -2,8 +2,9 @@ //use crate::music_controller::config::Config; use crate::music_storage::music_db::URI; use std::error::Error; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, Mutex, RwLock}; use std::sync::mpsc::{self, Receiver, Sender}; +use crossbeam_channel::bounded; // GStreamer things use glib::{FlagsClass, MainContext}; @@ -26,24 +27,32 @@ pub enum PlayerCmd { pub struct Player { source: Option<URI>, //pub message_tx: Sender<PlayerCmd>, - pub message_rx: Receiver<PlayerCmd>, - playbin: Element, + pub message_rx: crossbeam::channel::Receiver<PlayerCmd>, + playbin: Arc<RwLock<Element>>, paused: bool, volume: f64, - start: Option<Duration>, - end: Option<Duration>, - position: Option<Duration>, + start: Arc<RwLock<Option<Duration>>>, + end: Arc<RwLock<Option<Duration>>>, + pub position: Arc<RwLock<Option<Duration>>>, gapless: bool, } +impl Default for Player { + fn default() -> Self { + Self::new() + } +} + impl Player { pub fn new() -> Self { // Initialize GStreamer gst::init().unwrap(); - let playbin = gst::ElementFactory::make("playbin").build().unwrap(); + let playbin_arc = Arc::new(RwLock::new(gst::ElementFactory::make("playbin3").build().unwrap())); - let flags = playbin.property_value("flags"); + let playbin = playbin_arc.clone(); + + let flags = playbin.read().unwrap().property_value("flags"); let flags_class = FlagsClass::with_type(flags.type_()).unwrap(); // Set up the Playbin flags to only play audio @@ -56,15 +65,61 @@ impl Player { .unset_by_nick("text") .build() .unwrap(); - playbin.set_property_from_value("flags", &flags); + playbin.write().unwrap().set_property_from_value("flags", &flags); - let (message_tx, message_rx) = std::sync::mpsc::channel(); - playbin.connect("about-to-finish", false, move |_| { - println!("test"); - message_tx.send(PlayerCmd::AboutToFinish).unwrap(); - None - }); + let position = Arc::new(RwLock::new(None)); + let start = Arc::new(RwLock::new(None)); + let end: Arc<RwLock<Option<Duration>>> = Arc::new(RwLock::new(None)); + + let position_update = position.clone(); + let start_update = start.clone(); + let end_update = end.clone(); + let (message_tx, message_rx) = bounded(1); + std::thread::spawn(move || { + loop { + let mut pos_temp = playbin_arc + .read() + .unwrap() + .query_position::<ClockTime>() + .map(|pos| Duration::nanoseconds(pos.nseconds() as i64)); + + if pos_temp.is_some() + && start_update.read().unwrap().is_some() + && end_update.read().unwrap().is_some() + { + if let Some(time) = *start_update.read().unwrap() { pos_temp = Some(pos_temp.unwrap() - time) } + + let atf = end_update.read().unwrap().unwrap() - Duration::milliseconds(100); + if pos_temp.unwrap() >= end_update.read().unwrap().unwrap() { + message_tx.try_send(PlayerCmd::Eos).unwrap(); + playbin_arc + .write() + .unwrap() + .set_state(gst::State::Ready) + .expect("Unable to set the pipeline state"); + *start_update.write().unwrap() = None; + *end_update.write().unwrap() = None; + } else if pos_temp.unwrap() >= atf { + match message_tx.try_send(PlayerCmd::AboutToFinish) { + Ok(_) => println!("Sent ATF"), + Err(err) => println!("{}", err), + } + } + } + + *position_update.write().unwrap() = pos_temp; + + std::thread::sleep(std::time::Duration::from_millis(50)); + } + }); + + /* + playbin.read().unwrap().connect("about-to-finish", false, move |_| { + //message_tx.send(PlayerCmd::AboutToFinish).unwrap(); + None + }); + */ let source = None; Self { @@ -72,11 +127,11 @@ impl Player { playbin, message_rx, paused: false, - volume: 0.5, + volume: 1.0, gapless: false, - start: None, - end: None, - position: None, + start, + end, + position, } } @@ -84,27 +139,48 @@ impl Player { &self.source } - pub fn enqueue_next(&mut self, next_track: URI) { - self.set_state(gst::State::Ready); - + pub fn enqueue_next(&mut self, next_track: &URI) { + self.ready(); self.set_source(next_track); - self.play(); } /// Set the playback URI - pub fn set_source(&mut self, source: URI) { + fn set_source(&mut self, source: &URI) { self.source = Some(source.clone()); match source { - URI::Cue {start, ..} => { - self.playbin.set_property("uri", source.as_uri()); - self.play(); - while self.state() != gst::State::Playing { - std::thread::sleep(std::time::Duration::from_millis(10)); + URI::Cue {start, end, ..} => { + self.playbin.write().unwrap().set_property("uri", source.as_uri()); + + // Set the start and end positions of the CUE file + *self.start.write().unwrap() = Some(Duration::from_std(*start).unwrap()); + *self.end.write().unwrap() = Some(Duration::from_std(*end).unwrap()); + + self.pause(); + + // Wait for it to be ready, and then move to the proper position + while self.playbin.read().unwrap().query_duration::<ClockTime>().is_none() { + std::thread::sleep(std::time::Duration::from_millis(1)); }; - self.seek_to(Duration::from_std(start).unwrap()).unwrap(); - } - _ => self.playbin.set_property("uri", source.as_uri()), + + self.seek_to(Duration::from_std(*start).unwrap()).unwrap(); + }, + _ => { + self.playbin.write().unwrap().set_property("uri", source.as_uri()); + + self.pause(); + + while self.playbin.read().unwrap().query_duration::<ClockTime>().is_none() { + std::thread::sleep(std::time::Duration::from_millis(1)); + }; + + *self.start.write().unwrap() = Some(Duration::seconds(0)); + *self.end.write().unwrap() = self.playbin + .read() + .unwrap() + .query_duration::<ClockTime>() + .map(|pos| Duration::nanoseconds(pos.nseconds() as i64)); + }, } } @@ -117,7 +193,7 @@ impl Player { /// Set volume of the internal playbin player, can be /// used to bypass the main volume control for seeking fn set_gstreamer_volume(&mut self, volume: f64) { - self.playbin.set_property("volume", volume) + self.playbin.write().unwrap().set_property("volume", volume) } /// Returns the current volume level, a float from 0 to 1 @@ -127,10 +203,16 @@ impl Player { fn set_state(&mut self, state: gst::State) { self.playbin + .write() + .unwrap() .set_state(state) .expect("Unable to set the pipeline state"); } + pub fn ready(&mut self) { + self.set_state(gst::State::Ready) + } + /// If the player is paused or stopped, starts playback pub fn play(&mut self) { self.set_state(gst::State::Playing); @@ -150,30 +232,30 @@ impl Player { /// Check if playback is paused pub fn is_paused(&mut self) -> bool { - self.playbin.current_state() == gst::State::Paused + self.playbin.read().unwrap().current_state() == gst::State::Paused } /// Get the current playback position of the player pub fn position(&mut self) -> Option<Duration> { - self.position = self - .playbin - .query_position::<ClockTime>() - .map(|pos| Duration::nanoseconds(pos.nseconds() as i64)); - self.position + *self.position.read().unwrap() } /// Get the duration of the currently playing track pub fn duration(&mut self) -> Option<Duration> { - if self.end.is_some() && self.start.is_some() { - Some(self.end.unwrap() - self.start.unwrap()) + if self.end.read().unwrap().is_some() && self.start.read().unwrap().is_some() { + Some(self.end.read().unwrap().unwrap() - self.start.read().unwrap().unwrap()) } else { - None + self.playbin + .read() + .unwrap() + .query_duration::<ClockTime>() + .map(|pos| Duration::nanoseconds(pos.nseconds() as i64)) } } /// Seek relative to the current position pub fn seek_by(&mut self, seek_amount: Duration) -> Result<(), Box<dyn Error>> { - let time_pos = match self.position() { + let time_pos = match *self.position.read().unwrap() { Some(pos) => pos, None => return Err("No position".into()), }; @@ -188,13 +270,15 @@ impl Player { let seek_pos_clock = ClockTime::from_useconds(target_pos.num_microseconds().unwrap() as u64); self.set_gstreamer_volume(0.0); self.playbin + .write() + .unwrap() .seek_simple(gst::SeekFlags::FLUSH, seek_pos_clock)?; self.set_gstreamer_volume(self.volume); Ok(()) } pub fn state(&mut self) -> gst::State { - self.playbin.current_state() + self.playbin.read().unwrap().current_state() } } @@ -202,6 +286,8 @@ impl Drop for Player { /// Cleans up `GStreamer` pipeline when `Backend` is dropped. fn drop(&mut self) { self.playbin + .write() + .unwrap() .set_state(gst::State::Null) .expect("Unable to set the pipeline to the `Null` state"); } diff --git a/src/music_storage/music_db.rs b/src/music_storage/music_db.rs index 21df4f5..3faf165 100644 --- a/src/music_storage/music_db.rs +++ b/src/music_storage/music_db.rs @@ -76,6 +76,7 @@ impl ToString for Tag { } /// A field within a Song struct +#[derive(Debug)] pub enum Field { Location(URI), Plays(i32), @@ -399,7 +400,7 @@ impl MusicLibrary { &mut self, target_path: &str, config: &Config, - ) -> Result<usize, Box<dyn std::error::Error>> { + ) -> Result<i32, Box<dyn std::error::Error>> { let mut total = 0; let mut errors = 0; for target_file in WalkDir::new(target_path) @@ -568,7 +569,7 @@ impl MusicLibrary { Ok(()) } - pub fn add_cuesheet(&mut self, cuesheet: &Path) -> Result<usize, Box<dyn Error>> { + pub fn add_cuesheet(&mut self, cuesheet: &Path) -> Result<i32, Box<dyn Error>> { let mut tracks_added = 0; let cue_data = parse_from_file(&cuesheet.to_string_lossy(), false).unwrap(); @@ -602,7 +603,13 @@ impl MusicLibrary { Some(postgap) => postgap, None => Duration::from_secs(0), }; - let mut start = track.indices[0].1; + + let mut start; + if track.indices.len() > 1 { + start = track.indices[1].1; + } else { + start = track.indices[0].1; + } if !start.is_zero() { start -= pregap; } From 38d4fe9bc8284a2e982025f1d29c8c25e4271bae Mon Sep 17 00:00:00 2001 From: G2-Games <ke0bhogsg@gmail.com> Date: Mon, 4 Dec 2023 22:09:51 -0600 Subject: [PATCH 038/136] Added more error handling --- src/music_player.rs | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/src/music_player.rs b/src/music_player.rs index 2ffbdd7..af42728 100644 --- a/src/music_player.rs +++ b/src/music_player.rs @@ -140,9 +140,9 @@ impl Player { } pub fn enqueue_next(&mut self, next_track: &URI) { - self.ready(); + self.ready().unwrap(); self.set_source(next_track); - self.play(); + self.play().unwrap(); } /// Set the playback URI @@ -156,7 +156,7 @@ impl Player { *self.start.write().unwrap() = Some(Duration::from_std(*start).unwrap()); *self.end.write().unwrap() = Some(Duration::from_std(*end).unwrap()); - self.pause(); + self.pause().unwrap(); // Wait for it to be ready, and then move to the proper position while self.playbin.read().unwrap().query_duration::<ClockTime>().is_none() { @@ -168,7 +168,7 @@ impl Player { _ => { self.playbin.write().unwrap().set_property("uri", source.as_uri()); - self.pause(); + self.pause().unwrap(); while self.playbin.read().unwrap().query_duration::<ClockTime>().is_none() { std::thread::sleep(std::time::Duration::from_millis(1)); @@ -201,33 +201,34 @@ impl Player { self.volume } - fn set_state(&mut self, state: gst::State) { + fn set_state(&mut self, state: gst::State) -> Result<(), gst::StateChangeError> { self.playbin .write() .unwrap() - .set_state(state) - .expect("Unable to set the pipeline state"); + .set_state(state)?; + + Ok(()) } - pub fn ready(&mut self) { + pub fn ready(&mut self) -> Result<(), gst::StateChangeError> { self.set_state(gst::State::Ready) } /// If the player is paused or stopped, starts playback - pub fn play(&mut self) { - self.set_state(gst::State::Playing); + pub fn play(&mut self) -> Result<(), gst::StateChangeError> { + self.set_state(gst::State::Playing) } /// Pause, if playing - pub fn pause(&mut self) { + pub fn pause(&mut self) -> Result<(), gst::StateChangeError> { self.paused = true; - self.set_state(gst::State::Paused); + self.set_state(gst::State::Paused) } /// Resume from being paused - pub fn resume(&mut self) { + pub fn resume(&mut self) -> Result<(), gst::StateChangeError> { self.paused = false; - self.set_state(gst::State::Playing); + self.set_state(gst::State::Playing) } /// Check if playback is paused From 38b27c66c2e34730c20ccf1a50be35570be87032 Mon Sep 17 00:00:00 2001 From: G2-Games <ke0bhogsg@gmail.com> Date: Mon, 4 Dec 2023 22:33:52 -0600 Subject: [PATCH 039/136] Fixed GStreamer URI generation --- src/music_storage/music_db.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/music_storage/music_db.rs b/src/music_storage/music_db.rs index 3faf165..f66c8df 100644 --- a/src/music_storage/music_db.rs +++ b/src/music_storage/music_db.rs @@ -14,6 +14,7 @@ use rcue::parser::parse_from_file; use std::fs; use std::path::{Path, PathBuf}; use walkdir::WalkDir; +use glib::filename_to_uri; // Time use chrono::{serde::ts_milliseconds_option, DateTime, Utc}; @@ -224,8 +225,8 @@ impl URI { pub fn as_uri(&self) -> String { let path_str = match self { - URI::Local(location) => format!("file://{}", location.as_path().to_string_lossy()), - URI::Cue { location, .. } => format!("file://{}", location.as_path().to_string_lossy()), + URI::Local(location) => filename_to_uri(location, None).expect("couldn't convert path to URI").to_string(), + URI::Cue { location, .. } => filename_to_uri(location, None).expect("couldn't convert path to URI").to_string(), URI::Remote(_, location) => location.clone(), }; path_str.to_string() From d87927a7db5fc6dc2cf153acb24222c2de520448 Mon Sep 17 00:00:00 2001 From: G2-Games <ke0bhogsg@gmail.com> Date: Tue, 5 Dec 2023 08:19:10 -0600 Subject: [PATCH 040/136] Fixed an issue with CUE playback, fixed an issue with CUE file reading The CUE artist was being inserted as the Album title --- src/music_player.rs | 27 +++++++++++++++++---------- src/music_storage/music_db.rs | 5 ++++- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/music_player.rs b/src/music_player.rs index af42728..4f30286 100644 --- a/src/music_player.rs +++ b/src/music_player.rs @@ -73,8 +73,8 @@ impl Player { let end: Arc<RwLock<Option<Duration>>> = Arc::new(RwLock::new(None)); let position_update = position.clone(); - let start_update = start.clone(); - let end_update = end.clone(); + let start_update = Arc::clone(&start); + let end_update = Arc::clone(&end); let (message_tx, message_rx) = bounded(1); std::thread::spawn(move || { loop { @@ -88,8 +88,6 @@ impl Player { && start_update.read().unwrap().is_some() && end_update.read().unwrap().is_some() { - if let Some(time) = *start_update.read().unwrap() { pos_temp = Some(pos_temp.unwrap() - time) } - let atf = end_update.read().unwrap().unwrap() - Duration::milliseconds(100); if pos_temp.unwrap() >= end_update.read().unwrap().unwrap() { message_tx.try_send(PlayerCmd::Eos).unwrap(); @@ -102,10 +100,16 @@ impl Player { *end_update.write().unwrap() = None; } else if pos_temp.unwrap() >= atf { match message_tx.try_send(PlayerCmd::AboutToFinish) { - Ok(_) => println!("Sent ATF"), - Err(err) => println!("{}", err), + Ok(_) => (), + Err(_) => (), } } + + // This has to be done AFTER the current time in the file + // is calculated, or everything else is wrong + if let Some(time) = *start_update.read().unwrap() { + pos_temp = Some(pos_temp.unwrap() - time) + } } *position_update.write().unwrap() = pos_temp; @@ -159,11 +163,14 @@ impl Player { self.pause().unwrap(); // Wait for it to be ready, and then move to the proper position - while self.playbin.read().unwrap().query_duration::<ClockTime>().is_none() { + let now = std::time::Instant::now(); + while now.elapsed() < std::time::Duration::from_millis(20) { + if self.seek_to(Duration::from_std(*start).unwrap()).is_ok() { + return; + } std::thread::sleep(std::time::Duration::from_millis(1)); - }; - - self.seek_to(Duration::from_std(*start).unwrap()).unwrap(); + } + panic!("Couldn't seek to beginning of cue track in reasonable time (>20ms)"); }, _ => { self.playbin.write().unwrap().set_property("uri", source.as_uri()); diff --git a/src/music_storage/music_db.rs b/src/music_storage/music_db.rs index f66c8df..ec56547 100644 --- a/src/music_storage/music_db.rs +++ b/src/music_storage/music_db.rs @@ -149,6 +149,7 @@ impl Song { self.tags.get(target_key) } + /// Gets an internal field from a song pub fn get_field(&self, target_field: &str) -> Option<Field> { let lower_target = target_field.to_lowercase(); match lower_target.as_str() { @@ -164,10 +165,12 @@ impl Song { } } + /// Sets the value of a tag in the song pub fn set_tag(&mut self, target_key: Tag, new_value: String) { self.tags.insert(target_key, new_value); } + /// Deletes a tag from the song pub fn remove_tag(&mut self, target_key: &Tag) { self.tags.remove(target_key); } @@ -648,7 +651,7 @@ impl MusicLibrary { } match album_artist { Some(artist) => { - tags.insert(Tag::Album, artist.clone()); + tags.insert(Tag::Artist, artist.clone()); } None => (), } From f392e6a0af7afa63c94e509718496bf0d125db42 Mon Sep 17 00:00:00 2001 From: G2-Games <ke0bhogsg@gmail.com> Date: Tue, 5 Dec 2023 12:59:52 -0600 Subject: [PATCH 041/136] Gapless playback works --- src/music_player.rs | 98 ++++++++++++++++++++++++--------------------- 1 file changed, 53 insertions(+), 45 deletions(-) diff --git a/src/music_player.rs b/src/music_player.rs index 4f30286..929332f 100644 --- a/src/music_player.rs +++ b/src/music_player.rs @@ -1,13 +1,12 @@ // Crate things //use crate::music_controller::config::Config; use crate::music_storage::music_db::URI; -use std::error::Error; -use std::sync::{Arc, Mutex, RwLock}; -use std::sync::mpsc::{self, Receiver, Sender}; use crossbeam_channel::bounded; +use std::error::Error; +use std::sync::{Arc, RwLock}; // GStreamer things -use glib::{FlagsClass, MainContext}; +use glib::FlagsClass; use gst::{ClockTime, Element}; use gstreamer as gst; use gstreamer::prelude::*; @@ -34,7 +33,6 @@ pub struct Player { start: Arc<RwLock<Option<Duration>>>, end: Arc<RwLock<Option<Duration>>>, pub position: Arc<RwLock<Option<Duration>>>, - gapless: bool, } impl Default for Player { @@ -48,7 +46,9 @@ impl Player { // Initialize GStreamer gst::init().unwrap(); - let playbin_arc = Arc::new(RwLock::new(gst::ElementFactory::make("playbin3").build().unwrap())); + let playbin_arc = Arc::new(RwLock::new( + gst::ElementFactory::make("playbin3").build().unwrap(), + )); let playbin = playbin_arc.clone(); @@ -66,7 +66,12 @@ impl Player { .build() .unwrap(); - playbin.write().unwrap().set_property_from_value("flags", &flags); + playbin + .write() + .unwrap() + .set_property_from_value("flags", &flags); + + playbin.write().unwrap().set_property("instant-uri", true); let position = Arc::new(RwLock::new(None)); let start = Arc::new(RwLock::new(None)); @@ -88,7 +93,7 @@ impl Player { && start_update.read().unwrap().is_some() && end_update.read().unwrap().is_some() { - let atf = end_update.read().unwrap().unwrap() - Duration::milliseconds(100); + let atf = end_update.read().unwrap().unwrap() - Duration::milliseconds(250); if pos_temp.unwrap() >= end_update.read().unwrap().unwrap() { message_tx.try_send(PlayerCmd::Eos).unwrap(); playbin_arc @@ -99,10 +104,7 @@ impl Player { *start_update.write().unwrap() = None; *end_update.write().unwrap() = None; } else if pos_temp.unwrap() >= atf { - match message_tx.try_send(PlayerCmd::AboutToFinish) { - Ok(_) => (), - Err(_) => (), - } + let _ = message_tx.try_send(PlayerCmd::AboutToFinish); } // This has to be done AFTER the current time in the file @@ -118,13 +120,6 @@ impl Player { } }); - /* - playbin.read().unwrap().connect("about-to-finish", false, move |_| { - //message_tx.send(PlayerCmd::AboutToFinish).unwrap(); - None - }); - */ - let source = None; Self { source, @@ -132,7 +127,6 @@ impl Player { message_rx, paused: false, volume: 1.0, - gapless: false, start, end, position, @@ -144,23 +138,22 @@ impl Player { } pub fn enqueue_next(&mut self, next_track: &URI) { - self.ready().unwrap(); self.set_source(next_track); - self.play().unwrap(); } /// Set the playback URI fn set_source(&mut self, source: &URI) { + let uri = self.playbin.read().unwrap().property_value("current-uri"); self.source = Some(source.clone()); match source { - URI::Cue {start, end, ..} => { + URI::Cue { start, end, .. } => { self.playbin.write().unwrap().set_property("uri", source.as_uri()); // Set the start and end positions of the CUE file *self.start.write().unwrap() = Some(Duration::from_std(*start).unwrap()); *self.end.write().unwrap() = Some(Duration::from_std(*end).unwrap()); - self.pause().unwrap(); + self.play().unwrap(); // Wait for it to be ready, and then move to the proper position let now = std::time::Instant::now(); @@ -171,23 +164,21 @@ impl Player { std::thread::sleep(std::time::Duration::from_millis(1)); } panic!("Couldn't seek to beginning of cue track in reasonable time (>20ms)"); - }, + } _ => { self.playbin.write().unwrap().set_property("uri", source.as_uri()); - self.pause().unwrap(); + self.play().unwrap(); - while self.playbin.read().unwrap().query_duration::<ClockTime>().is_none() { - std::thread::sleep(std::time::Duration::from_millis(1)); - }; + while uri.get::<&str>().unwrap_or("") == self.property("current-uri").get::<&str>().unwrap_or("") + || self.raw_duration().is_none() + { + std::thread::sleep(std::time::Duration::from_millis(10)); + } *self.start.write().unwrap() = Some(Duration::seconds(0)); - *self.end.write().unwrap() = self.playbin - .read() - .unwrap() - .query_duration::<ClockTime>() - .map(|pos| Duration::nanoseconds(pos.nseconds() as i64)); - }, + *self.end.write().unwrap() = self.raw_duration(); + } } } @@ -209,10 +200,7 @@ impl Player { } fn set_state(&mut self, state: gst::State) -> Result<(), gst::StateChangeError> { - self.playbin - .write() - .unwrap() - .set_state(state)?; + self.playbin.write().unwrap().set_state(state)?; Ok(()) } @@ -253,14 +241,18 @@ impl Player { if self.end.read().unwrap().is_some() && self.start.read().unwrap().is_some() { Some(self.end.read().unwrap().unwrap() - self.start.read().unwrap().unwrap()) } else { - self.playbin - .read() - .unwrap() - .query_duration::<ClockTime>() - .map(|pos| Duration::nanoseconds(pos.nseconds() as i64)) + self.raw_duration() } } + pub fn raw_duration(&self) -> Option<Duration> { + self.playbin + .read() + .unwrap() + .query_duration::<ClockTime>() + .map(|pos| Duration::nanoseconds(pos.nseconds() as i64)) + } + /// Seek relative to the current position pub fn seek_by(&mut self, seek_amount: Duration) -> Result<(), Box<dyn Error>> { let time_pos = match *self.position.read().unwrap() { @@ -275,7 +267,19 @@ impl Player { /// Seek absolutely pub fn seek_to(&mut self, target_pos: Duration) -> Result<(), Box<dyn Error>> { - let seek_pos_clock = ClockTime::from_useconds(target_pos.num_microseconds().unwrap() as u64); + if self.start.read().unwrap().is_none() { + return Err("Failed to seek: No START time".into()); + } + + if self.end.read().unwrap().is_none() { + return Err("Failed to seek: No END time".into()); + } + + let clamped_target = target_pos.clamp(self.start.read().unwrap().unwrap(), self.end.read().unwrap().unwrap()); + + let seek_pos_clock = + ClockTime::from_useconds(clamped_target.num_microseconds().unwrap() as u64); + self.set_gstreamer_volume(0.0); self.playbin .write() @@ -288,6 +292,10 @@ impl Player { pub fn state(&mut self) -> gst::State { self.playbin.read().unwrap().current_state() } + + pub fn property(&self, property: &str) -> glib::Value { + self.playbin.read().unwrap().property_value(property) + } } impl Drop for Player { From 5c2b718d79dd706a9f2fe92085cc60206ac587ed Mon Sep 17 00:00:00 2001 From: G2-Games <ke0bhogsg@gmail.com> Date: Tue, 5 Dec 2023 13:26:47 -0600 Subject: [PATCH 042/136] Indefinite network streams are now properly queued --- src/music_player.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/music_player.rs b/src/music_player.rs index 929332f..0bae684 100644 --- a/src/music_player.rs +++ b/src/music_player.rs @@ -171,7 +171,7 @@ impl Player { self.play().unwrap(); while uri.get::<&str>().unwrap_or("") == self.property("current-uri").get::<&str>().unwrap_or("") - || self.raw_duration().is_none() + || self.position().is_none() { std::thread::sleep(std::time::Duration::from_millis(10)); } @@ -267,15 +267,22 @@ impl Player { /// Seek absolutely pub fn seek_to(&mut self, target_pos: Duration) -> Result<(), Box<dyn Error>> { + let start; if self.start.read().unwrap().is_none() { return Err("Failed to seek: No START time".into()); + } else { + start = self.start.read().unwrap().unwrap(); } + let end; if self.end.read().unwrap().is_none() { return Err("Failed to seek: No END time".into()); + } else { + end = self.end.read().unwrap().unwrap(); } - let clamped_target = target_pos.clamp(self.start.read().unwrap().unwrap(), self.end.read().unwrap().unwrap()); + let adjusted_target = target_pos + start; + let clamped_target = adjusted_target.clamp(start, end); let seek_pos_clock = ClockTime::from_useconds(clamped_target.num_microseconds().unwrap() as u64); From 17c2745cdd366cb17657fe257344fb14c56029ba Mon Sep 17 00:00:00 2001 From: G2-Games <ke0bhogsg@gmail.com> Date: Thu, 7 Dec 2023 15:14:03 -0600 Subject: [PATCH 043/136] Added buffering and bus message watching --- src/music_player.rs | 132 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 121 insertions(+), 11 deletions(-) diff --git a/src/music_player.rs b/src/music_player.rs index 0bae684..647e470 100644 --- a/src/music_player.rs +++ b/src/music_player.rs @@ -1,6 +1,6 @@ // Crate things //use crate::music_controller::config::Config; -use crate::music_storage::music_db::URI; +use crate::music_storage::music_db::{URI, Tag}; use crossbeam_channel::bounded; use std::error::Error; use std::sync::{Arc, RwLock}; @@ -22,17 +22,55 @@ pub enum PlayerCmd { AboutToFinish, } +#[derive(Debug)] +pub enum PlayerState { + Playing, + Paused, + Ready, + Buffering(u8), + Null, + VoidPending, +} + +impl From<gst::State> for PlayerState { + fn from(value: gst::State) -> Self { + match value { + gst::State::VoidPending => Self::VoidPending, + gst::State::Playing => Self::Playing, + gst::State::Paused => Self::Paused, + gst::State::Ready => Self::Ready, + gst::State::Null => Self::Null, + } + } +} + +impl TryInto<gst::State> for PlayerState { + fn try_into(self) -> Result<gst::State, Box<dyn Error>> { + match self { + Self::VoidPending => Ok(gst::State::VoidPending), + Self::Playing => Ok(gst::State::Playing), + Self::Paused => Ok(gst::State::Paused), + Self::Ready => Ok(gst::State::Ready), + Self::Null => Ok(gst::State::Null), + state => Err(format!("Invalid gst::State: {:?}", state).into()) + } + } + + type Error = Box<dyn Error>; +} + /// An instance of a music player with a GStreamer backend pub struct Player { source: Option<URI>, //pub message_tx: Sender<PlayerCmd>, pub message_rx: crossbeam::channel::Receiver<PlayerCmd>, playbin: Arc<RwLock<Element>>, - paused: bool, volume: f64, start: Arc<RwLock<Option<Duration>>>, end: Arc<RwLock<Option<Duration>>>, - pub position: Arc<RwLock<Option<Duration>>>, + position: Arc<RwLock<Option<Duration>>>, + buffer: Arc<RwLock<Option<u8>>>, + paused: Arc<RwLock<bool>>, } impl Default for Player { @@ -45,6 +83,9 @@ impl Player { pub fn new() -> Self { // Initialize GStreamer gst::init().unwrap(); + let ctx = glib::MainContext::default(); + let _guard = ctx.acquire(); + let mainloop = glib::MainLoop::new(Some(&ctx), false); let playbin_arc = Arc::new(RwLock::new( gst::ElementFactory::make("playbin3").build().unwrap(), @@ -76,11 +117,14 @@ impl Player { let position = Arc::new(RwLock::new(None)); let start = Arc::new(RwLock::new(None)); let end: Arc<RwLock<Option<Duration>>> = Arc::new(RwLock::new(None)); + let buffer = Arc::new(RwLock::new(None)); + let paused = Arc::new(RwLock::new(false)); + // Set up the thread to monitor the position let position_update = position.clone(); let start_update = Arc::clone(&start); let end_update = Arc::clone(&end); - let (message_tx, message_rx) = bounded(1); + let (message_tx, message_rx) = bounded(1); //TODO: Maybe figure out a better method than making this bounded std::thread::spawn(move || { loop { let mut pos_temp = playbin_arc @@ -95,7 +139,7 @@ impl Player { { let atf = end_update.read().unwrap().unwrap() - Duration::milliseconds(250); if pos_temp.unwrap() >= end_update.read().unwrap().unwrap() { - message_tx.try_send(PlayerCmd::Eos).unwrap(); + let _ = message_tx.try_send(PlayerCmd::Eos); playbin_arc .write() .unwrap() @@ -114,22 +158,70 @@ impl Player { } } + //println!("{:?}", pos_temp); + *position_update.write().unwrap() = pos_temp; - std::thread::sleep(std::time::Duration::from_millis(50)); + std::thread::sleep(std::time::Duration::from_millis(100)); } }); + // Set up the thread to monitor bus messages + let playbin_bus_ctrl = Arc::clone(&playbin); + let buffer_bus_ctrl = Arc::clone(&buffer); + let paused_bus_ctrl = Arc::clone(&paused); + let bus_watch = playbin + .read() + .unwrap() + .bus() + .expect("Failed to get GStreamer message bus") + .add_watch(move |_bus, msg| { + match msg.view() { + gst::MessageView::Eos(_) => {}, + gst::MessageView::StreamStart(_) => {}, + gst::MessageView::Error(e) => + println!("song {}", e.error()), + gst::MessageView::Tag(tag) => { + if let Some(title) = tag.tags().get::<gst::tags::Title>() { + println!(" Title: {}", title.get()); + } + if let Some(album) = tag.tags().get::<gst::tags::Album>() { + println!(" Album: {}", album.get()); + } + } + gst::MessageView::Buffering(buffering) => { + let percent = buffering.percent(); + if percent < 100 { + *buffer_bus_ctrl.write().unwrap() = Some(percent as u8); + playbin_bus_ctrl.write().unwrap().set_state(gst::State::Paused).unwrap(); + } else if *paused_bus_ctrl.read().unwrap() == false { + *buffer_bus_ctrl.write().unwrap() = None; + playbin_bus_ctrl.write().unwrap().set_state(gst::State::Playing).unwrap(); + } + } + _ => (), + } + glib::ControlFlow::Continue + }) + .expect("Failed to connect to GStreamer message bus"); + + // Set up a thread to watch the messages + std::thread::spawn(move || { + let _watch = bus_watch; + mainloop.run() + }); + let source = None; Self { source, playbin, message_rx, - paused: false, volume: 1.0, start, end, + paused, position, + buffer, } } @@ -216,13 +308,13 @@ impl Player { /// Pause, if playing pub fn pause(&mut self) -> Result<(), gst::StateChangeError> { - self.paused = true; + *self.paused.write().unwrap() = true; self.set_state(gst::State::Paused) } /// Resume from being paused pub fn resume(&mut self) -> Result<(), gst::StateChangeError> { - self.paused = false; + *self.paused.write().unwrap() = false; self.set_state(gst::State::Playing) } @@ -296,13 +388,31 @@ impl Player { Ok(()) } - pub fn state(&mut self) -> gst::State { - self.playbin.read().unwrap().current_state() + /// Get the current state of the playback + pub fn state(&mut self) -> PlayerState { + match *self.buffer.read().unwrap() { + None => self.playbin.read().unwrap().current_state().into(), + Some(value) => { + PlayerState::Buffering(value) + } + } } pub fn property(&self, property: &str) -> glib::Value { self.playbin.read().unwrap().property_value(property) } + + /// Stop the playback entirely + pub fn stop(&mut self) -> Result<(), gst::StateChangeError> { + self.pause()?; + self.ready()?; + + // Set all positions to none + *self.position.write().unwrap() = None; + *self.start.write().unwrap() = None; + *self.end.write().unwrap() = None; + Ok(()) + } } impl Drop for Player { From b2e7367795e1df6f3e91d944656d95b5040a57df Mon Sep 17 00:00:00 2001 From: G2-Games <ke0bhogsg@gmail.com> Date: Fri, 8 Dec 2023 00:37:19 -0600 Subject: [PATCH 044/136] Ran `cargo fmt` --- src/music_controller/controller.rs | 10 ++---- src/music_player.rs | 57 +++++++++++++++++++++--------- src/music_storage/music_db.rs | 26 ++++++++------ src/music_storage/utils.rs | 11 +++--- 4 files changed, 64 insertions(+), 40 deletions(-) diff --git a/src/music_controller/controller.rs b/src/music_controller/controller.rs index 6f4881a..333db0b 100644 --- a/src/music_controller/controller.rs +++ b/src/music_controller/controller.rs @@ -18,10 +18,7 @@ impl MusicController { Err(error) => return Err(error), }; - let controller = MusicController { - config, - library, - }; + let controller = MusicController { config, library }; Ok(controller) } @@ -34,10 +31,7 @@ impl MusicController { Err(error) => return Err(error), }; - let controller = MusicController { - config, - library, - }; + let controller = MusicController { config, library }; Ok(controller) } diff --git a/src/music_player.rs b/src/music_player.rs index 647e470..0c9b2d6 100644 --- a/src/music_player.rs +++ b/src/music_player.rs @@ -1,6 +1,6 @@ // Crate things //use crate::music_controller::config::Config; -use crate::music_storage::music_db::{URI, Tag}; +use crate::music_storage::music_db::{Tag, URI}; use crossbeam_channel::bounded; use std::error::Error; use std::sync::{Arc, RwLock}; @@ -48,11 +48,11 @@ impl TryInto<gst::State> for PlayerState { fn try_into(self) -> Result<gst::State, Box<dyn Error>> { match self { Self::VoidPending => Ok(gst::State::VoidPending), - Self::Playing => Ok(gst::State::Playing), + Self::Playing => Ok(gst::State::Playing), Self::Paused => Ok(gst::State::Paused), Self::Ready => Ok(gst::State::Ready), Self::Null => Ok(gst::State::Null), - state => Err(format!("Invalid gst::State: {:?}", state).into()) + state => Err(format!("Invalid gst::State: {:?}", state).into()), } } @@ -177,10 +177,22 @@ impl Player { .expect("Failed to get GStreamer message bus") .add_watch(move |_bus, msg| { match msg.view() { - gst::MessageView::Eos(_) => {}, - gst::MessageView::StreamStart(_) => {}, - gst::MessageView::Error(e) => - println!("song {}", e.error()), + gst::MessageView::Eos(_) => {} + gst::MessageView::StreamStart(_) => println!("Stream start"), + gst::MessageView::Error(e) => { + println!("ERROR: {}", e.error()); + playbin_bus_ctrl + .write() + .unwrap() + .set_state(gst::State::Ready) + .unwrap(); + + playbin_bus_ctrl + .write() + .unwrap() + .set_state(gst::State::Playing) + .unwrap(); + }, gst::MessageView::Tag(tag) => { if let Some(title) = tag.tags().get::<gst::tags::Title>() { println!(" Title: {}", title.get()); @@ -193,10 +205,18 @@ impl Player { let percent = buffering.percent(); if percent < 100 { *buffer_bus_ctrl.write().unwrap() = Some(percent as u8); - playbin_bus_ctrl.write().unwrap().set_state(gst::State::Paused).unwrap(); + playbin_bus_ctrl + .write() + .unwrap() + .set_state(gst::State::Paused) + .unwrap(); } else if *paused_bus_ctrl.read().unwrap() == false { *buffer_bus_ctrl.write().unwrap() = None; - playbin_bus_ctrl.write().unwrap().set_state(gst::State::Playing).unwrap(); + playbin_bus_ctrl + .write() + .unwrap() + .set_state(gst::State::Playing) + .unwrap(); } } _ => (), @@ -239,7 +259,10 @@ impl Player { self.source = Some(source.clone()); match source { URI::Cue { start, end, .. } => { - self.playbin.write().unwrap().set_property("uri", source.as_uri()); + self.playbin + .write() + .unwrap() + .set_property("uri", source.as_uri()); // Set the start and end positions of the CUE file *self.start.write().unwrap() = Some(Duration::from_std(*start).unwrap()); @@ -258,11 +281,15 @@ impl Player { panic!("Couldn't seek to beginning of cue track in reasonable time (>20ms)"); } _ => { - self.playbin.write().unwrap().set_property("uri", source.as_uri()); + self.playbin + .write() + .unwrap() + .set_property("uri", source.as_uri()); self.play().unwrap(); - while uri.get::<&str>().unwrap_or("") == self.property("current-uri").get::<&str>().unwrap_or("") + while uri.get::<&str>().unwrap_or("") + == self.property("current-uri").get::<&str>().unwrap_or("") || self.position().is_none() { std::thread::sleep(std::time::Duration::from_millis(10)); @@ -392,13 +419,11 @@ impl Player { pub fn state(&mut self) -> PlayerState { match *self.buffer.read().unwrap() { None => self.playbin.read().unwrap().current_state().into(), - Some(value) => { - PlayerState::Buffering(value) - } + Some(value) => PlayerState::Buffering(value), } } - pub fn property(&self, property: &str) -> glib::Value { + pub fn property(&self, property: &str) -> glib::Value { self.playbin.read().unwrap().property_value(property) } diff --git a/src/music_storage/music_db.rs b/src/music_storage/music_db.rs index ec56547..41d2e22 100644 --- a/src/music_storage/music_db.rs +++ b/src/music_storage/music_db.rs @@ -9,12 +9,12 @@ use std::ops::ControlFlow::{Break, Continue}; // Files use file_format::{FileFormat, Kind}; +use glib::filename_to_uri; use lofty::{AudioFile, ItemKey, ItemValue, ParseOptions, Probe, TagType, TaggedFileExt}; use rcue::parser::parse_from_file; use std::fs; use std::path::{Path, PathBuf}; use walkdir::WalkDir; -use glib::filename_to_uri; // Time use chrono::{serde::ts_milliseconds_option, DateTime, Utc}; @@ -102,7 +102,7 @@ impl ToString for Field { Self::Rating(rating) => rating.to_string(), Self::Format(format) => match format.short_name() { Some(name) => name.to_string(), - None => format.to_string() + None => format.to_string(), }, Self::Duration(duration) => duration.as_millis().to_string(), Self::PlayTime(time) => time.as_millis().to_string(), @@ -153,14 +153,14 @@ impl Song { pub fn get_field(&self, target_field: &str) -> Option<Field> { let lower_target = target_field.to_lowercase(); match lower_target.as_str() { - "location" => Some(Field::Location(self.location.clone())), - "plays" => Some(Field::Plays(self.plays)), - "skips" => Some(Field::Skips(self.skips)), + "location" => Some(Field::Location(self.location.clone())), + "plays" => Some(Field::Plays(self.plays)), + "skips" => Some(Field::Skips(self.skips)), "favorited" => Some(Field::Favorited(self.favorited)), - "rating" => self.rating.map(Field::Rating), - "duration" => Some(Field::Duration(self.duration)), + "rating" => self.rating.map(Field::Rating), + "duration" => Some(Field::Duration(self.duration)), "play_time" => Some(Field::PlayTime(self.play_time)), - "format" => self.format.map(Field::Format), + "format" => self.format.map(Field::Format), _ => todo!(), // Other field types are not yet supported } } @@ -228,8 +228,12 @@ impl URI { pub fn as_uri(&self) -> String { let path_str = match self { - URI::Local(location) => filename_to_uri(location, None).expect("couldn't convert path to URI").to_string(), - URI::Cue { location, .. } => filename_to_uri(location, None).expect("couldn't convert path to URI").to_string(), + URI::Local(location) => filename_to_uri(location, None) + .expect("couldn't convert path to URI") + .to_string(), + URI::Cue { location, .. } => filename_to_uri(location, None) + .expect("couldn't convert path to URI") + .to_string(), URI::Remote(_, location) => location.clone(), }; path_str.to_string() @@ -492,7 +496,7 @@ impl MusicLibrary { None => blank_tag, }, } - }, + } Err(_) => blank_tag, }; diff --git a/src/music_storage/utils.rs b/src/music_storage/utils.rs index 5c4865a..03e9740 100644 --- a/src/music_storage/utils.rs +++ b/src/music_storage/utils.rs @@ -1,12 +1,12 @@ +use file_format::{FileFormat, Kind}; use std::io::{BufReader, BufWriter}; use std::path::{Path, PathBuf}; use std::{error::Error, fs}; use walkdir::WalkDir; -use file_format::{FileFormat, Kind}; use snap; -use super::music_db::{Song, AlbumArt, URI}; +use super::music_db::{AlbumArt, Song, URI}; use unidecode::unidecode; pub(super) fn normalize(input_string: &str) -> String { @@ -76,8 +76,9 @@ pub fn find_images(song_path: &Path) -> Result<Vec<AlbumArt>, Box<dyn Error>> { .into_iter() .filter_map(|e| e.ok()) { - if target_file.depth() >= 3 { // Don't recurse very deep - break + if target_file.depth() >= 3 { + // Don't recurse very deep + break; } let path = target_file.path(); @@ -87,7 +88,7 @@ pub fn find_images(song_path: &Path) -> Result<Vec<AlbumArt>, Box<dyn Error>> { let format = FileFormat::from_file(path)?.kind(); if format != Kind::Image { - break + break; } let image_uri = URI::Local(path.to_path_buf().canonicalize().unwrap()); From 7a93974f5b77afc09724758cc9efcf50b6d37e5d Mon Sep 17 00:00:00 2001 From: MrDulfin <Dulfinaminator@gmail.com> Date: Sat, 9 Dec 2023 01:52:57 -0500 Subject: [PATCH 045/136] Added Playlists, Implemented MusicCollection for Album and Playlist --- src/music_storage/{music_db.rs => library.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/music_storage/{music_db.rs => library.rs} (100%) diff --git a/src/music_storage/music_db.rs b/src/music_storage/library.rs similarity index 100% rename from src/music_storage/music_db.rs rename to src/music_storage/library.rs From 15b49840543a21781dc4010bff93e11c63b973f6 Mon Sep 17 00:00:00 2001 From: MrDulfin <Dulfinaminator@gmail.com> Date: Sat, 9 Dec 2023 01:57:46 -0500 Subject: [PATCH 046/136] changed music_db.rs to library.rs --- src/lib.rs | 3 ++- src/music_controller/controller.rs | 2 +- src/music_player.rs | 11 +------- src/music_storage/library.rs | 37 ++++++++++++++------------- src/music_storage/music_collection.rs | 8 ++++++ src/music_storage/playlist.rs | 29 ++++++++++++++++++--- src/music_storage/utils.rs | 2 +- 7 files changed, 57 insertions(+), 35 deletions(-) create mode 100644 src/music_storage/music_collection.rs diff --git a/src/lib.rs b/src/lib.rs index d98209c..31538fa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,8 @@ pub mod music_storage { - pub mod music_db; + pub mod library; pub mod playlist; mod utils; + pub mod music_collection; } pub mod music_controller { diff --git a/src/music_controller/controller.rs b/src/music_controller/controller.rs index 333db0b..6d0acb4 100644 --- a/src/music_controller/controller.rs +++ b/src/music_controller/controller.rs @@ -2,7 +2,7 @@ use std::path::PathBuf; use std::sync::{Arc, RwLock}; use crate::music_controller::config::Config; -use crate::music_storage::music_db::{MusicLibrary, Song, Tag}; +use crate::music_storage::library::{MusicLibrary, Song, Tag}; pub struct MusicController { pub config: Arc<RwLock<Config>>, diff --git a/src/music_player.rs b/src/music_player.rs index 0c9b2d6..ecafff6 100644 --- a/src/music_player.rs +++ b/src/music_player.rs @@ -1,6 +1,6 @@ // Crate things //use crate::music_controller::config::Config; -use crate::music_storage::music_db::{Tag, URI}; +use crate::music_storage::library::{Tag, URI}; use crossbeam_channel::bounded; use std::error::Error; use std::sync::{Arc, RwLock}; @@ -180,7 +180,6 @@ impl Player { gst::MessageView::Eos(_) => {} gst::MessageView::StreamStart(_) => println!("Stream start"), gst::MessageView::Error(e) => { - println!("ERROR: {}", e.error()); playbin_bus_ctrl .write() .unwrap() @@ -193,14 +192,6 @@ impl Player { .set_state(gst::State::Playing) .unwrap(); }, - gst::MessageView::Tag(tag) => { - if let Some(title) = tag.tags().get::<gst::tags::Title>() { - println!(" Title: {}", title.get()); - } - if let Some(album) = tag.tags().get::<gst::tags::Album>() { - println!(" Album: {}", album.get()); - } - } gst::MessageView::Buffering(buffering) => { let percent = buffering.percent(); if percent < 100 { diff --git a/src/music_storage/library.rs b/src/music_storage/library.rs index 41d2e22..4fdcabd 100644 --- a/src/music_storage/library.rs +++ b/src/music_storage/library.rs @@ -1,3 +1,4 @@ +use super::music_collection::MusicCollection; // Crate things use super::utils::{find_images, normalize, read_library, write_library}; use crate::music_controller::config::Config; @@ -269,33 +270,16 @@ pub struct Album<'a> { #[allow(clippy::len_without_is_empty)] 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<usize, Vec<&Song>> { &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> { @@ -311,6 +295,23 @@ impl Album<'_> { total } } +impl MusicCollection for Album<'_> { + //returns the Album title + fn title(&self) -> &String { + self.title + } + /// Returns the album cover as an AlbumArt struct, if it exists + fn cover(&self) -> Option<&AlbumArt> { + self.cover + } + fn tracks(&self) -> Vec<&Song> { + let mut songs = Vec::new(); + for disc in &self.discs { + songs.append(&mut disc.1.clone()) + } + songs + } +} const BLOCKED_EXTENSIONS: [&str; 4] = ["vob", "log", "txt", "sf2"]; diff --git a/src/music_storage/music_collection.rs b/src/music_storage/music_collection.rs new file mode 100644 index 0000000..8abcd71 --- /dev/null +++ b/src/music_storage/music_collection.rs @@ -0,0 +1,8 @@ +use crate::music_storage::library::{ AlbumArt, Song }; + +pub trait MusicCollection { + fn title(&self) -> &String; + fn cover(&self) -> Option<&AlbumArt>; + fn tracks(&self) -> Vec<&Song>; +} + diff --git a/src/music_storage/playlist.rs b/src/music_storage/playlist.rs index 4f8892d..2ad4742 100644 --- a/src/music_storage/playlist.rs +++ b/src/music_storage/playlist.rs @@ -1,6 +1,27 @@ -use crate::music_controller::config::Config; -use std::path::Path; +use walkdir::Error; -pub fn playlist_add(_config: &Config, _playlist_name: &str, _song_paths: &[&Path]) { - unimplemented!() +use crate::music_controller::config::Config; +use std::{path::Path, default, thread::AccessError}; + +use super::{library::{AlbumArt, Song}, music_collection::MusicCollection}; + +#[derive(Debug, Default)] +pub struct Playlist<'a> { + title: String, + cover: Option<&'a AlbumArt>, + tracks: Vec<&'a Song>, +} +impl MusicCollection for Playlist<'_> { + fn title(&self) -> &String { + &self.title + } + fn cover(&self) -> Option<&AlbumArt> { + match self.cover { + Some(e) => Some(e), + None => None, + } + } + fn tracks(&self) -> Vec<&Song> { + self.tracks.clone() + } } diff --git a/src/music_storage/utils.rs b/src/music_storage/utils.rs index 03e9740..a25313a 100644 --- a/src/music_storage/utils.rs +++ b/src/music_storage/utils.rs @@ -6,7 +6,7 @@ use walkdir::WalkDir; use snap; -use super::music_db::{AlbumArt, Song, URI}; +use super::library::{AlbumArt, Song, URI}; use unidecode::unidecode; pub(super) fn normalize(input_string: &str) -> String { From 594e426a3f1c3c28c3fa291a053a2d6c8800cf8d Mon Sep 17 00:00:00 2001 From: MrDulfin <Dulfinaminator@gmail.com> Date: Sat, 9 Dec 2023 23:37:22 -0500 Subject: [PATCH 047/136] Added db_reader, implemented ExternalLibrary, updated Playlist --- Cargo.toml | 2 + src/lib.rs | 14 ++ src/music_storage/db_reader/common.rs | 51 ++++ src/music_storage/db_reader/extern_library.rs | 9 + src/music_storage/db_reader/foobar/reader.rs | 176 ++++++++++++++ .../db_reader/musicbee/reader.rs | 220 +++++++++++++++++ src/music_storage/db_reader/musicbee/utils.rs | 29 +++ src/music_storage/db_reader/xml/reader.rs | 222 ++++++++++++++++++ src/music_storage/library.rs | 5 +- src/music_storage/playlist.rs | 68 +++++- 10 files changed, 791 insertions(+), 5 deletions(-) create mode 100644 src/music_storage/db_reader/common.rs create mode 100644 src/music_storage/db_reader/extern_library.rs create mode 100644 src/music_storage/db_reader/foobar/reader.rs create mode 100644 src/music_storage/db_reader/musicbee/reader.rs create mode 100644 src/music_storage/db_reader/musicbee/utils.rs create mode 100644 src/music_storage/db_reader/xml/reader.rs diff --git a/Cargo.toml b/Cargo.toml index a7eb36f..76b0ba9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,3 +29,5 @@ gstreamer = "0.21.2" glib = "0.18.3" crossbeam-channel = "0.5.8" crossbeam = "0.8.2" +quick-xml = "0.31.0" +leb128 = "0.2.5" \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 31538fa..32a8453 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,20 @@ pub mod music_storage { pub mod playlist; mod utils; pub mod music_collection; + pub mod db_reader { + pub mod foobar { + pub mod reader; + } + pub mod musicbee { + pub mod utils; + pub mod reader; + } + pub mod xml { + pub mod reader; + } + pub mod common; + pub mod extern_library; + } } pub mod music_controller { diff --git a/src/music_storage/db_reader/common.rs b/src/music_storage/db_reader/common.rs new file mode 100644 index 0000000..80e0910 --- /dev/null +++ b/src/music_storage/db_reader/common.rs @@ -0,0 +1,51 @@ +use std::path::PathBuf; + +use chrono::{DateTime, TimeZone, Utc}; + +pub fn get_bytes<const S: usize>(iterator: &mut std::vec::IntoIter<u8>) -> [u8; S] { + let mut bytes = [0; S]; + + for i in 0..S { + bytes[i] = iterator.next().unwrap(); + } + + return bytes; +} + +pub fn get_bytes_vec(iterator: &mut std::vec::IntoIter<u8>, number: usize) -> Vec<u8> { + let mut bytes = Vec::new(); + + for _ in 0..number { + bytes.push(iterator.next().unwrap()); + } + + return bytes; +} + +/// Converts the windows DateTime into Chrono DateTime +pub fn get_datetime(iterator: &mut std::vec::IntoIter<u8>, topbyte: bool) -> DateTime<Utc> { + let mut datetime_i64 = i64::from_le_bytes(get_bytes(iterator)); + + if topbyte { + // Zero the topmost byte + datetime_i64 = datetime_i64 & 0x00FFFFFFFFFFFFFFF; + } + + if datetime_i64 <= 0 { + return Utc.timestamp_opt(0, 0).unwrap(); + } + + let unix_time_ticks = datetime_i64 - 621355968000000000; + + let unix_time_seconds = unix_time_ticks / 10000000; + + let unix_time_nanos = match (unix_time_ticks as f64 / 10000000.0) - unix_time_seconds as f64 + > 0.0 + { + true => ((unix_time_ticks as f64 / 10000000.0) - unix_time_seconds as f64) * 1000000000.0, + false => 0.0, + }; + + Utc.timestamp_opt(unix_time_seconds, unix_time_nanos as u32) + .unwrap() +} \ No newline at end of file diff --git a/src/music_storage/db_reader/extern_library.rs b/src/music_storage/db_reader/extern_library.rs new file mode 100644 index 0000000..ffe0711 --- /dev/null +++ b/src/music_storage/db_reader/extern_library.rs @@ -0,0 +1,9 @@ +use std::path::PathBuf; + + +pub trait ExternalLibrary { + fn from_file(&mut self, file: &PathBuf) -> Self; + fn write(&self) { + unimplemented!(); + } +} \ No newline at end of file diff --git a/src/music_storage/db_reader/foobar/reader.rs b/src/music_storage/db_reader/foobar/reader.rs new file mode 100644 index 0000000..017d7fc --- /dev/null +++ b/src/music_storage/db_reader/foobar/reader.rs @@ -0,0 +1,176 @@ +use chrono::{DateTime, Utc}; +use std::{fs::File, io::Read, path::PathBuf, time::Duration}; + +use crate::music_storage::db_reader::common::{get_bytes, get_bytes_vec, get_datetime}; + +const MAGIC: [u8; 16] = [ + 0xE1, 0xA0, 0x9C, 0x91, 0xF8, 0x3C, 0x77, 0x42, 0x85, 0x2C, 0x3B, 0xCC, 0x14, 0x01, 0xD3, 0xF2, +]; + +#[derive(Debug)] +pub struct FoobarPlaylist { + path: PathBuf, + metadata: Vec<u8>, +} + +#[derive(Debug, Default)] +pub struct FoobarPlaylistTrack { + flags: i32, + file_name: String, + subsong_index: i32, + file_size: i64, + file_time: DateTime<Utc>, + duration: Duration, + rpg_album: u32, + rpg_track: u32, + rpk_album: u32, + rpk_track: u32, + entries: Vec<(String, String)>, +} + +impl FoobarPlaylist { + pub fn new(path: &String) -> Self { + FoobarPlaylist { + path: PathBuf::from(path), + metadata: Vec::new(), + } + } + + fn get_meta_offset(&self, offset: usize) -> String { + let mut result_vec = Vec::new(); + + let mut i = offset; + loop { + if self.metadata[i] == 0x00 { + break; + } + + result_vec.push(self.metadata[i]); + i += 1; + } + + String::from_utf8_lossy(&result_vec).into() + } + + /// Reads the entire MusicBee library and returns relevant values + /// as a `Vec` of `Song`s + pub fn read(&mut self) -> Result<Vec<FoobarPlaylistTrack>, Box<dyn std::error::Error>> { + let mut f = File::open(&self.path).unwrap(); + let mut buffer = Vec::new(); + let mut retrieved_songs: Vec<FoobarPlaylistTrack> = Vec::new(); + + // Read the whole file + f.read_to_end(&mut buffer)?; + + let mut buf_iter = buffer.into_iter(); + + // Parse the header + let magic = get_bytes::<16>(&mut buf_iter); + if magic != MAGIC { + return Err("Magic bytes mismatch!".into()); + } + + let meta_size = i32::from_le_bytes(get_bytes(&mut buf_iter)) as usize; + self.metadata = get_bytes_vec(&mut buf_iter, meta_size); + let track_count = i32::from_le_bytes(get_bytes(&mut buf_iter)); + + // Read all the track fields + for _ in 0..track_count { + let flags = i32::from_le_bytes(get_bytes(&mut buf_iter)); + + let has_metadata = (0x01 & flags) != 0; + let has_padding = (0x04 & flags) != 0; + + let file_name_offset = i32::from_le_bytes(get_bytes(&mut buf_iter)) as usize; + let file_name = self.get_meta_offset(file_name_offset); + + let subsong_index = i32::from_le_bytes(get_bytes(&mut buf_iter)); + + if !has_metadata { + let track = FoobarPlaylistTrack { + file_name, + subsong_index, + ..Default::default() + }; + retrieved_songs.push(track); + continue; + } + + let file_size = i64::from_le_bytes(get_bytes(&mut buf_iter)); + + let file_time = get_datetime(&mut buf_iter, false); + + let duration = Duration::from_nanos(u64::from_le_bytes(get_bytes(&mut buf_iter)) / 100); + + let rpg_album = u32::from_le_bytes(get_bytes(&mut buf_iter)); + + let rpg_track = u32::from_le_bytes(get_bytes(&mut buf_iter)); + + let rpk_album = u32::from_le_bytes(get_bytes(&mut buf_iter)); + + let rpk_track = u32::from_le_bytes(get_bytes(&mut buf_iter)); + + get_bytes::<4>(&mut buf_iter); + + let mut entries = Vec::new(); + let primary_count = i32::from_le_bytes(get_bytes(&mut buf_iter)); + let secondary_count = i32::from_le_bytes(get_bytes(&mut buf_iter)); + let _secondary_offset = i32::from_le_bytes(get_bytes(&mut buf_iter)); + + // Get primary keys + for _ in 0..primary_count { + println!("{}", i32::from_le_bytes(get_bytes(&mut buf_iter))); + + let key = + self.get_meta_offset(i32::from_le_bytes(get_bytes(&mut buf_iter)) as usize); + + entries.push((key, String::new())); + } + + // Consume unknown 32 bit value + println!("unk"); + get_bytes::<4>(&mut buf_iter); + + // Get primary values + for i in 0..primary_count { + println!("primkey {i}"); + + let value = + self.get_meta_offset(i32::from_le_bytes(get_bytes(&mut buf_iter)) as usize); + + entries[i as usize].1 = value; + } + + // Get secondary Keys + for _ in 0..secondary_count { + let key = + self.get_meta_offset(i32::from_le_bytes(get_bytes(&mut buf_iter)) as usize); + let value = + self.get_meta_offset(i32::from_le_bytes(get_bytes(&mut buf_iter)) as usize); + entries.push((key, value)); + } + + if has_padding { + get_bytes::<64>(&mut buf_iter); + } + + let track = FoobarPlaylistTrack { + flags, + file_name, + subsong_index, + file_size, + file_time, + duration, + rpg_album, + rpg_track, + rpk_album, + rpk_track, + entries, + }; + + retrieved_songs.push(track); + } + + Ok(retrieved_songs) + } +} diff --git a/src/music_storage/db_reader/musicbee/reader.rs b/src/music_storage/db_reader/musicbee/reader.rs new file mode 100644 index 0000000..0a5cac6 --- /dev/null +++ b/src/music_storage/db_reader/musicbee/reader.rs @@ -0,0 +1,220 @@ +use super::utils::get_string; +use crate::music_storage::db_reader::common::{get_bytes, get_datetime}; +use chrono::{DateTime, Utc}; +use std::fs::File; +use std::io::prelude::*; +use std::time::Duration; + +pub struct MusicBeeDatabase { + path: String, +} + +impl MusicBeeDatabase { + pub fn new(path: String) -> MusicBeeDatabase { + MusicBeeDatabase { path } + } + + /// Reads the entire MusicBee library and returns relevant values + /// as a `Vec` of `Song`s + pub fn read(&self) -> Result<Vec<MusicBeeSong>, Box<dyn std::error::Error>> { + let mut f = File::open(&self.path).unwrap(); + let mut buffer = Vec::new(); + let mut retrieved_songs: Vec<MusicBeeSong> = Vec::new(); + + // Read the whole file + f.read_to_end(&mut buffer)?; + + let mut buf_iter = buffer.into_iter(); + + // Get the song count from the first 4 bytes + // and then right shift it by 8 for some reason + let mut database_song_count = i32::from_le_bytes(get_bytes(&mut buf_iter)); + database_song_count = database_song_count >> 8; + + let mut song_count = 0; + loop { + // If the file designation is 1, then the end of the database + // has been reached + let file_designation = match buf_iter.next() { + Some(1) => break, + Some(value) => value, + None => break, + }; + + song_count += 1; + + // Get the file status. Unknown what this means + let status = buf_iter.next().unwrap(); + + buf_iter.next(); // Read in a byte to throw it away + + // Get the play count + let play_count = u16::from_le_bytes(get_bytes(&mut buf_iter)); + + // Get the time the song was last played, stored as a signed 64 bit number of microseconds + let last_played = get_datetime(buf_iter.by_ref(), true); + + // Get the number of times the song was skipped + let skip_count = u16::from_le_bytes(get_bytes(&mut buf_iter)); + + // Get the path to the song + let path = get_string(buf_iter.by_ref()); + + // Get the file size + let file_size = i32::from_le_bytes(get_bytes(&mut buf_iter)); + + // Get the sample rate + let sample_rate = i32::from_le_bytes(get_bytes(&mut buf_iter)); + + // Get the channel count + let channel_count = buf_iter.next().unwrap(); + + // Get the bitrate type (CBR, VBR, etc.) + let bitrate_type = buf_iter.next().unwrap(); + + // Get the actual bitrate + let bitrate = i16::from_le_bytes(get_bytes(&mut buf_iter)); + + // Get the track length in milliseconds + let track_length = + Duration::from_millis(i32::from_le_bytes(get_bytes(&mut buf_iter)) as u64); + + // Get the date added and modified in the same format + let date_added = get_datetime(buf_iter.by_ref(), true); + let date_modified = get_datetime(buf_iter.by_ref(), true); + + // Gets artwork information + // + // Artworks are stored as chunks describing the type + // (embedded, file), and some other information. + let mut artwork: Vec<MusicBeeAlbumArt> = vec![]; + loop { + let artwork_type = buf_iter.next().unwrap(); + if artwork_type > 253 { + break; + } + + let unknown_string = get_string(buf_iter.by_ref()); + let storage_mode = buf_iter.next().unwrap(); + let storage_path = get_string(buf_iter.by_ref()); + + artwork.push(MusicBeeAlbumArt { + artwork_type, + unknown_string, + storage_mode, + storage_path, + }); + } + + buf_iter.next(); // Read in a byte to throw it away + + // Gets all the tags on the song in the database + let mut tags: Vec<MusicBeeTag> = vec![]; + loop { + // If the tag code is 0, the end of the block has been reached, so break. + // + // If the tag code is 255, it pertains to some CUE file values that are not known + // throw away these values + let tag_code = match buf_iter.next() { + Some(0) => break, + Some(255) => { + let repeats = u16::from_le_bytes(get_bytes(&mut buf_iter)); + for _ in 0..(repeats * 13) - 2 { + buf_iter.next().unwrap(); + } + + 255 + } + Some(value) => value, + None => panic!(), + }; + + // Get the string value of the tag + let tag_value = get_string(buf_iter.by_ref()); + tags.push(MusicBeeTag { + tag_code, + tag_value, + }); + } + + // Construct the finished song and add it to the vec + let constructed_song = MusicBeeSong { + file_designation, + status, + play_count, + last_played, + skip_count, + path, + file_size, + sample_rate, + channel_count, + bitrate_type, + bitrate, + track_length, + date_added, + date_modified, + artwork, + tags, + }; + + retrieved_songs.push(constructed_song); + } + + println!("The database claims you have: {database_song_count} songs\nThe retrieved number is: {song_count} songs"); + + match database_song_count == song_count { + true => Ok(retrieved_songs), + false => Err("Song counts do not match!".into()), + } + } +} + +#[derive(Debug)] +pub struct MusicBeeTag { + tag_code: u8, + tag_value: String, +} + +#[derive(Debug)] +pub struct MusicBeeAlbumArt { + artwork_type: u8, + unknown_string: String, + storage_mode: u8, + storage_path: String, +} + +#[derive(Debug)] +pub struct MusicBeeSong { + file_designation: u8, + status: u8, + play_count: u16, + pub last_played: DateTime<Utc>, + skip_count: u16, + path: String, + file_size: i32, + sample_rate: i32, + channel_count: u8, + bitrate_type: u8, + bitrate: i16, + track_length: Duration, + date_added: DateTime<Utc>, + date_modified: DateTime<Utc>, + + /* Album art stuff */ + artwork: Vec<MusicBeeAlbumArt>, + + /* All tags */ + tags: Vec<MusicBeeTag>, +} + +impl MusicBeeSong { + pub fn get_tag_code(self, code: u8) -> Option<String> { + for tag in &self.tags { + if tag.tag_code == code { + return Some(tag.tag_value.clone()); + } + } + + None + } +} diff --git a/src/music_storage/db_reader/musicbee/utils.rs b/src/music_storage/db_reader/musicbee/utils.rs new file mode 100644 index 0000000..65d6333 --- /dev/null +++ b/src/music_storage/db_reader/musicbee/utils.rs @@ -0,0 +1,29 @@ +use leb128; + +/// Gets a string from the MusicBee database format +/// +/// The length of the string is defined by an LEB128 encoded value at the beginning, followed by the string of that length +pub fn get_string(iterator: &mut std::vec::IntoIter<u8>) -> String { + let mut string_length = iterator.next().unwrap() as usize; + if string_length == 0 { + return String::new(); + } + + // Decode the LEB128 value + let mut leb_bytes: Vec<u8> = vec![]; + loop { + leb_bytes.push(string_length as u8); + + if string_length >> 7 != 1 { + break; + } + string_length = iterator.next().unwrap() as usize; + } + string_length = leb128::read::unsigned(&mut leb_bytes.as_slice()).unwrap() as usize; + + let mut string_bytes = vec![]; + for _ in 0..string_length { + string_bytes.push(iterator.next().unwrap()); + } + String::from_utf8(string_bytes).unwrap() +} diff --git a/src/music_storage/db_reader/xml/reader.rs b/src/music_storage/db_reader/xml/reader.rs new file mode 100644 index 0000000..de2a057 --- /dev/null +++ b/src/music_storage/db_reader/xml/reader.rs @@ -0,0 +1,222 @@ +use quick_xml::events::Event; +use quick_xml::reader::Reader; + +use std::collections::{BTreeMap, HashMap}; +use std::io::Error; +use std::path::PathBuf; +use std::str::FromStr; +use std::vec::Vec; + +use chrono::prelude::*; + +use crate::music_storage::db_reader::extern_library::ExternalLibrary; + +#[derive(Debug, Default, Clone)] +pub struct XmlLibrary { + tracks: Vec<XMLSong> +} +impl XmlLibrary { + fn new() -> Self { + Default::default() + } +} +impl ExternalLibrary for XmlLibrary { + fn from_file(&mut self, file: &PathBuf) -> Self { + let mut reader = Reader::from_file(file).unwrap(); + reader.trim_text(true); + //count every event, for fun ig? + let mut count = 0; + //count for skipping useless beginning key + let mut count2 = 0; + //number of grabbed songs + let mut count3 = 0; + //number of IDs skipped + let mut count4 = 0; + + let mut buf = Vec::new(); + let mut skip = false; + + let mut converted_songs: Vec<XMLSong> = Vec::new(); + + + let mut song_tags: HashMap<String, String> = HashMap::new(); + let mut key: String = String::new(); + let mut tagvalue: String = String::new(); + let mut key_selected = false; + + use std::time::Instant; + let now = Instant::now(); + + loop { + //push tag to song_tags map + if !key.is_empty() && !tagvalue.is_empty() { + song_tags.insert(key.clone(), tagvalue.clone()); + key.clear(); + tagvalue.clear(); + key_selected = false; + + //end the song to start a new one, and turn turn current song map into XMLSong + if song_tags.contains_key(&"Location".to_string()) { + count3 += 1; + //check for skipped IDs + if &count3.to_string() + != song_tags.get_key_value(&"Track ID".to_string()).unwrap().1 + { + count3 += 1; + count4 += 1; + } + converted_songs.push(XMLSong::from_hashmap(&mut song_tags).unwrap()); + song_tags.clear(); + skip = true; + } + } + match reader.read_event_into(&mut buf) { + Ok(Event::Start(_)) => { + count += 1; + count2 += 1; + } + Ok(Event::Text(e)) => { + if count < 17 && count != 10 { + continue; + }else if skip { + skip = false; + continue; + } + + let text = e.unescape().unwrap().to_string(); + + if text == count2.to_string() && !key_selected { + continue; + } + + //Add the key/value depenidng on if the key is selected or not ⛩️sorry buzz + + match key_selected { + true => tagvalue.push_str(&text), + false => { + key.push_str(&text); + if !key.is_empty() { + key_selected = true + } else { + panic!("Key not selected?!") + } + } + _ => panic!("WHAT DID YOU JUST DO?!🐰🐰🐰🐰"), + } + } + Err(e) => panic!("Error at position {}: {:?}", reader.buffer_position(), e), + Ok(Event::Eof) => break, + _ => (), + } + buf.clear(); + } + let elasped = now.elapsed(); + println!("\n\nXMLReader\n=========================================\n\nDone!\n{} songs grabbed in {:#?}\nIDs Skipped: {}", count3, elasped, count4); + // dbg!(folder); + self.tracks.append(converted_songs.as_mut()); + self.clone() + } +} +#[derive(Debug, Clone, Default)] +pub struct XMLSong { + pub id: i32, + pub plays: i32, + pub favorited: bool, + pub banned: bool, + pub rating: Option<u8>, + pub format: Option<String>, + pub song_type: Option<String>, + pub last_played: Option<DateTime<Utc>>, + pub date_added: Option<DateTime<Utc>>, + pub date_modified: Option<DateTime<Utc>>, + pub tags: BTreeMap<String, String>, + pub location: String, +} + +impl XMLSong { + pub fn new() -> XMLSong { + Default::default() + } + + + fn from_hashmap(map: &mut HashMap<String, String>) -> Result<XMLSong, Error> { + let mut song = XMLSong::new(); + //get the path with the first bit chopped off + let path_: String = map.get_key_value("Location").unwrap().1.clone(); + let track_type: String = map.get_key_value("Track Type").unwrap().1.clone(); + let path: String = match track_type.as_str() { + "File" => { + if path_.contains("file://localhost/") { + path_.strip_prefix("file://localhost/").unwrap(); + } + path_ + } + "URL" => path_, + _ => path_, + }; + + for (key, value) in map { + match key.as_str() { + "Track ID" => song.id = value.parse().unwrap(), + "Location" => song.location = path.to_string(), + "Play Count" => song.plays = value.parse().unwrap(), + "Love" => { + //check if the track is (L)Loved or (B)Banned + match value.as_str() { + "L" => song.favorited = true, + "B" => song.banned = false, + _ => continue, + } + } + "Rating" => song.rating = Some(value.parse().unwrap()), + "Kind" => song.format = Some(value.to_string()), + "Play Date UTC" => { + song.last_played = Some(DateTime::<Utc>::from_str(value).unwrap()) + } + "Date Added" => song.date_added = Some(DateTime::<Utc>::from_str(value).unwrap()), + "Date Modified" => { + song.date_modified = Some(DateTime::<Utc>::from_str(value).unwrap()) + } + "Track Type" => song.song_type = Some(value.to_string()), + _ => { + song.tags.insert(key.to_string(), value.to_string()); + } + } + } + // println!("{:.2?}", song); + Ok(song) + } +} + + +pub fn get_folder(file: &PathBuf) -> String { + let mut reader = Reader::from_file(file).unwrap(); + reader.trim_text(true); + //count every event, for fun ig? + let mut count = 0; + let mut buf = Vec::new(); + let mut folder = String::new(); + loop { + match reader.read_event_into(&mut buf) { + Ok(Event::Start(_)) => { + count += 1; + } + Ok(Event::Text(e)) => { + if count == 10 { + folder = String::from( + e.unescape() + .unwrap() + .to_string() + .strip_prefix("file://localhost/") + .unwrap(), + ); + return folder; + } + } + Err(_e) => { + panic!("oh no! something happened in the public function `get_reader_from_xml()!`") + } + _ => (), + } + } +} diff --git a/src/music_storage/library.rs b/src/music_storage/library.rs index 4fdcabd..d264bdf 100644 --- a/src/music_storage/library.rs +++ b/src/music_storage/library.rs @@ -29,7 +29,7 @@ use serde::{Deserialize, Serialize}; use rayon::prelude::*; use std::sync::{Arc, Mutex, RwLock}; -#[derive(Debug, Clone, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] pub enum AlbumArt { Embedded(usize), External(URI), @@ -115,7 +115,7 @@ impl ToString for Field { } /// Stores information about a single song -#[derive(Debug, Clone, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] pub struct Song { pub location: URI, pub plays: i32, @@ -275,7 +275,6 @@ impl Album<'_> { self.artist } - pub fn discs(&self) -> &BTreeMap<usize, Vec<&Song>> { &self.discs diff --git a/src/music_storage/playlist.rs b/src/music_storage/playlist.rs index 2ad4742..710e6fd 100644 --- a/src/music_storage/playlist.rs +++ b/src/music_storage/playlist.rs @@ -1,15 +1,68 @@ +use chrono::Duration; use walkdir::Error; use crate::music_controller::config::Config; use std::{path::Path, default, thread::AccessError}; -use super::{library::{AlbumArt, Song}, music_collection::MusicCollection}; +use super::{library::{AlbumArt, Song, self, Tag}, music_collection::MusicCollection}; -#[derive(Debug, Default)] +#[derive(Debug, Clone)] pub struct Playlist<'a> { title: String, cover: Option<&'a AlbumArt>, tracks: Vec<&'a Song>, + play_count: i32, + play_time: Duration, +} +impl<'a> Playlist<'a> { + pub fn new() -> Self { + Default::default() + } + pub fn play_count(&self) -> i32 { + self.play_count + } + pub fn play_time(&self) -> chrono::Duration { + self.play_time + } + pub fn set_tracks(&mut self, songs: Vec<&'a Song>) -> Result<(), Error> { + self.tracks = songs; + Ok(()) + } + pub fn add_track(&mut self, song: &'a Song) -> Result<(), Error> { + self.tracks.push(song); + Ok(()) + } + pub fn remove_track(&mut self, index: i32) -> Result<(), Error> { + let bun: usize = index as usize; + let mut name = String::new(); + if self.tracks.len() >= bun { + name = String::from(self.tracks[bun].tags.get_key_value(&Tag::Title).unwrap().1); + self.tracks.remove(bun); + } + dbg!(name); + Ok(()) + } + pub fn get_index(&self, song_name: &str) -> Option<usize> { + let mut index = 0; + if self.contains(&Tag::Title, song_name) { + for track in &self.tracks { + index += 1; + if song_name == track.tags.get_key_value(&Tag::Title).unwrap().1 { + dbg!("Index gotted! ",index); + return Some(index); + } + } + } + None + } + pub fn contains(&self, tag: &Tag, title: &str) -> bool { + for track in &self.tracks { + if title == track.tags.get_key_value(tag).unwrap().1 { + return true; + } + } + false + } } impl MusicCollection for Playlist<'_> { fn title(&self) -> &String { @@ -25,3 +78,14 @@ impl MusicCollection for Playlist<'_> { self.tracks.clone() } } +impl Default for Playlist<'_> { + fn default() -> Self { + Playlist { + title: String::default(), + cover: None, + tracks: Vec::default(), + play_count: -1, + play_time: Duration::zero(), + } + } +} \ No newline at end of file From 886fc1a3c8038b69fdb4767842179fbad574839d Mon Sep 17 00:00:00 2001 From: G2-Games <ke0bhogsg@gmail.com> Date: Sat, 9 Dec 2023 23:39:15 -0600 Subject: [PATCH 048/136] Updated `from_file()` in ExternalLibrary trait --- src/music_storage/db_reader/extern_library.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/music_storage/db_reader/extern_library.rs b/src/music_storage/db_reader/extern_library.rs index ffe0711..047fcc6 100644 --- a/src/music_storage/db_reader/extern_library.rs +++ b/src/music_storage/db_reader/extern_library.rs @@ -2,8 +2,8 @@ use std::path::PathBuf; pub trait ExternalLibrary { - fn from_file(&mut self, file: &PathBuf) -> Self; + fn from_file(file: &PathBuf) -> Self; fn write(&self) { unimplemented!(); } -} \ No newline at end of file +} From 0072963a6092cbbe3c5441547b22526fe156e3b6 Mon Sep 17 00:00:00 2001 From: MrDulfin <Dulfinaminator@gmail.com> Date: Sun, 10 Dec 2023 00:43:39 -0500 Subject: [PATCH 049/136] added ``to_songs()`` in ExternalLibrary --- src/music_storage/db_reader/extern_library.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/music_storage/db_reader/extern_library.rs b/src/music_storage/db_reader/extern_library.rs index ffe0711..fa5cd4e 100644 --- a/src/music_storage/db_reader/extern_library.rs +++ b/src/music_storage/db_reader/extern_library.rs @@ -1,9 +1,12 @@ use std::path::PathBuf; +use crate::music_storage::library::Song; + pub trait ExternalLibrary { - fn from_file(&mut self, file: &PathBuf) -> Self; + fn from_file(file: &PathBuf) -> Self; fn write(&self) { unimplemented!(); } + fn to_songs(&self) -> Vec<Song>; } \ No newline at end of file From 3c007ed886fa67dd6d426db70ba1928717297570 Mon Sep 17 00:00:00 2001 From: G2-Games <ke0bhogsg@gmail.com> Date: Sun, 10 Dec 2023 00:29:54 -0600 Subject: [PATCH 050/136] Removed MusicBee library reader --- .../db_reader/musicbee/reader.rs | 220 ------------------ src/music_storage/db_reader/musicbee/utils.rs | 29 --- 2 files changed, 249 deletions(-) delete mode 100644 src/music_storage/db_reader/musicbee/reader.rs delete mode 100644 src/music_storage/db_reader/musicbee/utils.rs diff --git a/src/music_storage/db_reader/musicbee/reader.rs b/src/music_storage/db_reader/musicbee/reader.rs deleted file mode 100644 index 0a5cac6..0000000 --- a/src/music_storage/db_reader/musicbee/reader.rs +++ /dev/null @@ -1,220 +0,0 @@ -use super::utils::get_string; -use crate::music_storage::db_reader::common::{get_bytes, get_datetime}; -use chrono::{DateTime, Utc}; -use std::fs::File; -use std::io::prelude::*; -use std::time::Duration; - -pub struct MusicBeeDatabase { - path: String, -} - -impl MusicBeeDatabase { - pub fn new(path: String) -> MusicBeeDatabase { - MusicBeeDatabase { path } - } - - /// Reads the entire MusicBee library and returns relevant values - /// as a `Vec` of `Song`s - pub fn read(&self) -> Result<Vec<MusicBeeSong>, Box<dyn std::error::Error>> { - let mut f = File::open(&self.path).unwrap(); - let mut buffer = Vec::new(); - let mut retrieved_songs: Vec<MusicBeeSong> = Vec::new(); - - // Read the whole file - f.read_to_end(&mut buffer)?; - - let mut buf_iter = buffer.into_iter(); - - // Get the song count from the first 4 bytes - // and then right shift it by 8 for some reason - let mut database_song_count = i32::from_le_bytes(get_bytes(&mut buf_iter)); - database_song_count = database_song_count >> 8; - - let mut song_count = 0; - loop { - // If the file designation is 1, then the end of the database - // has been reached - let file_designation = match buf_iter.next() { - Some(1) => break, - Some(value) => value, - None => break, - }; - - song_count += 1; - - // Get the file status. Unknown what this means - let status = buf_iter.next().unwrap(); - - buf_iter.next(); // Read in a byte to throw it away - - // Get the play count - let play_count = u16::from_le_bytes(get_bytes(&mut buf_iter)); - - // Get the time the song was last played, stored as a signed 64 bit number of microseconds - let last_played = get_datetime(buf_iter.by_ref(), true); - - // Get the number of times the song was skipped - let skip_count = u16::from_le_bytes(get_bytes(&mut buf_iter)); - - // Get the path to the song - let path = get_string(buf_iter.by_ref()); - - // Get the file size - let file_size = i32::from_le_bytes(get_bytes(&mut buf_iter)); - - // Get the sample rate - let sample_rate = i32::from_le_bytes(get_bytes(&mut buf_iter)); - - // Get the channel count - let channel_count = buf_iter.next().unwrap(); - - // Get the bitrate type (CBR, VBR, etc.) - let bitrate_type = buf_iter.next().unwrap(); - - // Get the actual bitrate - let bitrate = i16::from_le_bytes(get_bytes(&mut buf_iter)); - - // Get the track length in milliseconds - let track_length = - Duration::from_millis(i32::from_le_bytes(get_bytes(&mut buf_iter)) as u64); - - // Get the date added and modified in the same format - let date_added = get_datetime(buf_iter.by_ref(), true); - let date_modified = get_datetime(buf_iter.by_ref(), true); - - // Gets artwork information - // - // Artworks are stored as chunks describing the type - // (embedded, file), and some other information. - let mut artwork: Vec<MusicBeeAlbumArt> = vec![]; - loop { - let artwork_type = buf_iter.next().unwrap(); - if artwork_type > 253 { - break; - } - - let unknown_string = get_string(buf_iter.by_ref()); - let storage_mode = buf_iter.next().unwrap(); - let storage_path = get_string(buf_iter.by_ref()); - - artwork.push(MusicBeeAlbumArt { - artwork_type, - unknown_string, - storage_mode, - storage_path, - }); - } - - buf_iter.next(); // Read in a byte to throw it away - - // Gets all the tags on the song in the database - let mut tags: Vec<MusicBeeTag> = vec![]; - loop { - // If the tag code is 0, the end of the block has been reached, so break. - // - // If the tag code is 255, it pertains to some CUE file values that are not known - // throw away these values - let tag_code = match buf_iter.next() { - Some(0) => break, - Some(255) => { - let repeats = u16::from_le_bytes(get_bytes(&mut buf_iter)); - for _ in 0..(repeats * 13) - 2 { - buf_iter.next().unwrap(); - } - - 255 - } - Some(value) => value, - None => panic!(), - }; - - // Get the string value of the tag - let tag_value = get_string(buf_iter.by_ref()); - tags.push(MusicBeeTag { - tag_code, - tag_value, - }); - } - - // Construct the finished song and add it to the vec - let constructed_song = MusicBeeSong { - file_designation, - status, - play_count, - last_played, - skip_count, - path, - file_size, - sample_rate, - channel_count, - bitrate_type, - bitrate, - track_length, - date_added, - date_modified, - artwork, - tags, - }; - - retrieved_songs.push(constructed_song); - } - - println!("The database claims you have: {database_song_count} songs\nThe retrieved number is: {song_count} songs"); - - match database_song_count == song_count { - true => Ok(retrieved_songs), - false => Err("Song counts do not match!".into()), - } - } -} - -#[derive(Debug)] -pub struct MusicBeeTag { - tag_code: u8, - tag_value: String, -} - -#[derive(Debug)] -pub struct MusicBeeAlbumArt { - artwork_type: u8, - unknown_string: String, - storage_mode: u8, - storage_path: String, -} - -#[derive(Debug)] -pub struct MusicBeeSong { - file_designation: u8, - status: u8, - play_count: u16, - pub last_played: DateTime<Utc>, - skip_count: u16, - path: String, - file_size: i32, - sample_rate: i32, - channel_count: u8, - bitrate_type: u8, - bitrate: i16, - track_length: Duration, - date_added: DateTime<Utc>, - date_modified: DateTime<Utc>, - - /* Album art stuff */ - artwork: Vec<MusicBeeAlbumArt>, - - /* All tags */ - tags: Vec<MusicBeeTag>, -} - -impl MusicBeeSong { - pub fn get_tag_code(self, code: u8) -> Option<String> { - for tag in &self.tags { - if tag.tag_code == code { - return Some(tag.tag_value.clone()); - } - } - - None - } -} diff --git a/src/music_storage/db_reader/musicbee/utils.rs b/src/music_storage/db_reader/musicbee/utils.rs deleted file mode 100644 index 65d6333..0000000 --- a/src/music_storage/db_reader/musicbee/utils.rs +++ /dev/null @@ -1,29 +0,0 @@ -use leb128; - -/// Gets a string from the MusicBee database format -/// -/// The length of the string is defined by an LEB128 encoded value at the beginning, followed by the string of that length -pub fn get_string(iterator: &mut std::vec::IntoIter<u8>) -> String { - let mut string_length = iterator.next().unwrap() as usize; - if string_length == 0 { - return String::new(); - } - - // Decode the LEB128 value - let mut leb_bytes: Vec<u8> = vec![]; - loop { - leb_bytes.push(string_length as u8); - - if string_length >> 7 != 1 { - break; - } - string_length = iterator.next().unwrap() as usize; - } - string_length = leb128::read::unsigned(&mut leb_bytes.as_slice()).unwrap() as usize; - - let mut string_bytes = vec![]; - for _ in 0..string_length { - string_bytes.push(iterator.next().unwrap()); - } - String::from_utf8(string_bytes).unwrap() -} From e5bfde684631782e53a3f44f069413dd3c6c0448 Mon Sep 17 00:00:00 2001 From: G2-Games <ke0bhogsg@gmail.com> Date: Sun, 10 Dec 2023 00:36:20 -0600 Subject: [PATCH 051/136] Revert "Removed MusicBee library reader" This reverts commit 942f12f3ebc1c557b8023ad8494375c4115404e1. --- .../db_reader/musicbee/reader.rs | 220 ++++++++++++++++++ src/music_storage/db_reader/musicbee/utils.rs | 29 +++ 2 files changed, 249 insertions(+) create mode 100644 src/music_storage/db_reader/musicbee/reader.rs create mode 100644 src/music_storage/db_reader/musicbee/utils.rs diff --git a/src/music_storage/db_reader/musicbee/reader.rs b/src/music_storage/db_reader/musicbee/reader.rs new file mode 100644 index 0000000..0a5cac6 --- /dev/null +++ b/src/music_storage/db_reader/musicbee/reader.rs @@ -0,0 +1,220 @@ +use super::utils::get_string; +use crate::music_storage::db_reader::common::{get_bytes, get_datetime}; +use chrono::{DateTime, Utc}; +use std::fs::File; +use std::io::prelude::*; +use std::time::Duration; + +pub struct MusicBeeDatabase { + path: String, +} + +impl MusicBeeDatabase { + pub fn new(path: String) -> MusicBeeDatabase { + MusicBeeDatabase { path } + } + + /// Reads the entire MusicBee library and returns relevant values + /// as a `Vec` of `Song`s + pub fn read(&self) -> Result<Vec<MusicBeeSong>, Box<dyn std::error::Error>> { + let mut f = File::open(&self.path).unwrap(); + let mut buffer = Vec::new(); + let mut retrieved_songs: Vec<MusicBeeSong> = Vec::new(); + + // Read the whole file + f.read_to_end(&mut buffer)?; + + let mut buf_iter = buffer.into_iter(); + + // Get the song count from the first 4 bytes + // and then right shift it by 8 for some reason + let mut database_song_count = i32::from_le_bytes(get_bytes(&mut buf_iter)); + database_song_count = database_song_count >> 8; + + let mut song_count = 0; + loop { + // If the file designation is 1, then the end of the database + // has been reached + let file_designation = match buf_iter.next() { + Some(1) => break, + Some(value) => value, + None => break, + }; + + song_count += 1; + + // Get the file status. Unknown what this means + let status = buf_iter.next().unwrap(); + + buf_iter.next(); // Read in a byte to throw it away + + // Get the play count + let play_count = u16::from_le_bytes(get_bytes(&mut buf_iter)); + + // Get the time the song was last played, stored as a signed 64 bit number of microseconds + let last_played = get_datetime(buf_iter.by_ref(), true); + + // Get the number of times the song was skipped + let skip_count = u16::from_le_bytes(get_bytes(&mut buf_iter)); + + // Get the path to the song + let path = get_string(buf_iter.by_ref()); + + // Get the file size + let file_size = i32::from_le_bytes(get_bytes(&mut buf_iter)); + + // Get the sample rate + let sample_rate = i32::from_le_bytes(get_bytes(&mut buf_iter)); + + // Get the channel count + let channel_count = buf_iter.next().unwrap(); + + // Get the bitrate type (CBR, VBR, etc.) + let bitrate_type = buf_iter.next().unwrap(); + + // Get the actual bitrate + let bitrate = i16::from_le_bytes(get_bytes(&mut buf_iter)); + + // Get the track length in milliseconds + let track_length = + Duration::from_millis(i32::from_le_bytes(get_bytes(&mut buf_iter)) as u64); + + // Get the date added and modified in the same format + let date_added = get_datetime(buf_iter.by_ref(), true); + let date_modified = get_datetime(buf_iter.by_ref(), true); + + // Gets artwork information + // + // Artworks are stored as chunks describing the type + // (embedded, file), and some other information. + let mut artwork: Vec<MusicBeeAlbumArt> = vec![]; + loop { + let artwork_type = buf_iter.next().unwrap(); + if artwork_type > 253 { + break; + } + + let unknown_string = get_string(buf_iter.by_ref()); + let storage_mode = buf_iter.next().unwrap(); + let storage_path = get_string(buf_iter.by_ref()); + + artwork.push(MusicBeeAlbumArt { + artwork_type, + unknown_string, + storage_mode, + storage_path, + }); + } + + buf_iter.next(); // Read in a byte to throw it away + + // Gets all the tags on the song in the database + let mut tags: Vec<MusicBeeTag> = vec![]; + loop { + // If the tag code is 0, the end of the block has been reached, so break. + // + // If the tag code is 255, it pertains to some CUE file values that are not known + // throw away these values + let tag_code = match buf_iter.next() { + Some(0) => break, + Some(255) => { + let repeats = u16::from_le_bytes(get_bytes(&mut buf_iter)); + for _ in 0..(repeats * 13) - 2 { + buf_iter.next().unwrap(); + } + + 255 + } + Some(value) => value, + None => panic!(), + }; + + // Get the string value of the tag + let tag_value = get_string(buf_iter.by_ref()); + tags.push(MusicBeeTag { + tag_code, + tag_value, + }); + } + + // Construct the finished song and add it to the vec + let constructed_song = MusicBeeSong { + file_designation, + status, + play_count, + last_played, + skip_count, + path, + file_size, + sample_rate, + channel_count, + bitrate_type, + bitrate, + track_length, + date_added, + date_modified, + artwork, + tags, + }; + + retrieved_songs.push(constructed_song); + } + + println!("The database claims you have: {database_song_count} songs\nThe retrieved number is: {song_count} songs"); + + match database_song_count == song_count { + true => Ok(retrieved_songs), + false => Err("Song counts do not match!".into()), + } + } +} + +#[derive(Debug)] +pub struct MusicBeeTag { + tag_code: u8, + tag_value: String, +} + +#[derive(Debug)] +pub struct MusicBeeAlbumArt { + artwork_type: u8, + unknown_string: String, + storage_mode: u8, + storage_path: String, +} + +#[derive(Debug)] +pub struct MusicBeeSong { + file_designation: u8, + status: u8, + play_count: u16, + pub last_played: DateTime<Utc>, + skip_count: u16, + path: String, + file_size: i32, + sample_rate: i32, + channel_count: u8, + bitrate_type: u8, + bitrate: i16, + track_length: Duration, + date_added: DateTime<Utc>, + date_modified: DateTime<Utc>, + + /* Album art stuff */ + artwork: Vec<MusicBeeAlbumArt>, + + /* All tags */ + tags: Vec<MusicBeeTag>, +} + +impl MusicBeeSong { + pub fn get_tag_code(self, code: u8) -> Option<String> { + for tag in &self.tags { + if tag.tag_code == code { + return Some(tag.tag_value.clone()); + } + } + + None + } +} diff --git a/src/music_storage/db_reader/musicbee/utils.rs b/src/music_storage/db_reader/musicbee/utils.rs new file mode 100644 index 0000000..65d6333 --- /dev/null +++ b/src/music_storage/db_reader/musicbee/utils.rs @@ -0,0 +1,29 @@ +use leb128; + +/// Gets a string from the MusicBee database format +/// +/// The length of the string is defined by an LEB128 encoded value at the beginning, followed by the string of that length +pub fn get_string(iterator: &mut std::vec::IntoIter<u8>) -> String { + let mut string_length = iterator.next().unwrap() as usize; + if string_length == 0 { + return String::new(); + } + + // Decode the LEB128 value + let mut leb_bytes: Vec<u8> = vec![]; + loop { + leb_bytes.push(string_length as u8); + + if string_length >> 7 != 1 { + break; + } + string_length = iterator.next().unwrap() as usize; + } + string_length = leb128::read::unsigned(&mut leb_bytes.as_slice()).unwrap() as usize; + + let mut string_bytes = vec![]; + for _ in 0..string_length { + string_bytes.push(iterator.next().unwrap()); + } + String::from_utf8(string_bytes).unwrap() +} From 82e429895f20dc93b0e76c9bc7838f761b370bef Mon Sep 17 00:00:00 2001 From: MrDulfin <Dulfinaminator@gmail.com> Date: Mon, 11 Dec 2023 00:30:26 -0500 Subject: [PATCH 052/136] added to_songs() for XmlLibrary --- Cargo.toml | 3 +- src/lib.rs | 4 +- src/music_player.rs | 2 +- src/music_storage/db_reader/common.rs | 2 +- src/music_storage/db_reader/extern_library.rs | 1 - src/music_storage/db_reader/xml/reader.rs | 158 ++++++++++++++++-- src/music_storage/library.rs | 1 - src/music_storage/music_collection.rs | 3 +- src/music_storage/playlist.rs | 25 +-- 9 files changed, 167 insertions(+), 32 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 76b0ba9..1673cfd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,4 +30,5 @@ glib = "0.18.3" crossbeam-channel = "0.5.8" crossbeam = "0.8.2" quick-xml = "0.31.0" -leb128 = "0.2.5" \ No newline at end of file +leb128 = "0.2.5" +urlencoding = "2.1.3" \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 32a8453..9981527 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,15 +1,15 @@ pub mod music_storage { pub mod library; + pub mod music_collection; pub mod playlist; mod utils; - pub mod music_collection; pub mod db_reader { pub mod foobar { pub mod reader; } pub mod musicbee { - pub mod utils; pub mod reader; + pub mod utils; } pub mod xml { pub mod reader; diff --git a/src/music_player.rs b/src/music_player.rs index ecafff6..4c0ee19 100644 --- a/src/music_player.rs +++ b/src/music_player.rs @@ -191,7 +191,7 @@ impl Player { .unwrap() .set_state(gst::State::Playing) .unwrap(); - }, + } gst::MessageView::Buffering(buffering) => { let percent = buffering.percent(); if percent < 100 { diff --git a/src/music_storage/db_reader/common.rs b/src/music_storage/db_reader/common.rs index 80e0910..0849081 100644 --- a/src/music_storage/db_reader/common.rs +++ b/src/music_storage/db_reader/common.rs @@ -48,4 +48,4 @@ pub fn get_datetime(iterator: &mut std::vec::IntoIter<u8>, topbyte: bool) -> Dat Utc.timestamp_opt(unix_time_seconds, unix_time_nanos as u32) .unwrap() -} \ No newline at end of file +} diff --git a/src/music_storage/db_reader/extern_library.rs b/src/music_storage/db_reader/extern_library.rs index 5a0add5..dc5c306 100644 --- a/src/music_storage/db_reader/extern_library.rs +++ b/src/music_storage/db_reader/extern_library.rs @@ -2,7 +2,6 @@ use std::path::PathBuf; use crate::music_storage::library::Song; - pub trait ExternalLibrary { fn from_file(file: &PathBuf) -> Self; fn write(&self) { diff --git a/src/music_storage/db_reader/xml/reader.rs b/src/music_storage/db_reader/xml/reader.rs index de2a057..0b1689c 100644 --- a/src/music_storage/db_reader/xml/reader.rs +++ b/src/music_storage/db_reader/xml/reader.rs @@ -1,19 +1,26 @@ +use file_format::FileFormat; +use lofty::{AudioFile, LoftyError, ParseOptions, Probe, TagType, TaggedFileExt}; use quick_xml::events::Event; use quick_xml::reader::Reader; - use std::collections::{BTreeMap, HashMap}; +use std::fs::File; use std::io::Error; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::str::FromStr; +use std::time::Duration as StdDur; use std::vec::Vec; use chrono::prelude::*; use crate::music_storage::db_reader::extern_library::ExternalLibrary; +use crate::music_storage::library::{AlbumArt, Service, Song, Tag, URI}; +use crate::music_storage::utils; + +use urlencoding::decode; #[derive(Debug, Default, Clone)] pub struct XmlLibrary { - tracks: Vec<XMLSong> + tracks: Vec<XMLSong>, } impl XmlLibrary { fn new() -> Self { @@ -21,7 +28,7 @@ impl XmlLibrary { } } impl ExternalLibrary for XmlLibrary { - fn from_file(&mut self, file: &PathBuf) -> Self { + fn from_file(file: &PathBuf) -> Self { let mut reader = Reader::from_file(file).unwrap(); reader.trim_text(true); //count every event, for fun ig? @@ -38,7 +45,6 @@ impl ExternalLibrary for XmlLibrary { let mut converted_songs: Vec<XMLSong> = Vec::new(); - let mut song_tags: HashMap<String, String> = HashMap::new(); let mut key: String = String::new(); let mut tagvalue: String = String::new(); @@ -78,7 +84,7 @@ impl ExternalLibrary for XmlLibrary { Ok(Event::Text(e)) => { if count < 17 && count != 10 { continue; - }else if skip { + } else if skip { skip = false; continue; } @@ -113,10 +119,140 @@ impl ExternalLibrary for XmlLibrary { let elasped = now.elapsed(); println!("\n\nXMLReader\n=========================================\n\nDone!\n{} songs grabbed in {:#?}\nIDs Skipped: {}", count3, elasped, count4); // dbg!(folder); - self.tracks.append(converted_songs.as_mut()); - self.clone() + let mut lib = XmlLibrary::new(); + lib.tracks.append(converted_songs.as_mut()); + lib + } + fn to_songs(&self) -> Vec<crate::music_storage::library::Song> { + let mut count = 0; + let mut bun: Vec<Song> = Vec::new(); + for track in &self.tracks { + //grab "other" tags + let mut tags_: BTreeMap<Tag, String> = BTreeMap::new(); + for (key, val) in &track.tags { + tags_.insert(to_tag(key.clone()), val.clone()); + } + //make the path readable + let loc_ = if track.location.contains("file://localhost/") { + decode(track.location.strip_prefix("file://localhost/").unwrap()) + .unwrap() + .into_owned() + } else { + decode(track.location.as_str()).unwrap().into_owned() + }; + let loc = loc_.as_str(); + if File::open(loc).is_err() && !loc.contains("http") { + count += 1; + dbg!(loc); + continue; + } + + let sug: URI = if track.location.contains("file://localhost/") { + URI::Local(PathBuf::from( + decode(track.location.strip_prefix("file://localhost/").unwrap()) + .unwrap() + .into_owned() + .as_str(), + )) + } else { + URI::Remote(Service::None, decode(&track.location).unwrap().into_owned()) + }; + let dur = match get_duration(Path::new(&loc)) { + Ok(e) => e, + Err(e) => { + dbg!(e); + StdDur::from_secs(0) + } + }; + let play_time_ = StdDur::from_secs(track.plays as u64 * dur.as_secs()); + + let ny: Song = Song { + location: sug, + plays: track.plays, + skips: 0, + favorited: track.favorited, + rating: track.rating, + format: match FileFormat::from_file(PathBuf::from(&loc)) { + Ok(e) => Some(e), + Err(_) => None, + }, + duration: dur, + play_time: play_time_, + last_played: track.last_played, + date_added: track.date_added, + date_modified: track.date_modified, + album_art: match get_art(Path::new(&loc)) { + Ok(e) => e, + Err(_) => Vec::new(), + }, + tags: tags_, + }; + // dbg!(&ny.tags); + bun.push(ny); + } + println!("skipped: {}", count); + bun } } +fn to_tag(string: String) -> Tag { + match string.to_lowercase().as_str() { + "name" => Tag::Title, + "album" => Tag::Album, + "artist" => Tag::Artist, + "album artist" => Tag::AlbumArtist, + "genre" => Tag::Genre, + "comment" => Tag::Comment, + "track" => Tag::Track, + "disc" => Tag::Disk, + _ => Tag::Key(string), + } +} +fn get_duration(file: &Path) -> Result<StdDur, lofty::LoftyError> { + let dur = match Probe::open(file)?.read() { + Ok(tagged_file) => tagged_file.properties().duration(), + + Err(_) => StdDur::from_secs(0), + }; + Ok(dur) +} +fn get_art(file: &Path) -> Result<Vec<AlbumArt>, LoftyError> { + let mut album_art: Vec<AlbumArt> = Vec::new(); + + let blank_tag = &lofty::Tag::new(TagType::Id3v2); + let normal_options = ParseOptions::new().parsing_mode(lofty::ParsingMode::Relaxed); + let tagged_file: lofty::TaggedFile; + + let tag = match Probe::open(file)?.options(normal_options).read() { + Ok(e) => { + tagged_file = e; + match tagged_file.primary_tag() { + Some(primary_tag) => primary_tag, + + None => match tagged_file.first_tag() { + Some(first_tag) => first_tag, + None => blank_tag, + }, + } + } + Err(_) => blank_tag, + }; + let mut img = match utils::find_images(file) { + Ok(e) => e, + Err(_) => Vec::new(), + }; + if !img.is_empty() { + album_art.append(img.as_mut()); + } + + for (i, _art) in tag.pictures().iter().enumerate() { + let new_art = AlbumArt::Embedded(i); + + album_art.push(new_art) + } + + Ok(album_art) +} + #[derive(Debug, Clone, Default)] pub struct XMLSong { pub id: i32, @@ -138,8 +274,7 @@ impl XMLSong { Default::default() } - - fn from_hashmap(map: &mut HashMap<String, String>) -> Result<XMLSong, Error> { + fn from_hashmap(map: &mut HashMap<String, String>) -> Result<XMLSong, LoftyError> { let mut song = XMLSong::new(); //get the path with the first bit chopped off let path_: String = map.get_key_value("Location").unwrap().1.clone(); @@ -188,8 +323,7 @@ impl XMLSong { } } - -pub fn get_folder(file: &PathBuf) -> String { +fn get_folder(file: &PathBuf) -> String { let mut reader = Reader::from_file(file).unwrap(); reader.trim_text(true); //count every event, for fun ig? diff --git a/src/music_storage/library.rs b/src/music_storage/library.rs index d264bdf..99b8bb5 100644 --- a/src/music_storage/library.rs +++ b/src/music_storage/library.rs @@ -275,7 +275,6 @@ impl Album<'_> { self.artist } - pub fn discs(&self) -> &BTreeMap<usize, Vec<&Song>> { &self.discs } diff --git a/src/music_storage/music_collection.rs b/src/music_storage/music_collection.rs index 8abcd71..9b1c7ca 100644 --- a/src/music_storage/music_collection.rs +++ b/src/music_storage/music_collection.rs @@ -1,8 +1,7 @@ -use crate::music_storage::library::{ AlbumArt, Song }; +use crate::music_storage::library::{AlbumArt, Song}; pub trait MusicCollection { fn title(&self) -> &String; fn cover(&self) -> Option<&AlbumArt>; fn tracks(&self) -> Vec<&Song>; } - diff --git a/src/music_storage/playlist.rs b/src/music_storage/playlist.rs index 710e6fd..ce4366d 100644 --- a/src/music_storage/playlist.rs +++ b/src/music_storage/playlist.rs @@ -2,9 +2,12 @@ use chrono::Duration; use walkdir::Error; use crate::music_controller::config::Config; -use std::{path::Path, default, thread::AccessError}; +use std::{default, path::Path, thread::AccessError}; -use super::{library::{AlbumArt, Song, self, Tag}, music_collection::MusicCollection}; +use super::{ + library::{self, AlbumArt, Song, Tag}, + music_collection::MusicCollection, +}; #[derive(Debug, Clone)] pub struct Playlist<'a> { @@ -48,7 +51,7 @@ impl<'a> Playlist<'a> { for track in &self.tracks { index += 1; if song_name == track.tags.get_key_value(&Tag::Title).unwrap().1 { - dbg!("Index gotted! ",index); + dbg!("Index gotted! ", index); return Some(index); } } @@ -75,17 +78,17 @@ impl MusicCollection for Playlist<'_> { } } fn tracks(&self) -> Vec<&Song> { - self.tracks.clone() + self.tracks.clone() } } impl Default for Playlist<'_> { fn default() -> Self { - Playlist { - title: String::default(), - cover: None, - tracks: Vec::default(), - play_count: -1, - play_time: Duration::zero(), + Playlist { + title: String::default(), + cover: None, + tracks: Vec::default(), + play_count: -1, + play_time: Duration::zero(), } } -} \ No newline at end of file +} From 4b55c2eaa8ccc271cd659fc7212482b1e03c9f60 Mon Sep 17 00:00:00 2001 From: MrDulfin <Dulfinaminator@gmail.com> Date: Mon, 11 Dec 2023 00:39:02 -0500 Subject: [PATCH 053/136] fixed tag reading in to_tag() --- src/music_storage/db_reader/xml/reader.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/music_storage/db_reader/xml/reader.rs b/src/music_storage/db_reader/xml/reader.rs index 0b1689c..bc651c5 100644 --- a/src/music_storage/db_reader/xml/reader.rs +++ b/src/music_storage/db_reader/xml/reader.rs @@ -202,8 +202,8 @@ fn to_tag(string: String) -> Tag { "album artist" => Tag::AlbumArtist, "genre" => Tag::Genre, "comment" => Tag::Comment, - "track" => Tag::Track, - "disc" => Tag::Disk, + "track number" => Tag::Track, + "disc number" => Tag::Disk, _ => Tag::Key(string), } } From cab60e1560be223379e3c65e0d8f7d9b41834bc2 Mon Sep 17 00:00:00 2001 From: G2-Games <ke0bhogsg@gmail.com> Date: Mon, 11 Dec 2023 02:27:33 -0600 Subject: [PATCH 054/136] Made foobar reader compliant with new Trait --- src/music_storage/db_reader/foobar/reader.rs | 130 ++++++++++--------- src/music_storage/db_reader/foobar/utils.rs | 15 +++ 2 files changed, 83 insertions(+), 62 deletions(-) create mode 100644 src/music_storage/db_reader/foobar/utils.rs diff --git a/src/music_storage/db_reader/foobar/reader.rs b/src/music_storage/db_reader/foobar/reader.rs index 017d7fc..23872b0 100644 --- a/src/music_storage/db_reader/foobar/reader.rs +++ b/src/music_storage/db_reader/foobar/reader.rs @@ -1,7 +1,10 @@ -use chrono::{DateTime, Utc}; +use std::collections::BTreeMap; use std::{fs::File, io::Read, path::PathBuf, time::Duration}; -use crate::music_storage::db_reader::common::{get_bytes, get_bytes_vec, get_datetime}; +use crate::music_storage::db_reader::common::{get_bytes, get_bytes_vec}; +use crate::music_storage::db_reader::extern_library::ExternalLibrary; +use crate::music_storage::library::{Song, URI}; +use super::utils::meta_offset; const MAGIC: [u8; 16] = [ 0xE1, 0xA0, 0x9C, 0x91, 0xF8, 0x3C, 0x77, 0x42, 0x85, 0x2C, 0x3B, 0xCC, 0x14, 0x01, 0xD3, 0xF2, @@ -9,69 +12,31 @@ const MAGIC: [u8; 16] = [ #[derive(Debug)] pub struct FoobarPlaylist { - path: PathBuf, metadata: Vec<u8>, + songs: Vec<FoobarPlaylistTrack>, } -#[derive(Debug, Default)] -pub struct FoobarPlaylistTrack { - flags: i32, - file_name: String, - subsong_index: i32, - file_size: i64, - file_time: DateTime<Utc>, - duration: Duration, - rpg_album: u32, - rpg_track: u32, - rpk_album: u32, - rpk_track: u32, - entries: Vec<(String, String)>, -} - -impl FoobarPlaylist { - pub fn new(path: &String) -> Self { - FoobarPlaylist { - path: PathBuf::from(path), - metadata: Vec::new(), - } - } - - fn get_meta_offset(&self, offset: usize) -> String { - let mut result_vec = Vec::new(); - - let mut i = offset; - loop { - if self.metadata[i] == 0x00 { - break; - } - - result_vec.push(self.metadata[i]); - i += 1; - } - - String::from_utf8_lossy(&result_vec).into() - } - +impl ExternalLibrary for FoobarPlaylist { /// Reads the entire MusicBee library and returns relevant values /// as a `Vec` of `Song`s - pub fn read(&mut self) -> Result<Vec<FoobarPlaylistTrack>, Box<dyn std::error::Error>> { - let mut f = File::open(&self.path).unwrap(); + fn from_file(file: &PathBuf) -> Self { + let mut f = File::open(file).unwrap(); let mut buffer = Vec::new(); let mut retrieved_songs: Vec<FoobarPlaylistTrack> = Vec::new(); // Read the whole file - f.read_to_end(&mut buffer)?; + f.read_to_end(&mut buffer).unwrap(); let mut buf_iter = buffer.into_iter(); // Parse the header let magic = get_bytes::<16>(&mut buf_iter); if magic != MAGIC { - return Err("Magic bytes mismatch!".into()); + panic!("Magic bytes mismatch!"); } let meta_size = i32::from_le_bytes(get_bytes(&mut buf_iter)) as usize; - self.metadata = get_bytes_vec(&mut buf_iter, meta_size); + let metadata = get_bytes_vec(&mut buf_iter, meta_size); let track_count = i32::from_le_bytes(get_bytes(&mut buf_iter)); // Read all the track fields @@ -82,7 +47,7 @@ impl FoobarPlaylist { let has_padding = (0x04 & flags) != 0; let file_name_offset = i32::from_le_bytes(get_bytes(&mut buf_iter)) as usize; - let file_name = self.get_meta_offset(file_name_offset); + let file_name = meta_offset(metadata, file_name_offset); let subsong_index = i32::from_le_bytes(get_bytes(&mut buf_iter)); @@ -98,17 +63,18 @@ impl FoobarPlaylist { let file_size = i64::from_le_bytes(get_bytes(&mut buf_iter)); - let file_time = get_datetime(&mut buf_iter, false); + // TODO: Figure out how to make this work properly + let file_time = i64::from_le_bytes(get_bytes(&mut buf_iter)); let duration = Duration::from_nanos(u64::from_le_bytes(get_bytes(&mut buf_iter)) / 100); - let rpg_album = u32::from_le_bytes(get_bytes(&mut buf_iter)); + let rpg_album = f32::from_le_bytes(get_bytes(&mut buf_iter)); - let rpg_track = u32::from_le_bytes(get_bytes(&mut buf_iter)); + let rpg_track = f32::from_le_bytes(get_bytes(&mut buf_iter)); - let rpk_album = u32::from_le_bytes(get_bytes(&mut buf_iter)); + let rpk_album = f32::from_le_bytes(get_bytes(&mut buf_iter)); - let rpk_track = u32::from_le_bytes(get_bytes(&mut buf_iter)); + let rpk_track = f32::from_le_bytes(get_bytes(&mut buf_iter)); get_bytes::<4>(&mut buf_iter); @@ -121,8 +87,7 @@ impl FoobarPlaylist { for _ in 0..primary_count { println!("{}", i32::from_le_bytes(get_bytes(&mut buf_iter))); - let key = - self.get_meta_offset(i32::from_le_bytes(get_bytes(&mut buf_iter)) as usize); + let key = meta_offset(metadata, i32::from_le_bytes(get_bytes(&mut buf_iter)) as usize); entries.push((key, String::new())); } @@ -135,18 +100,15 @@ impl FoobarPlaylist { for i in 0..primary_count { println!("primkey {i}"); - let value = - self.get_meta_offset(i32::from_le_bytes(get_bytes(&mut buf_iter)) as usize); + let value = meta_offset(metadata, i32::from_le_bytes(get_bytes(&mut buf_iter)) as usize); entries[i as usize].1 = value; } // Get secondary Keys for _ in 0..secondary_count { - let key = - self.get_meta_offset(i32::from_le_bytes(get_bytes(&mut buf_iter)) as usize); - let value = - self.get_meta_offset(i32::from_le_bytes(get_bytes(&mut buf_iter)) as usize); + let key = meta_offset(metadata, i32::from_le_bytes(get_bytes(&mut buf_iter)) as usize); + let value = meta_offset(metadata, i32::from_le_bytes(get_bytes(&mut buf_iter)) as usize); entries.push((key, value)); } @@ -171,6 +133,50 @@ impl FoobarPlaylist { retrieved_songs.push(track); } - Ok(retrieved_songs) + Self { + songs: retrieved_songs, + metadata, + } + } + + fn to_songs(&self) -> Vec<Song> { + self.songs.iter().map(|song| song.find_song()).collect() + } +} + +#[derive(Debug, Default)] +pub struct FoobarPlaylistTrack { + flags: i32, + file_name: String, + subsong_index: i32, + file_size: i64, + file_time: i64, + duration: Duration, + rpg_album: f32, + rpg_track: f32, + rpk_album: f32, + rpk_track: f32, + entries: Vec<(String, String)>, +} + +impl FoobarPlaylistTrack { + fn find_song(&self) -> Song { + let location = URI::Local(self.file_name.into()); + + Song { + location, + plays: 0, + skips: 0, + favorited: false, + rating: None, + format: None, + duration: self.duration, + play_time: Duration::from_secs(0), + last_played: None, + date_added: None, + date_modified: None, + album_art: Vec::new(), + tags: BTreeMap::new(), + } } } diff --git a/src/music_storage/db_reader/foobar/utils.rs b/src/music_storage/db_reader/foobar/utils.rs new file mode 100644 index 0000000..62ee270 --- /dev/null +++ b/src/music_storage/db_reader/foobar/utils.rs @@ -0,0 +1,15 @@ +pub fn meta_offset(metadata: Vec<u8>, offset: usize) -> String { + let mut result_vec = Vec::new(); + + let mut i = offset; + loop { + if metadata[i] == 0x00 { + break; + } + + result_vec.push(metadata[i]); + i += 1; + } + + String::from_utf8_lossy(&result_vec).into() +} From 5000d24a77b841b764d599e1c5d8fdb1175039f3 Mon Sep 17 00:00:00 2001 From: G2-Games <ke0bhogsg@gmail.com> Date: Mon, 11 Dec 2023 02:30:03 -0600 Subject: [PATCH 055/136] Fixed errors in foobar reader --- src/lib.rs | 1 + src/music_storage/db_reader/foobar/reader.rs | 6 +++--- src/music_storage/db_reader/foobar/utils.rs | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 9981527..c20df16 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ pub mod music_storage { pub mod db_reader { pub mod foobar { pub mod reader; + pub mod utils; } pub mod musicbee { pub mod reader; diff --git a/src/music_storage/db_reader/foobar/reader.rs b/src/music_storage/db_reader/foobar/reader.rs index 23872b0..165c08b 100644 --- a/src/music_storage/db_reader/foobar/reader.rs +++ b/src/music_storage/db_reader/foobar/reader.rs @@ -36,7 +36,7 @@ impl ExternalLibrary for FoobarPlaylist { } let meta_size = i32::from_le_bytes(get_bytes(&mut buf_iter)) as usize; - let metadata = get_bytes_vec(&mut buf_iter, meta_size); + let metadata = &get_bytes_vec(&mut buf_iter, meta_size); let track_count = i32::from_le_bytes(get_bytes(&mut buf_iter)); // Read all the track fields @@ -135,7 +135,7 @@ impl ExternalLibrary for FoobarPlaylist { Self { songs: retrieved_songs, - metadata, + metadata: metadata.clone(), } } @@ -161,7 +161,7 @@ pub struct FoobarPlaylistTrack { impl FoobarPlaylistTrack { fn find_song(&self) -> Song { - let location = URI::Local(self.file_name.into()); + let location = URI::Local(self.file_name.clone().into()); Song { location, diff --git a/src/music_storage/db_reader/foobar/utils.rs b/src/music_storage/db_reader/foobar/utils.rs index 62ee270..4b02f02 100644 --- a/src/music_storage/db_reader/foobar/utils.rs +++ b/src/music_storage/db_reader/foobar/utils.rs @@ -1,4 +1,4 @@ -pub fn meta_offset(metadata: Vec<u8>, offset: usize) -> String { +pub fn meta_offset(metadata: &Vec<u8>, offset: usize) -> String { let mut result_vec = Vec::new(); let mut i = offset; From 045cea90bf83da4bc96896790f5c2fb8aae94cbb Mon Sep 17 00:00:00 2001 From: G2-Games <ke0bhogsg@gmail.com> Date: Mon, 11 Dec 2023 02:42:13 -0600 Subject: [PATCH 056/136] Applied clippy suggestions --- src/music_player.rs | 18 ++++++++---------- src/music_storage/db_reader/common.rs | 12 +++++------- src/music_storage/db_reader/extern_library.rs | 4 ++-- src/music_storage/db_reader/foobar/reader.rs | 4 ++-- src/music_storage/db_reader/foobar/utils.rs | 2 +- src/music_storage/db_reader/musicbee/reader.rs | 2 +- src/music_storage/db_reader/xml/reader.rs | 2 +- 7 files changed, 20 insertions(+), 24 deletions(-) diff --git a/src/music_player.rs b/src/music_player.rs index 4c0ee19..aed3eaf 100644 --- a/src/music_player.rs +++ b/src/music_player.rs @@ -179,7 +179,7 @@ impl Player { match msg.view() { gst::MessageView::Eos(_) => {} gst::MessageView::StreamStart(_) => println!("Stream start"), - gst::MessageView::Error(e) => { + gst::MessageView::Error(_) => { playbin_bus_ctrl .write() .unwrap() @@ -201,7 +201,7 @@ impl Player { .unwrap() .set_state(gst::State::Paused) .unwrap(); - } else if *paused_bus_ctrl.read().unwrap() == false { + } else if !(*paused_bus_ctrl.read().unwrap()) { *buffer_bus_ctrl.write().unwrap() = None; playbin_bus_ctrl .write() @@ -377,19 +377,17 @@ impl Player { /// Seek absolutely pub fn seek_to(&mut self, target_pos: Duration) -> Result<(), Box<dyn Error>> { - let start; - if self.start.read().unwrap().is_none() { + let start = if self.start.read().unwrap().is_none() { return Err("Failed to seek: No START time".into()); } else { - start = self.start.read().unwrap().unwrap(); - } + self.start.read().unwrap().unwrap() + }; - let end; - if self.end.read().unwrap().is_none() { + let end = if self.end.read().unwrap().is_none() { return Err("Failed to seek: No END time".into()); } else { - end = self.end.read().unwrap().unwrap(); - } + self.end.read().unwrap().unwrap() + }; let adjusted_target = target_pos + start; let clamped_target = adjusted_target.clamp(start, end); diff --git a/src/music_storage/db_reader/common.rs b/src/music_storage/db_reader/common.rs index 0849081..368d86e 100644 --- a/src/music_storage/db_reader/common.rs +++ b/src/music_storage/db_reader/common.rs @@ -1,15 +1,13 @@ -use std::path::PathBuf; - use chrono::{DateTime, TimeZone, Utc}; pub fn get_bytes<const S: usize>(iterator: &mut std::vec::IntoIter<u8>) -> [u8; S] { let mut bytes = [0; S]; - for i in 0..S { - bytes[i] = iterator.next().unwrap(); + for byte in bytes.iter_mut().take(S) { + *byte = iterator.next().unwrap(); } - return bytes; + bytes } pub fn get_bytes_vec(iterator: &mut std::vec::IntoIter<u8>, number: usize) -> Vec<u8> { @@ -19,7 +17,7 @@ pub fn get_bytes_vec(iterator: &mut std::vec::IntoIter<u8>, number: usize) -> Ve bytes.push(iterator.next().unwrap()); } - return bytes; + bytes } /// Converts the windows DateTime into Chrono DateTime @@ -28,7 +26,7 @@ pub fn get_datetime(iterator: &mut std::vec::IntoIter<u8>, topbyte: bool) -> Dat if topbyte { // Zero the topmost byte - datetime_i64 = datetime_i64 & 0x00FFFFFFFFFFFFFFF; + datetime_i64 &= 0x00FFFFFFFFFFFFFFF; } if datetime_i64 <= 0 { diff --git a/src/music_storage/db_reader/extern_library.rs b/src/music_storage/db_reader/extern_library.rs index dc5c306..4312c4a 100644 --- a/src/music_storage/db_reader/extern_library.rs +++ b/src/music_storage/db_reader/extern_library.rs @@ -1,9 +1,9 @@ -use std::path::PathBuf; +use std::path::Path; use crate::music_storage::library::Song; pub trait ExternalLibrary { - fn from_file(file: &PathBuf) -> Self; + fn from_file(file: &Path) -> Self; fn write(&self) { unimplemented!(); } diff --git a/src/music_storage/db_reader/foobar/reader.rs b/src/music_storage/db_reader/foobar/reader.rs index 165c08b..218d6c0 100644 --- a/src/music_storage/db_reader/foobar/reader.rs +++ b/src/music_storage/db_reader/foobar/reader.rs @@ -1,5 +1,5 @@ use std::collections::BTreeMap; -use std::{fs::File, io::Read, path::PathBuf, time::Duration}; +use std::{fs::File, io::Read, path::Path, time::Duration}; use crate::music_storage::db_reader::common::{get_bytes, get_bytes_vec}; use crate::music_storage::db_reader::extern_library::ExternalLibrary; @@ -19,7 +19,7 @@ pub struct FoobarPlaylist { impl ExternalLibrary for FoobarPlaylist { /// Reads the entire MusicBee library and returns relevant values /// as a `Vec` of `Song`s - fn from_file(file: &PathBuf) -> Self { + fn from_file(file: &Path) -> Self { let mut f = File::open(file).unwrap(); let mut buffer = Vec::new(); let mut retrieved_songs: Vec<FoobarPlaylistTrack> = Vec::new(); diff --git a/src/music_storage/db_reader/foobar/utils.rs b/src/music_storage/db_reader/foobar/utils.rs index 4b02f02..278aa1a 100644 --- a/src/music_storage/db_reader/foobar/utils.rs +++ b/src/music_storage/db_reader/foobar/utils.rs @@ -1,4 +1,4 @@ -pub fn meta_offset(metadata: &Vec<u8>, offset: usize) -> String { +pub fn meta_offset(metadata: &[u8], offset: usize) -> String { let mut result_vec = Vec::new(); let mut i = offset; diff --git a/src/music_storage/db_reader/musicbee/reader.rs b/src/music_storage/db_reader/musicbee/reader.rs index 0a5cac6..9811862 100644 --- a/src/music_storage/db_reader/musicbee/reader.rs +++ b/src/music_storage/db_reader/musicbee/reader.rs @@ -29,7 +29,7 @@ impl MusicBeeDatabase { // Get the song count from the first 4 bytes // and then right shift it by 8 for some reason let mut database_song_count = i32::from_le_bytes(get_bytes(&mut buf_iter)); - database_song_count = database_song_count >> 8; + database_song_count >>= 8; let mut song_count = 0; loop { diff --git a/src/music_storage/db_reader/xml/reader.rs b/src/music_storage/db_reader/xml/reader.rs index bc651c5..fe6bc64 100644 --- a/src/music_storage/db_reader/xml/reader.rs +++ b/src/music_storage/db_reader/xml/reader.rs @@ -28,7 +28,7 @@ impl XmlLibrary { } } impl ExternalLibrary for XmlLibrary { - fn from_file(file: &PathBuf) -> Self { + fn from_file(file: &Path) -> Self { let mut reader = Reader::from_file(file).unwrap(); reader.trim_text(true); //count every event, for fun ig? From 7e041bddd0103e1278ffb603cac70ba9bfd34e25 Mon Sep 17 00:00:00 2001 From: MrDulfin <Dulfinaminator@gmail.com> Date: Mon, 11 Dec 2023 13:16:12 -0500 Subject: [PATCH 057/136] removed some warnings --- src/music_storage/db_reader/xml/reader.rs | 64 +++++++++++------------ 1 file changed, 31 insertions(+), 33 deletions(-) diff --git a/src/music_storage/db_reader/xml/reader.rs b/src/music_storage/db_reader/xml/reader.rs index bc651c5..70e2b77 100644 --- a/src/music_storage/db_reader/xml/reader.rs +++ b/src/music_storage/db_reader/xml/reader.rs @@ -4,7 +4,6 @@ use quick_xml::events::Event; use quick_xml::reader::Reader; use std::collections::{BTreeMap, HashMap}; use std::fs::File; -use std::io::Error; use std::path::{Path, PathBuf}; use std::str::FromStr; use std::time::Duration as StdDur; @@ -107,7 +106,6 @@ impl ExternalLibrary for XmlLibrary { panic!("Key not selected?!") } } - _ => panic!("WHAT DID YOU JUST DO?!🐰🐰🐰🐰"), } } Err(e) => panic!("Error at position {}: {:?}", reader.buffer_position(), e), @@ -323,34 +321,34 @@ impl XMLSong { } } -fn get_folder(file: &PathBuf) -> String { - let mut reader = Reader::from_file(file).unwrap(); - reader.trim_text(true); - //count every event, for fun ig? - let mut count = 0; - let mut buf = Vec::new(); - let mut folder = String::new(); - loop { - match reader.read_event_into(&mut buf) { - Ok(Event::Start(_)) => { - count += 1; - } - Ok(Event::Text(e)) => { - if count == 10 { - folder = String::from( - e.unescape() - .unwrap() - .to_string() - .strip_prefix("file://localhost/") - .unwrap(), - ); - return folder; - } - } - Err(_e) => { - panic!("oh no! something happened in the public function `get_reader_from_xml()!`") - } - _ => (), - } - } -} +// fn get_folder(file: &PathBuf) -> String { +// let mut reader = Reader::from_file(file).unwrap(); +// reader.trim_text(true); +// //count every event, for fun ig? +// let mut count = 0; +// let mut buf = Vec::new(); +// let mut folder = String::new(); +// loop { +// match reader.read_event_into(&mut buf) { +// Ok(Event::Start(_)) => { +// count += 1; +// } +// Ok(Event::Text(e)) => { +// if count == 10 { +// folder = String::from( +// e.unescape() +// .unwrap() +// .to_string() +// .strip_prefix("file://localhost/") +// .unwrap(), +// ); +// return folder; +// } +// } +// Err(_e) => { +// panic!("oh no! something happened in the public function `get_reader_from_xml()!`") +// } +// _ => (), +// } +// } +// } From d8be0535c26cf0bb88d1f39875ea4e5f0fc7c793 Mon Sep 17 00:00:00 2001 From: G2-Games <ke0bhogsg@gmail.com> Date: Thu, 14 Dec 2023 16:40:04 -0600 Subject: [PATCH 058/136] Changed the method to lock the `playbin` instance --- src/music_player.rs | 45 +++++++++++++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/src/music_player.rs b/src/music_player.rs index aed3eaf..20f9977 100644 --- a/src/music_player.rs +++ b/src/music_player.rs @@ -3,7 +3,7 @@ use crate::music_storage::library::{Tag, URI}; use crossbeam_channel::bounded; use std::error::Error; -use std::sync::{Arc, RwLock}; +use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}; // GStreamer things use glib::FlagsClass; @@ -292,6 +292,30 @@ impl Player { } } + /// Gets a mutable reference to the playbin element + fn playbin_mut( + &mut self, + ) -> Result<RwLockWriteGuard<gst::Element>, std::sync::PoisonError<RwLockWriteGuard<'_, Element>>> + { + let element = match self.playbin.write() { + Ok(element) => element, + Err(err) => return Err(err), + }; + Ok(element) + } + + /// Gets a read-only reference to the playbin element + fn playbin( + &self, + ) -> Result<RwLockReadGuard<gst::Element>, std::sync::PoisonError<RwLockReadGuard<'_, Element>>> + { + let element = match self.playbin.read() { + Ok(element) => element, + Err(err) => return Err(err), + }; + Ok(element) + } + /// Set the playback volume, accepts a float from 0 to 1 pub fn set_volume(&mut self, volume: f64) { self.volume = volume.clamp(0.0, 1.0); @@ -301,7 +325,7 @@ impl Player { /// Set volume of the internal playbin player, can be /// used to bypass the main volume control for seeking fn set_gstreamer_volume(&mut self, volume: f64) { - self.playbin.write().unwrap().set_property("volume", volume) + self.playbin_mut().unwrap().set_property("volume", volume) } /// Returns the current volume level, a float from 0 to 1 @@ -310,7 +334,7 @@ impl Player { } fn set_state(&mut self, state: gst::State) -> Result<(), gst::StateChangeError> { - self.playbin.write().unwrap().set_state(state)?; + self.playbin_mut().unwrap().set_state(state)?; Ok(()) } @@ -338,7 +362,7 @@ impl Player { /// Check if playback is paused pub fn is_paused(&mut self) -> bool { - self.playbin.read().unwrap().current_state() == gst::State::Paused + self.playbin().unwrap().current_state() == gst::State::Paused } /// Get the current playback position of the player @@ -356,8 +380,7 @@ impl Player { } pub fn raw_duration(&self) -> Option<Duration> { - self.playbin - .read() + self.playbin() .unwrap() .query_duration::<ClockTime>() .map(|pos| Duration::nanoseconds(pos.nseconds() as i64)) @@ -396,8 +419,7 @@ impl Player { ClockTime::from_useconds(clamped_target.num_microseconds().unwrap() as u64); self.set_gstreamer_volume(0.0); - self.playbin - .write() + self.playbin_mut() .unwrap() .seek_simple(gst::SeekFlags::FLUSH, seek_pos_clock)?; self.set_gstreamer_volume(self.volume); @@ -407,13 +429,13 @@ impl Player { /// Get the current state of the playback pub fn state(&mut self) -> PlayerState { match *self.buffer.read().unwrap() { - None => self.playbin.read().unwrap().current_state().into(), + None => self.playbin().unwrap().current_state().into(), Some(value) => PlayerState::Buffering(value), } } pub fn property(&self, property: &str) -> glib::Value { - self.playbin.read().unwrap().property_value(property) + self.playbin().unwrap().property_value(property) } /// Stop the playback entirely @@ -432,8 +454,7 @@ impl Player { impl Drop for Player { /// Cleans up `GStreamer` pipeline when `Backend` is dropped. fn drop(&mut self) { - self.playbin - .write() + self.playbin_mut() .unwrap() .set_state(gst::State::Null) .expect("Unable to set the pipeline to the `Null` state"); From eb9b2bf2304b209144185a236ee0ea84df889eb9 Mon Sep 17 00:00:00 2001 From: MrDulfin <74484768+MrDulfin@users.noreply.github.com> Date: Thu, 14 Dec 2023 18:04:55 -0500 Subject: [PATCH 059/136] Create README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..751669c --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# dango-core + +This is the backend crate for the [Dango Music Player](https://github.com/Dangoware/dango-music-player) From 42ff88578e9a7d507f0af7faa2b02c0207d016c1 Mon Sep 17 00:00:00 2001 From: MrDulfin <74484768+MrDulfin@users.noreply.github.com> Date: Thu, 14 Dec 2023 18:17:45 -0500 Subject: [PATCH 060/136] Create LICENSE --- LICENSE | 661 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 661 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0ad25db --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +<https://www.gnu.org/licenses/>. From ef78c36982adc6c747a5e08c47183b02eb0d806d Mon Sep 17 00:00:00 2001 From: MrDulfin <74484768+MrDulfin@users.noreply.github.com> Date: Thu, 14 Dec 2023 18:19:03 -0500 Subject: [PATCH 061/136] added gitignore --- .gitignore | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b4ff1a9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +target/ + +music_database* +*.db3* +config.toml +Cargo.lock +*.kate-swp* From 72da619f8e44b07ad1384226e0caafba76ea761a Mon Sep 17 00:00:00 2001 From: MrDulfin <Dulfinaminator@gmail.com> Date: Mon, 25 Dec 2023 14:22:57 -0500 Subject: [PATCH 062/136] added `to_m3u8()` and placeholders for Playlist --- .gitignore | 2 + Cargo.toml | 3 +- src/music_storage/db_reader/xml/reader.rs | 3 ++ src/music_storage/playlist.rs | 59 ++++++++++++++++++++--- 4 files changed, 60 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index b4ff1a9..62c5a2f 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ music_database* config.toml Cargo.lock *.kate-swp* +*.m3u +*.m3u8 \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 1673cfd..18d5519 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,4 +31,5 @@ crossbeam-channel = "0.5.8" crossbeam = "0.8.2" quick-xml = "0.31.0" leb128 = "0.2.5" -urlencoding = "2.1.3" \ No newline at end of file +urlencoding = "2.1.3" +m3u8-rs = "5.0.5" diff --git a/src/music_storage/db_reader/xml/reader.rs b/src/music_storage/db_reader/xml/reader.rs index faf5f0c..50beeac 100644 --- a/src/music_storage/db_reader/xml/reader.rs +++ b/src/music_storage/db_reader/xml/reader.rs @@ -25,6 +25,9 @@ impl XmlLibrary { fn new() -> Self { Default::default() } + pub fn tracks(self) -> Vec<XMLSong> { + self.tracks + } } impl ExternalLibrary for XmlLibrary { fn from_file(file: &Path) -> Self { diff --git a/src/music_storage/playlist.rs b/src/music_storage/playlist.rs index ce4366d..1c29bb6 100644 --- a/src/music_storage/playlist.rs +++ b/src/music_storage/playlist.rs @@ -2,12 +2,19 @@ use chrono::Duration; use walkdir::Error; use crate::music_controller::config::Config; -use std::{default, path::Path, thread::AccessError}; - use super::{ library::{self, AlbumArt, Song, Tag}, music_collection::MusicCollection, + db_reader::extern_library::ExternalLibrary }; +use crate::music_storage::db_reader::xml::reader::{XmlLibrary}; + +use std::{default, path::Path, path::PathBuf, thread::AccessError}; +use std::io::Read; + +use m3u8_rs::{MediaPlaylist, MediaPlaylistType, MediaSegment}; +// use nom::IResult; + #[derive(Debug, Clone)] pub struct Playlist<'a> { @@ -47,7 +54,7 @@ impl<'a> Playlist<'a> { } pub fn get_index(&self, song_name: &str) -> Option<usize> { let mut index = 0; - if self.contains(&Tag::Title, song_name) { + if self.contains_value(&Tag::Title, song_name) { for track in &self.tracks { index += 1; if song_name == track.tags.get_key_value(&Tag::Title).unwrap().1 { @@ -58,14 +65,41 @@ impl<'a> Playlist<'a> { } None } - pub fn contains(&self, tag: &Tag, title: &str) -> bool { + pub fn contains_value(&self, tag: &Tag, value: &str) -> bool { for track in &self.tracks { - if title == track.tags.get_key_value(tag).unwrap().1 { + if value == track.tags.get_key_value(tag).unwrap().1 { return true; } } false } + pub fn to_m3u8(&mut self) { + let seg = &self.tracks.iter().map({ + |track| + MediaSegment { + uri: track.location.to_string().into(), + duration: track.duration.as_millis() as f32, + title: Some(track.tags.get_key_value(&Tag::Title).unwrap().1.to_string().into()), + ..Default::default() + } + }).collect::<Vec<MediaSegment>>(); + + let m3u8 = MediaPlaylist { + version: Some(6), + target_duration: 3.0, + media_sequence: 338559, + discontinuity_sequence: 1234, + end_list: true, + playlist_type: Some(MediaPlaylistType::Vod), + segments: seg.clone(), + ..Default::default() + }; + let mut file = std::fs::OpenOptions::new().read(true).create(true).write(true).open("F:\\Dango Music Player\\playlist.m3u8").unwrap(); + m3u8.write_to(&mut file).unwrap(); + } + pub fn from_file(file: std::fs::File) -> Playlist<'a> { + todo!() + } } impl MusicCollection for Playlist<'_> { fn title(&self) -> &String { @@ -87,8 +121,21 @@ impl Default for Playlist<'_> { title: String::default(), cover: None, tracks: Vec::default(), - play_count: -1, + play_count: 0, play_time: Duration::zero(), } } } + +#[test] +fn list_to_m3u8() { + let lib = XmlLibrary::from_file(Path::new("F:\\Music\\Mp3\\Music Main\\iTunes Music Library.xml")); + let mut a = Playlist::new(); + let c = lib.to_songs(); + let mut b = c.iter().map({ + |song| + song + }).collect::<Vec<&Song>>(); + a.tracks.append(&mut b); + a.to_m3u8() +} \ No newline at end of file From c78f77165e5f4b2f2d3b199c0193b4027a5cfdaa Mon Sep 17 00:00:00 2001 From: MrDulfin <Dulfinaminator@gmail.com> Date: Tue, 9 Jan 2024 13:38:54 -0500 Subject: [PATCH 063/136] Cargo formatted and made `query_uri()` public --- src/music_storage/db_reader/foobar/reader.rs | 22 ++++++-- src/music_storage/library.rs | 2 +- src/music_storage/playlist.rs | 56 ++++++++++++-------- 3 files changed, 53 insertions(+), 27 deletions(-) diff --git a/src/music_storage/db_reader/foobar/reader.rs b/src/music_storage/db_reader/foobar/reader.rs index 218d6c0..3cb417d 100644 --- a/src/music_storage/db_reader/foobar/reader.rs +++ b/src/music_storage/db_reader/foobar/reader.rs @@ -1,10 +1,10 @@ use std::collections::BTreeMap; use std::{fs::File, io::Read, path::Path, time::Duration}; +use super::utils::meta_offset; use crate::music_storage::db_reader::common::{get_bytes, get_bytes_vec}; use crate::music_storage::db_reader::extern_library::ExternalLibrary; use crate::music_storage::library::{Song, URI}; -use super::utils::meta_offset; const MAGIC: [u8; 16] = [ 0xE1, 0xA0, 0x9C, 0x91, 0xF8, 0x3C, 0x77, 0x42, 0x85, 0x2C, 0x3B, 0xCC, 0x14, 0x01, 0xD3, 0xF2, @@ -87,7 +87,10 @@ impl ExternalLibrary for FoobarPlaylist { for _ in 0..primary_count { println!("{}", i32::from_le_bytes(get_bytes(&mut buf_iter))); - let key = meta_offset(metadata, i32::from_le_bytes(get_bytes(&mut buf_iter)) as usize); + let key = meta_offset( + metadata, + i32::from_le_bytes(get_bytes(&mut buf_iter)) as usize, + ); entries.push((key, String::new())); } @@ -100,15 +103,24 @@ impl ExternalLibrary for FoobarPlaylist { for i in 0..primary_count { println!("primkey {i}"); - let value = meta_offset(metadata, i32::from_le_bytes(get_bytes(&mut buf_iter)) as usize); + let value = meta_offset( + metadata, + i32::from_le_bytes(get_bytes(&mut buf_iter)) as usize, + ); entries[i as usize].1 = value; } // Get secondary Keys for _ in 0..secondary_count { - let key = meta_offset(metadata, i32::from_le_bytes(get_bytes(&mut buf_iter)) as usize); - let value = meta_offset(metadata, i32::from_le_bytes(get_bytes(&mut buf_iter)) as usize); + let key = meta_offset( + metadata, + i32::from_le_bytes(get_bytes(&mut buf_iter)) as usize, + ); + let value = meta_offset( + metadata, + i32::from_le_bytes(get_bytes(&mut buf_iter)) as usize, + ); entries.push((key, value)); } diff --git a/src/music_storage/library.rs b/src/music_storage/library.rs index 99b8bb5..c3458cf 100644 --- a/src/music_storage/library.rs +++ b/src/music_storage/library.rs @@ -368,7 +368,7 @@ 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)> { + pub fn query_uri(&self, path: &URI) -> Option<(&Song, usize)> { let result = self .library .par_iter() diff --git a/src/music_storage/playlist.rs b/src/music_storage/playlist.rs index 1c29bb6..6b3dc96 100644 --- a/src/music_storage/playlist.rs +++ b/src/music_storage/playlist.rs @@ -1,21 +1,20 @@ use chrono::Duration; use walkdir::Error; -use crate::music_controller::config::Config; use super::{ + db_reader::extern_library::ExternalLibrary, library::{self, AlbumArt, Song, Tag}, music_collection::MusicCollection, - db_reader::extern_library::ExternalLibrary }; -use crate::music_storage::db_reader::xml::reader::{XmlLibrary}; +use crate::music_controller::config::Config; +use crate::music_storage::db_reader::xml::reader::XmlLibrary; -use std::{default, path::Path, path::PathBuf, thread::AccessError}; use std::io::Read; +use std::{default, path::Path, path::PathBuf, thread::AccessError}; use m3u8_rs::{MediaPlaylist, MediaPlaylistType, MediaSegment}; // use nom::IResult; - #[derive(Debug, Clone)] pub struct Playlist<'a> { title: String, @@ -74,15 +73,26 @@ impl<'a> Playlist<'a> { false } pub fn to_m3u8(&mut self) { - let seg = &self.tracks.iter().map({ - |track| - MediaSegment { - uri: track.location.to_string().into(), - duration: track.duration.as_millis() as f32, - title: Some(track.tags.get_key_value(&Tag::Title).unwrap().1.to_string().into()), - ..Default::default() - } - }).collect::<Vec<MediaSegment>>(); + let seg = &self + .tracks + .iter() + .map({ + |track| MediaSegment { + uri: track.location.to_string().into(), + duration: track.duration.as_millis() as f32, + title: Some( + track + .tags + .get_key_value(&Tag::Title) + .unwrap() + .1 + .to_string() + .into(), + ), + ..Default::default() + } + }) + .collect::<Vec<MediaSegment>>(); let m3u8 = MediaPlaylist { version: Some(6), @@ -94,7 +104,12 @@ impl<'a> Playlist<'a> { segments: seg.clone(), ..Default::default() }; - let mut file = std::fs::OpenOptions::new().read(true).create(true).write(true).open("F:\\Dango Music Player\\playlist.m3u8").unwrap(); + let mut file = std::fs::OpenOptions::new() + .read(true) + .create(true) + .write(true) + .open("F:\\Dango Music Player\\playlist.m3u8") + .unwrap(); m3u8.write_to(&mut file).unwrap(); } pub fn from_file(file: std::fs::File) -> Playlist<'a> { @@ -129,13 +144,12 @@ impl Default for Playlist<'_> { #[test] fn list_to_m3u8() { - let lib = XmlLibrary::from_file(Path::new("F:\\Music\\Mp3\\Music Main\\iTunes Music Library.xml")); + let lib = XmlLibrary::from_file(Path::new( + "F:\\Music\\Mp3\\Music Main\\iTunes Music Library.xml", + )); let mut a = Playlist::new(); let c = lib.to_songs(); - let mut b = c.iter().map({ - |song| - song - }).collect::<Vec<&Song>>(); + let mut b = c.iter().map({ |song| song }).collect::<Vec<&Song>>(); a.tracks.append(&mut b); a.to_m3u8() -} \ No newline at end of file +} From 4fbda80397c1e01e9b5f7b348b9d0974d1ada78f Mon Sep 17 00:00:00 2001 From: MrDulfin <Dulfinaminator@gmail.com> Date: Fri, 12 Jan 2024 23:32:25 -0500 Subject: [PATCH 064/136] test --- src/music_storage/library.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/music_storage/library.rs b/src/music_storage/library.rs index c3458cf..706a05d 100644 --- a/src/music_storage/library.rs +++ b/src/music_storage/library.rs @@ -9,7 +9,7 @@ use std::error::Error; use std::ops::ControlFlow::{Break, Continue}; // Files -use file_format::{FileFormat, Kind}; +use file_format::{ FileFormat, Kind }; use glib::filename_to_uri; use lofty::{AudioFile, ItemKey, ItemValue, ParseOptions, Probe, TagType, TaggedFileExt}; use rcue::parser::parse_from_file; From 278f37dd6a7bd7d680c0abb8a19a7a8145e8b04d Mon Sep 17 00:00:00 2001 From: G2-Games <ke0bhogsg@gmail.com> Date: Fri, 12 Jan 2024 22:46:33 -0600 Subject: [PATCH 065/136] Made some changes to error handling in the `Player` and other minor changes --- Cargo.toml | 1 + src/music_controller/config.rs | 4 +-- src/music_player.rs | 45 +++++++++++++++++++++------------- src/music_storage/library.rs | 4 +-- 4 files changed, 33 insertions(+), 21 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 18d5519..bb0746c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,3 +33,4 @@ quick-xml = "0.31.0" leb128 = "0.2.5" urlencoding = "2.1.3" m3u8-rs = "5.0.5" +thiserror = "1.0.56" diff --git a/src/music_controller/config.rs b/src/music_controller/config.rs index 2014de7..a9523d3 100644 --- a/src/music_controller/config.rs +++ b/src/music_controller/config.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, PartialEq, Eq)] pub struct Config { - pub db_path: Box<PathBuf>, + pub db_path: PathBuf, } impl Default for Config { @@ -14,7 +14,7 @@ impl Default for Config { let path = PathBuf::from("./music_database"); Config { - db_path: Box::new(path), + db_path: path, } } } diff --git a/src/music_player.rs b/src/music_player.rs index 20f9977..85563e9 100644 --- a/src/music_player.rs +++ b/src/music_player.rs @@ -1,6 +1,6 @@ // Crate things //use crate::music_controller::config::Config; -use crate::music_storage::library::{Tag, URI}; +use crate::music_storage::library::URI; use crossbeam_channel::bounded; use std::error::Error; use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}; @@ -11,8 +11,9 @@ use gst::{ClockTime, Element}; use gstreamer as gst; use gstreamer::prelude::*; -// Time things +// Extra things use chrono::Duration; +use thiserror::Error; #[derive(Debug)] pub enum PlayerCmd { @@ -59,6 +60,22 @@ impl TryInto<gst::State> for PlayerState { type Error = Box<dyn Error>; } +#[derive(Error, Debug)] +pub enum PlayerError { + #[error("player initialization failed")] + Init(#[from] glib::Error), + #[error("element factory failed to create playbin3")] + Factory(#[from] glib::BoolError), + #[error("could not change playback state")] + StateChange(#[from] gst::StateChangeError), + #[error("failed to build gstreamer item")] + Build, + #[error("poison error")] + Poison, + #[error("general player error")] + General, +} + /// An instance of a music player with a GStreamer backend pub struct Player { source: Option<URI>, @@ -73,22 +90,16 @@ pub struct Player { paused: Arc<RwLock<bool>>, } -impl Default for Player { - fn default() -> Self { - Self::new() - } -} - impl Player { - pub fn new() -> Self { - // Initialize GStreamer - gst::init().unwrap(); + pub fn new() -> Result<Self, PlayerError> { + // Initialize GStreamer, maybe figure out how to nicely fail here + gst::init()?; let ctx = glib::MainContext::default(); let _guard = ctx.acquire(); let mainloop = glib::MainLoop::new(Some(&ctx), false); let playbin_arc = Arc::new(RwLock::new( - gst::ElementFactory::make("playbin3").build().unwrap(), + gst::ElementFactory::make("playbin3").build()?, )); let playbin = playbin_arc.clone(); @@ -99,13 +110,13 @@ impl Player { // Set up the Playbin flags to only play audio let flags = flags_class .builder_with_value(flags) - .unwrap() + .ok_or(PlayerError::Build)? .set_by_nick("audio") .set_by_nick("download") .unset_by_nick("video") .unset_by_nick("text") .build() - .unwrap(); + .ok_or(PlayerError::Build)?; playbin .write() @@ -125,7 +136,7 @@ impl Player { let start_update = Arc::clone(&start); let end_update = Arc::clone(&end); let (message_tx, message_rx) = bounded(1); //TODO: Maybe figure out a better method than making this bounded - std::thread::spawn(move || { + std::thread::spawn(move || { //TODO: Figure out how to return errors nicely in threads loop { let mut pos_temp = playbin_arc .read() @@ -223,7 +234,7 @@ impl Player { }); let source = None; - Self { + Ok(Self { source, playbin, message_rx, @@ -233,7 +244,7 @@ impl Player { paused, position, buffer, - } + }) } pub fn source(&self) -> &Option<URI> { diff --git a/src/music_storage/library.rs b/src/music_storage/library.rs index c3458cf..7c5d466 100644 --- a/src/music_storage/library.rs +++ b/src/music_storage/library.rs @@ -332,13 +332,13 @@ impl MusicLibrary { match global_config.db_path.exists() { true => { - library = read_library(*global_config.db_path.clone())?; + 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.exists() { - library = read_library(*backup_path.clone())?; + library = read_library(backup_path.clone())?; write_library(&library, global_config.db_path.to_path_buf(), false)?; } else { write_library(&library, global_config.db_path.to_path_buf(), false)?; From 463456ad5b43f179503963126f5707a478fcbd3e Mon Sep 17 00:00:00 2001 From: MrDulfin <Dulfinaminator@gmail.com> Date: Fri, 12 Jan 2024 23:47:00 -0500 Subject: [PATCH 066/136] test 2 --- src/music_storage/library.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/music_storage/library.rs b/src/music_storage/library.rs index 706a05d..c3458cf 100644 --- a/src/music_storage/library.rs +++ b/src/music_storage/library.rs @@ -9,7 +9,7 @@ use std::error::Error; use std::ops::ControlFlow::{Break, Continue}; // Files -use file_format::{ FileFormat, Kind }; +use file_format::{FileFormat, Kind}; use glib::filename_to_uri; use lofty::{AudioFile, ItemKey, ItemValue, ParseOptions, Probe, TagType, TaggedFileExt}; use rcue::parser::parse_from_file; From 9eaccc22fd124c28d56f0a3b4eaf6f7fed9123f1 Mon Sep 17 00:00:00 2001 From: G2-Games <ke0bhogsg@gmail.com> Date: Fri, 12 Jan 2024 23:01:06 -0600 Subject: [PATCH 067/136] Removed `music_controller` for restructuring --- src/music_controller/config.rs | 57 ------------------------------ src/music_controller/controller.rs | 50 -------------------------- 2 files changed, 107 deletions(-) delete mode 100644 src/music_controller/config.rs delete mode 100644 src/music_controller/controller.rs diff --git a/src/music_controller/config.rs b/src/music_controller/config.rs deleted file mode 100644 index a9523d3..0000000 --- a/src/music_controller/config.rs +++ /dev/null @@ -1,57 +0,0 @@ -use std::fs; -use std::fs::read_to_string; -use std::path::PathBuf; - -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize, PartialEq, Eq)] -pub struct Config { - pub db_path: PathBuf, -} - -impl Default for Config { - fn default() -> Self { - let path = PathBuf::from("./music_database"); - - Config { - db_path: path, - } - } -} - -impl Config { - /// Creates and saves a new config with default values - pub fn new(config_file: &PathBuf) -> std::io::Result<Config> { - 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<Config, toml::de::Error> { - 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 - if fs::metadata(config_file).is_ok() { - fs::remove_file(config_file)? - } - - fs::rename(temp_file, config_file)?; - Ok(()) - } -} diff --git a/src/music_controller/controller.rs b/src/music_controller/controller.rs deleted file mode 100644 index 6d0acb4..0000000 --- a/src/music_controller/controller.rs +++ /dev/null @@ -1,50 +0,0 @@ -use std::path::PathBuf; -use std::sync::{Arc, RwLock}; - -use crate::music_controller::config::Config; -use crate::music_storage::library::{MusicLibrary, Song, Tag}; - -pub struct MusicController { - pub config: Arc<RwLock<Config>>, - pub library: MusicLibrary, -} - -impl MusicController { - /// Creates new MusicController with config at given path - pub fn new(config_path: &PathBuf) -> Result<MusicController, Box<dyn std::error::Error>> { - let config = Arc::new(RwLock::new(Config::new(config_path)?)); - let library = match MusicLibrary::init(config.clone()) { - Ok(library) => library, - Err(error) => return Err(error), - }; - - let controller = MusicController { config, library }; - - Ok(controller) - } - - /// Creates new music controller from a config at given path - pub fn from(config_path: &PathBuf) -> Result<MusicController, Box<dyn std::error::Error>> { - let config = Arc::new(RwLock::new(Config::from(config_path)?)); - let library = match MusicLibrary::init(config.clone()) { - Ok(library) => library, - Err(error) => return Err(error), - }; - - let controller = MusicController { config, library }; - - Ok(controller) - } - - /// Queries the [MusicLibrary], returning a `Vec<Song>` - pub fn query_library( - &self, - query_string: &String, - target_tags: Vec<Tag>, - _search_location: bool, - sort_by: Vec<Tag>, - ) -> Option<Vec<&Song>> { - self.library - .query_tracks(query_string, &target_tags, &sort_by) - } -} From 3c54745303115ec6aff202621d143693cec27b85 Mon Sep 17 00:00:00 2001 From: MrDulfin <Dulfinaminator@gmail.com> Date: Sat, 13 Jan 2024 00:20:23 -0500 Subject: [PATCH 068/136] updated cargo.toml --- Cargo.toml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index bb0746c..cf407e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,21 +1,21 @@ [package] -name = "dango-core" -version = "0.1.1" +name = "dmp-core" +version = "0.0.0" edition = "2021" license = "AGPL-3.0-only" -description = "A music backend that manages storage, querying, and playback of remote and local songs." -homepage = "https://dangoware.com/dango-music-player" -documentation = "https://docs.rs/dango-core" +description = "Backend crate for the Dango Music Player " +homepage = "" +documentation = "" readme = "README.md" -repository = "https://github.com/Dangoware/dango-music-player" -keywords = ["audio", "music"] -categories = ["multimedia::audio"] +repository = "https://github.com/Dangoware/dmp-core" +keywords = [] +categories = [] [dependencies] -file-format = { version = "0.22.0", features = ["reader-asf", "reader-ebml", "reader-mp4", "reader-rm", "reader-txt", "reader-xml", "serde"] } -lofty = "0.17.1" +file-format = { version = "0.23.0", features = ["reader-asf", "reader-ebml", "reader-mp4", "reader-rm", "reader-txt", "reader-xml", "serde"] } +lofty = "0.18.0" serde = { version = "1.0.191", features = ["derive"] } -toml = "0.7.5" +toml = "0.8.8" walkdir = "2.4.0" chrono = { version = "0.4.31", features = ["serde"] } bincode = { version = "2.0.0-rc.3", features = ["serde"] } From 2d0464a9935652aa5621b6bff7f2d5220a490af9 Mon Sep 17 00:00:00 2001 From: MrDulfin <Dulfinaminator@gmail.com> Date: Sat, 13 Jan 2024 00:57:26 -0500 Subject: [PATCH 069/136] removed broken imports --- src/lib.rs | 5 ----- src/music_storage/library.rs | 1 - src/music_storage/playlist.rs | 4 +--- 3 files changed, 1 insertion(+), 9 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index c20df16..aca688d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,9 +20,4 @@ pub mod music_storage { } } -pub mod music_controller { - pub mod config; - pub mod controller; -} - pub mod music_player; diff --git a/src/music_storage/library.rs b/src/music_storage/library.rs index 7c5d466..5394652 100644 --- a/src/music_storage/library.rs +++ b/src/music_storage/library.rs @@ -1,7 +1,6 @@ use super::music_collection::MusicCollection; // Crate things use super::utils::{find_images, normalize, read_library, write_library}; -use crate::music_controller::config::Config; // Various std things use std::collections::BTreeMap; diff --git a/src/music_storage/playlist.rs b/src/music_storage/playlist.rs index 6b3dc96..099d015 100644 --- a/src/music_storage/playlist.rs +++ b/src/music_storage/playlist.rs @@ -6,7 +6,6 @@ use super::{ library::{self, AlbumArt, Song, Tag}, music_collection::MusicCollection, }; -use crate::music_controller::config::Config; use crate::music_storage::db_reader::xml::reader::XmlLibrary; use std::io::Read; @@ -86,7 +85,6 @@ impl<'a> Playlist<'a> { .get_key_value(&Tag::Title) .unwrap() .1 - .to_string() .into(), ), ..Default::default() @@ -149,7 +147,7 @@ fn list_to_m3u8() { )); let mut a = Playlist::new(); let c = lib.to_songs(); - let mut b = c.iter().map({ |song| song }).collect::<Vec<&Song>>(); + let mut b = c.iter().map( |song| song ).collect::<Vec<&Song>>(); a.tracks.append(&mut b); a.to_m3u8() } From bc75b92237189e9409a50b9aa4270ec662fa4170 Mon Sep 17 00:00:00 2001 From: MrDulfin <Dulfinaminator@gmail.com> Date: Wed, 17 Jan 2024 05:53:18 -0500 Subject: [PATCH 070/136] messing around with config --- Cargo.toml | 1 + src/config/config.rs | 16 +++++ src/config/other_settings.rs | 82 +++++++++++++++++++++++ src/lib.rs | 20 ++---- src/music_storage/db_reader/mod.rs | 13 ++++ src/music_storage/db_reader/xml/reader.rs | 4 +- src/music_storage/playlist.rs | 1 + 7 files changed, 119 insertions(+), 18 deletions(-) create mode 100644 src/config/config.rs create mode 100644 src/config/other_settings.rs create mode 100644 src/music_storage/db_reader/mod.rs diff --git a/Cargo.toml b/Cargo.toml index cf407e4..5a2fa1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,3 +34,4 @@ leb128 = "0.2.5" urlencoding = "2.1.3" m3u8-rs = "5.0.5" thiserror = "1.0.56" +font = "0.27.0" diff --git a/src/config/config.rs b/src/config/config.rs new file mode 100644 index 0000000..ee86c15 --- /dev/null +++ b/src/config/config.rs @@ -0,0 +1,16 @@ +use std::{path::PathBuf, marker::PhantomData}; + +#[derive(Debug, Default)] +pub struct Config { + db_path: Option<PathBuf>, +} + +impl Config { + pub fn new_main() -> Self { + Config::default() + } + //TODO: Add new function for test tube + pub fn load(&self) { + + } +} \ No newline at end of file diff --git a/src/config/other_settings.rs b/src/config/other_settings.rs new file mode 100644 index 0000000..c1940a4 --- /dev/null +++ b/src/config/other_settings.rs @@ -0,0 +1,82 @@ +use std::{marker::PhantomData, fs::File, path::PathBuf}; + +use font::Font; + +pub trait Setting {} + +pub struct DropDown { + name: String, + //value: ??? +} +impl Setting for DropDown {} + +#[derive(Debug, Default)] +pub struct Slider { + name: String, + value: i32, +} +impl Setting for Slider {} + +#[derive(Debug, Default)] +pub struct CheckBox { + name: String, + value: bool, +} +impl Setting for CheckBox {} + +enum TextBoxSize { + Small, + Large, +} +#[derive(Debug, Default)] +pub struct TextBox<Size = TextBoxSize> { + name: String, + text: String, + size: PhantomData<Size> +} +impl Setting for TextBox {} + +#[derive(Debug, Default)] +pub struct SingleSelect { + name: String, + value: bool, +} +impl Setting for SingleSelect {} + +#[derive(Debug, Default)] +pub struct MultiSelect { + name: String, + value: bool, +} +impl Setting for MultiSelect {} + +#[derive(Debug, Default)] +pub struct ConfigCounter { + name: String, + value: i32, +} +impl Setting for ConfigCounter {} + +#[derive(Debug, Default)] +pub struct ConfigFont { + name: String, + value: Font, +} +impl Setting for ConfigFont {} + +#[derive(Debug, Default)] +pub struct ConfigFile { + name: String, + value: PathBuf, +} +impl Setting for ConfigFile {} + +#[derive(Debug, Default)] +pub struct List<T: Setting> { + items: Vec<T> +} + +pub struct Form { + +} + diff --git a/src/lib.rs b/src/lib.rs index aca688d..6d8be4c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,21 +3,11 @@ pub mod music_storage { pub mod music_collection; pub mod playlist; mod utils; - pub mod db_reader { - pub mod foobar { - pub mod reader; - pub mod utils; - } - pub mod musicbee { - pub mod reader; - pub mod utils; - } - pub mod xml { - pub mod reader; - } - pub mod common; - pub mod extern_library; - } + pub mod db_reader; } pub mod music_player; +pub mod config { + pub mod config; + pub mod other_settings; +} diff --git a/src/music_storage/db_reader/mod.rs b/src/music_storage/db_reader/mod.rs new file mode 100644 index 0000000..6569a82 --- /dev/null +++ b/src/music_storage/db_reader/mod.rs @@ -0,0 +1,13 @@ +pub mod foobar { + pub mod reader; + pub mod utils; +} +pub mod musicbee { + pub mod reader; + pub mod utils; +} +pub mod xml { + pub mod reader; +} +pub mod common; +pub mod extern_library; \ No newline at end of file diff --git a/src/music_storage/db_reader/xml/reader.rs b/src/music_storage/db_reader/xml/reader.rs index 50beeac..1652551 100644 --- a/src/music_storage/db_reader/xml/reader.rs +++ b/src/music_storage/db_reader/xml/reader.rs @@ -92,7 +92,6 @@ impl ExternalLibrary for XmlLibrary { } let text = e.unescape().unwrap().to_string(); - if text == count2.to_string() && !key_selected { continue; } @@ -118,8 +117,7 @@ impl ExternalLibrary for XmlLibrary { buf.clear(); } let elasped = now.elapsed(); - println!("\n\nXMLReader\n=========================================\n\nDone!\n{} songs grabbed in {:#?}\nIDs Skipped: {}", count3, elasped, count4); - // dbg!(folder); + println!("\n\nXMLReader grabbed {} songs in {:#?} seconds\nIDs Skipped: {}", count3, elasped.as_secs(), count4); let mut lib = XmlLibrary::new(); lib.tracks.append(converted_songs.as_mut()); lib diff --git a/src/music_storage/playlist.rs b/src/music_storage/playlist.rs index 099d015..4ad49dd 100644 --- a/src/music_storage/playlist.rs +++ b/src/music_storage/playlist.rs @@ -102,6 +102,7 @@ impl<'a> Playlist<'a> { segments: seg.clone(), ..Default::default() }; + //TODO: change this to put in a real file path let mut file = std::fs::OpenOptions::new() .read(true) .create(true) From 4dfc321d2a9fe07669cfb969dfb735c5f40d3e44 Mon Sep 17 00:00:00 2001 From: G2-Games <ke0bhogsg@gmail.com> Date: Wed, 17 Jan 2024 04:54:05 -0600 Subject: [PATCH 071/136] re-added the controller --- src/lib.rs | 3 +-- src/music_controller/controller.rs | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 src/music_controller/controller.rs diff --git a/src/lib.rs b/src/lib.rs index c20df16..f4a93a8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,8 +20,7 @@ pub mod music_storage { } } -pub mod music_controller { - pub mod config; +pub mod music_controller{ pub mod controller; } diff --git a/src/music_controller/controller.rs b/src/music_controller/controller.rs new file mode 100644 index 0000000..8d1c8b6 --- /dev/null +++ b/src/music_controller/controller.rs @@ -0,0 +1 @@ + From c14ab1f04b124547d0adc6ea4bee594c98061713 Mon Sep 17 00:00:00 2001 From: G2-Games <ke0bhogsg@gmail.com> Date: Wed, 17 Jan 2024 06:18:09 -0600 Subject: [PATCH 072/136] Minor work starting on `controller.rs` --- src/config/config.rs | 4 +- src/music_controller/controller.rs | 25 +++++++++++- src/music_storage/library.rs | 56 ++++++++++++++------------- src/music_storage/music_collection.rs | 2 +- src/music_storage/playlist.rs | 35 +++++++---------- 5 files changed, 70 insertions(+), 52 deletions(-) diff --git a/src/config/config.rs b/src/config/config.rs index ee86c15..cac729d 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -2,7 +2,7 @@ use std::{path::PathBuf, marker::PhantomData}; #[derive(Debug, Default)] pub struct Config { - db_path: Option<PathBuf>, + pub db_path: Option<PathBuf>, } impl Config { @@ -13,4 +13,4 @@ impl Config { pub fn load(&self) { } -} \ No newline at end of file +} diff --git a/src/music_controller/controller.rs b/src/music_controller/controller.rs index 8d1c8b6..94509d8 100644 --- a/src/music_controller/controller.rs +++ b/src/music_controller/controller.rs @@ -1 +1,24 @@ - +//! The [Controller] is the input and output for the entire +//! player. It manages queues, playback, library access, and +//! other functions + +use crate::{ + music_player::Player, + music_storage::library::Song, + config::config::Config +}; + +struct Queue { + player: Player, + name: String, + songs: Vec<Song>, +} + +pub struct Controller { + queues: Vec<Queue>, + config: Config, +} + +impl Controller { + // more stuff to come +} diff --git a/src/music_storage/library.rs b/src/music_storage/library.rs index 5394652..873865f 100644 --- a/src/music_storage/library.rs +++ b/src/music_storage/library.rs @@ -1,6 +1,7 @@ -use super::music_collection::MusicCollection; // Crate things use super::utils::{find_images, normalize, read_library, write_library}; +use super::music_collection::MusicCollection; +use crate::config::config::Config; // Various std things use std::collections::BTreeMap; @@ -269,6 +270,16 @@ pub struct Album<'a> { #[allow(clippy::len_without_is_empty)] impl Album<'_> { + //returns the Album title + fn title(&self) -> &String { + self.title + } + + /// Returns the album cover as an AlbumArt struct, if it exists + fn cover(&self) -> Option<&AlbumArt> { + self.cover + } + /// Returns the Album Artist, if they exist pub fn artist(&self) -> Option<&String> { self.artist @@ -283,6 +294,14 @@ impl Album<'_> { Some(self.discs.get(&disc)?[index]) } + fn tracks(&self) -> Vec<&Song> { + let mut songs = Vec::new(); + for disc in &self.discs { + songs.append(&mut disc.1.clone()) + } + songs + } + /// Returns the number of songs in the album pub fn len(&self) -> usize { let mut total = 0; @@ -292,23 +311,6 @@ impl Album<'_> { total } } -impl MusicCollection for Album<'_> { - //returns the Album title - fn title(&self) -> &String { - self.title - } - /// Returns the album cover as an AlbumArt struct, if it exists - fn cover(&self) -> Option<&AlbumArt> { - self.cover - } - fn tracks(&self) -> Vec<&Song> { - let mut songs = Vec::new(); - for disc in &self.discs { - songs.append(&mut disc.1.clone()) - } - songs - } -} const BLOCKED_EXTENSIONS: [&str; 4] = ["vob", "log", "txt", "sf2"]; @@ -327,20 +329,20 @@ impl MusicLibrary { let global_config = &*config.read().unwrap(); let mut library: Vec<Song> = Vec::new(); let mut backup_path = global_config.db_path.clone(); - backup_path.set_extension("bkp"); + backup_path.unwrap().set_extension("bkp"); - match global_config.db_path.exists() { + match global_config.db_path.unwrap().exists() { true => { - library = read_library(global_config.db_path.clone())?; + library = read_library(global_config.db_path.unwrap().clone())?; } false => { // Create the database if it does not exist // possibly from the backup file - if backup_path.exists() { - library = read_library(backup_path.clone())?; - write_library(&library, global_config.db_path.to_path_buf(), false)?; + if backup_path.unwrap().exists() { + library = read_library(backup_path.unwrap().clone())?; + write_library(&library, global_config.db_path.unwrap().to_path_buf(), false)?; } else { - write_library(&library, global_config.db_path.to_path_buf(), false)?; + write_library(&library, global_config.db_path.unwrap().to_path_buf(), false)?; } } }; @@ -350,9 +352,9 @@ impl MusicLibrary { /// Serializes the database out to the file specified in the config pub fn save(&self, config: &Config) -> Result<(), Box<dyn Error>> { - match config.db_path.try_exists() { + match config.db_path.unwrap().try_exists() { Ok(exists) => { - write_library(&self.library, config.db_path.to_path_buf(), exists)?; + write_library(&self.library, config.db_path.unwrap().to_path_buf(), exists)?; } Err(error) => return Err(error.into()), } diff --git a/src/music_storage/music_collection.rs b/src/music_storage/music_collection.rs index 9b1c7ca..964c79a 100644 --- a/src/music_storage/music_collection.rs +++ b/src/music_storage/music_collection.rs @@ -3,5 +3,5 @@ use crate::music_storage::library::{AlbumArt, Song}; pub trait MusicCollection { fn title(&self) -> &String; fn cover(&self) -> Option<&AlbumArt>; - fn tracks(&self) -> Vec<&Song>; + fn tracks(&self) -> Vec<Song>; } diff --git a/src/music_storage/playlist.rs b/src/music_storage/playlist.rs index 4ad49dd..62b3aa9 100644 --- a/src/music_storage/playlist.rs +++ b/src/music_storage/playlist.rs @@ -1,15 +1,15 @@ +use std::path::Path; + use chrono::Duration; use walkdir::Error; use super::{ - db_reader::extern_library::ExternalLibrary, - library::{self, AlbumArt, Song, Tag}, - music_collection::MusicCollection, + library::{AlbumArt, Song, Tag}, + music_collection::MusicCollection, db_reader::{ + xml::reader::XmlLibrary, + extern_library::ExternalLibrary + }, }; -use crate::music_storage::db_reader::xml::reader::XmlLibrary; - -use std::io::Read; -use std::{default, path::Path, path::PathBuf, thread::AccessError}; use m3u8_rs::{MediaPlaylist, MediaPlaylistType, MediaSegment}; // use nom::IResult; @@ -18,7 +18,7 @@ use m3u8_rs::{MediaPlaylist, MediaPlaylistType, MediaSegment}; pub struct Playlist<'a> { title: String, cover: Option<&'a AlbumArt>, - tracks: Vec<&'a Song>, + tracks: Vec<Song>, play_count: i32, play_time: Duration, } @@ -32,11 +32,11 @@ impl<'a> Playlist<'a> { pub fn play_time(&self) -> chrono::Duration { self.play_time } - pub fn set_tracks(&mut self, songs: Vec<&'a Song>) -> Result<(), Error> { + pub fn set_tracks(&mut self, songs: Vec<Song>) -> Result<(), Error> { self.tracks = songs; Ok(()) } - pub fn add_track(&mut self, song: &'a Song) -> Result<(), Error> { + pub fn add_track(&mut self, song: Song) -> Result<(), Error> { self.tracks.push(song); Ok(()) } @@ -79,14 +79,7 @@ impl<'a> Playlist<'a> { |track| MediaSegment { uri: track.location.to_string().into(), duration: track.duration.as_millis() as f32, - title: Some( - track - .tags - .get_key_value(&Tag::Title) - .unwrap() - .1 - .into(), - ), + title: Some(track.tags.get_key_value(&Tag::Title).unwrap().1.into()), ..Default::default() } }) @@ -125,8 +118,8 @@ impl MusicCollection for Playlist<'_> { None => None, } } - fn tracks(&self) -> Vec<&Song> { - self.tracks.clone() + fn tracks(&self) -> Vec<Song> { + self.tracks } } impl Default for Playlist<'_> { @@ -148,7 +141,7 @@ fn list_to_m3u8() { )); let mut a = Playlist::new(); let c = lib.to_songs(); - let mut b = c.iter().map( |song| song ).collect::<Vec<&Song>>(); + let mut b = c.iter().map(|song| song).collect::<Vec<&Song>>(); a.tracks.append(&mut b); a.to_m3u8() } From 4e5a45e544a96a908d701036f6c9289716b0b27a Mon Sep 17 00:00:00 2001 From: MrDulfin <Dulfinaminator@gmail.com> Date: Wed, 17 Jan 2024 07:20:51 -0500 Subject: [PATCH 073/136] messing with Config 2: Electric Boogaloo --- src/config/config.rs | 9 +++- src/config/other_settings.rs | 84 ++++++------------------------------ 2 files changed, 21 insertions(+), 72 deletions(-) diff --git a/src/config/config.rs b/src/config/config.rs index ee86c15..7bec4a0 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -1,8 +1,15 @@ use std::{path::PathBuf, marker::PhantomData}; +use crate::music_storage::library::MusicLibrary; + +#[derive(Debug, Default)] +struct ConfigLibrary { + name: String, + path: PathBuf, +} #[derive(Debug, Default)] pub struct Config { - db_path: Option<PathBuf>, + libraries: Vec<ConfigLibrary> } impl Config { diff --git a/src/config/other_settings.rs b/src/config/other_settings.rs index c1940a4..da94670 100644 --- a/src/config/other_settings.rs +++ b/src/config/other_settings.rs @@ -2,78 +2,20 @@ use std::{marker::PhantomData, fs::File, path::PathBuf}; use font::Font; -pub trait Setting {} +pub enum Setting { + String { + name: String, + value: String + }, + Int { + name: String, + value: i32 + }, + Bool { + name: String, + value: bool + }, -pub struct DropDown { - name: String, - //value: ??? -} -impl Setting for DropDown {} - -#[derive(Debug, Default)] -pub struct Slider { - name: String, - value: i32, -} -impl Setting for Slider {} - -#[derive(Debug, Default)] -pub struct CheckBox { - name: String, - value: bool, -} -impl Setting for CheckBox {} - -enum TextBoxSize { - Small, - Large, -} -#[derive(Debug, Default)] -pub struct TextBox<Size = TextBoxSize> { - name: String, - text: String, - size: PhantomData<Size> -} -impl Setting for TextBox {} - -#[derive(Debug, Default)] -pub struct SingleSelect { - name: String, - value: bool, -} -impl Setting for SingleSelect {} - -#[derive(Debug, Default)] -pub struct MultiSelect { - name: String, - value: bool, -} -impl Setting for MultiSelect {} - -#[derive(Debug, Default)] -pub struct ConfigCounter { - name: String, - value: i32, -} -impl Setting for ConfigCounter {} - -#[derive(Debug, Default)] -pub struct ConfigFont { - name: String, - value: Font, -} -impl Setting for ConfigFont {} - -#[derive(Debug, Default)] -pub struct ConfigFile { - name: String, - value: PathBuf, -} -impl Setting for ConfigFile {} - -#[derive(Debug, Default)] -pub struct List<T: Setting> { - items: Vec<T> } pub struct Form { From 4546082e5446031e7f51f8ef20fb175e4f10ac7b Mon Sep 17 00:00:00 2001 From: MrDulfin <Dulfinaminator@gmail.com> Date: Wed, 17 Jan 2024 09:07:06 -0500 Subject: [PATCH 074/136] created more functions for the config --- Cargo.toml | 2 + src/config/config.rs | 72 ++++++++++++++++++++++++++---- src/music_controller/controller.rs | 4 +- src/music_storage/playlist.rs | 4 +- 4 files changed, 71 insertions(+), 11 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5a2fa1e..aaf4409 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,3 +35,5 @@ urlencoding = "2.1.3" m3u8-rs = "5.0.5" thiserror = "1.0.56" font = "0.27.0" +uuid = { version = "1.6.1", features = ["v4", "serde"]} +serde_json = "1.0.111" diff --git a/src/config/config.rs b/src/config/config.rs index 76ab38a..a54186f 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -1,15 +1,44 @@ -use std::{path::PathBuf, marker::PhantomData}; +use std::{path::{PathBuf, Path}, marker::PhantomData, fs::{File, OpenOptions, self}, io::{Error, Write, Read}, default}; -use crate::music_storage::library::MusicLibrary; +use serde::{Serialize, Deserialize}; +use serde_json::{to_string, to_string_pretty}; +use uuid::Uuid; -#[derive(Debug, Default)] +#[derive(Debug, Clone, Serialize, Deserialize)] struct ConfigLibrary { - name: String, - path: PathBuf, + pub name: String, + pub path: PathBuf, + pub uuid: Uuid } -#[derive(Debug, Default)] +impl ConfigLibrary { + fn new() -> Self { + ConfigLibrary { + name: String::new(), + path: PathBuf::default(), + uuid: Uuid::new_v4() + } + } + pub fn open(&self) -> Result<File, Error> { + match File::open(self.path.as_path()) { + Ok(ok) => Ok(ok), + Err(e) => Err(e) + } + } +} +impl Default for ConfigLibrary { + fn default() -> Self { + ConfigLibrary { + name: String::default(), + path: PathBuf::default(), + uuid: Uuid::new_v4() + } + } +} +#[derive(Debug, Default, Serialize, Deserialize)] pub struct Config { - libraries: Vec<ConfigLibrary> + pub path: PathBuf, + default_library: Uuid, + pub libraries: Vec<ConfigLibrary>, } impl Config { @@ -17,7 +46,34 @@ impl Config { Config::default() } //TODO: Add new function for test tube - pub fn load(&self) { + pub fn set_default_library(&self, uuid: Uuid) { + self.default_library = uuid; + } + //TODO: make this a ConfigError type + pub fn default_library(&self) -> Result<&ConfigLibrary, String> { + for library in &self.libraries { + if library.uuid == self.default_library { + return Ok(library) + } + else { + continue; + } + } + Err("No default library!".to_string()) + } + pub fn save(&self) -> Result<(), Error> { + let mut file = OpenOptions::new().create(true).truncate(true).read(true).write(true).open("dango_temp_config_save.json")?; + let config = to_string_pretty(self)?; + file.write_all(&config.as_bytes())?; + fs::rename("dango_temp_config_save.json", self.path.as_path())?; + Ok(()) + } + pub fn load(path: PathBuf) -> Result<Self, Error> { + let mut file: File = File::open(path)?; + let mut bun: String = String::new(); + _ = file.read_to_string(&mut bun); + let ny: Config = serde_json::from_str::<Config>(&bun)?; + Ok(ny) } } diff --git a/src/music_controller/controller.rs b/src/music_controller/controller.rs index 94509d8..7f62871 100644 --- a/src/music_controller/controller.rs +++ b/src/music_controller/controller.rs @@ -2,6 +2,8 @@ //! player. It manages queues, playback, library access, and //! other functions +use std::sync::{Arc, RwLock}; + use crate::{ music_player::Player, music_storage::library::Song, @@ -16,7 +18,7 @@ struct Queue { pub struct Controller { queues: Vec<Queue>, - config: Config, + config: Arc<RwLock<Config>>, } impl Controller { diff --git a/src/music_storage/playlist.rs b/src/music_storage/playlist.rs index 62b3aa9..cc8166c 100644 --- a/src/music_storage/playlist.rs +++ b/src/music_storage/playlist.rs @@ -119,7 +119,7 @@ impl MusicCollection for Playlist<'_> { } } fn tracks(&self) -> Vec<Song> { - self.tracks + self.tracks.to_owned() } } impl Default for Playlist<'_> { @@ -141,7 +141,7 @@ fn list_to_m3u8() { )); let mut a = Playlist::new(); let c = lib.to_songs(); - let mut b = c.iter().map(|song| song).collect::<Vec<&Song>>(); + let mut b = c.iter().map(|song| song.to_owned()).collect::<Vec<Song>>(); a.tracks.append(&mut b); a.to_m3u8() } From c057352e9b406142806ec3425e02df5f232dd9a7 Mon Sep 17 00:00:00 2001 From: MrDulfin <Dulfinaminator@gmail.com> Date: Wed, 17 Jan 2024 15:22:28 -0500 Subject: [PATCH 075/136] Changed MusicLibrary and config file handling --- src/config/config.rs | 19 +++++++++---------- src/music_storage/library.rs | 14 +++++++++++++- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/config/config.rs b/src/config/config.rs index a54186f..ec2a521 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -11,12 +11,8 @@ struct ConfigLibrary { pub uuid: Uuid } impl ConfigLibrary { - fn new() -> Self { - ConfigLibrary { - name: String::new(), - path: PathBuf::default(), - uuid: Uuid::new_v4() - } + pub fn new() -> Self { + ConfigLibrary::default() } pub fn open(&self) -> Result<File, Error> { match File::open(self.path.as_path()) { @@ -39,6 +35,7 @@ pub struct Config { pub path: PathBuf, default_library: Uuid, pub libraries: Vec<ConfigLibrary>, + volume: f32, } impl Config { @@ -61,15 +58,17 @@ impl Config { } Err("No default library!".to_string()) } - pub fn save(&self) -> Result<(), Error> { - let mut file = OpenOptions::new().create(true).truncate(true).read(true).write(true).open("dango_temp_config_save.json")?; + pub fn to_file(&self) -> Result<(), Error> { + let mut writer = self.path.clone(); + writer.set_extension("tmp"); + let mut file = OpenOptions::new().create(true).truncate(true).read(true).write(true).open(writer)?; let config = to_string_pretty(self)?; file.write_all(&config.as_bytes())?; - fs::rename("dango_temp_config_save.json", self.path.as_path())?; + fs::rename(writer, self.path.as_path())?; Ok(()) } - pub fn load(path: PathBuf) -> Result<Self, Error> { + pub fn load_file(path: PathBuf) -> Result<Self, Error> { let mut file: File = File::open(path)?; let mut bun: String = String::new(); _ = file.read_to_string(&mut bun); diff --git a/src/music_storage/library.rs b/src/music_storage/library.rs index 873865f..a9ffe7a 100644 --- a/src/music_storage/library.rs +++ b/src/music_storage/library.rs @@ -13,6 +13,7 @@ use file_format::{FileFormat, Kind}; use glib::filename_to_uri; use lofty::{AudioFile, ItemKey, ItemValue, ParseOptions, Probe, TagType, TaggedFileExt}; use rcue::parser::parse_from_file; +use uuid::Uuid; use std::fs; use std::path::{Path, PathBuf}; use walkdir::WalkDir; @@ -316,16 +317,27 @@ const BLOCKED_EXTENSIONS: [&str; 4] = ["vob", "log", "txt", "sf2"]; #[derive(Debug)] pub struct MusicLibrary { + pub name: String, + pub uuid: Uuid, pub library: Vec<Song>, } impl MusicLibrary { + pub fn with_uuid(uuid: Uuid, path: PathBuf) -> Result<Self, Box<dyn Error>> { + MusicLibrary { + name: String::default(), + uuid, + library: Vec::new(), + }; + + todo!() + } /// Initialize the database /// /// 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 [MusicLibrary] Vec - pub fn init(config: Arc<RwLock<Config>>) -> Result<Self, Box<dyn Error>> { + pub fn init(config: Arc<RwLock<Config>>, uuid: Uuid) -> Result<Self, Box<dyn Error>> { let global_config = &*config.read().unwrap(); let mut library: Vec<Song> = Vec::new(); let mut backup_path = global_config.db_path.clone(); From b937ac55f1fec63db176037a637d69e255aaba60 Mon Sep 17 00:00:00 2001 From: MrDulfin <Dulfinaminator@gmail.com> Date: Fri, 19 Jan 2024 05:33:58 -0500 Subject: [PATCH 076/136] broke code --- src/config/config.rs | 46 +++++++++++++++++++++------ src/music_storage/library.rs | 61 +++++++++++++++++++++--------------- src/music_storage/utils.rs | 7 ----- 3 files changed, 73 insertions(+), 41 deletions(-) diff --git a/src/config/config.rs b/src/config/config.rs index ec2a521..b3ce860 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -2,10 +2,11 @@ use std::{path::{PathBuf, Path}, marker::PhantomData, fs::{File, OpenOptions, se use serde::{Serialize, Deserialize}; use serde_json::{to_string, to_string_pretty}; +use thiserror::Error; use uuid::Uuid; #[derive(Debug, Clone, Serialize, Deserialize)] -struct ConfigLibrary { +pub struct ConfigLibrary { pub name: String, pub path: PathBuf, pub uuid: Uuid @@ -35,33 +36,52 @@ pub struct Config { pub path: PathBuf, default_library: Uuid, pub libraries: Vec<ConfigLibrary>, + pub library_folder: PathBuf, volume: f32, } impl Config { + pub fn new() -> Self { + Config { + libraries: vec![ConfigLibrary::default()], + ..Default::default() + } + } pub fn new_main() -> Self { Config::default() } //TODO: Add new function for test tube - pub fn set_default_library(&self, uuid: Uuid) { - self.default_library = uuid; + pub fn set_default_library(mut self, uuid: &Uuid) { + self.default_library = *uuid; } - //TODO: make this a ConfigError type - pub fn default_library(&self) -> Result<&ConfigLibrary, String> { + pub fn get_default_library(&self) -> Result<&ConfigLibrary, ConfigError> { for library in &self.libraries { if library.uuid == self.default_library { return Ok(library) } - else { - continue; + } + Err(ConfigError::NoDefaultLibrary) + } + pub fn get_library(&self, uuid: &Uuid) -> Result<ConfigLibrary, ConfigError> { + for library in &self.libraries { + if &library.uuid == uuid { + return Ok(library.to_owned()) } } - Err("No default library!".to_string()) + Err(ConfigError::NoConfigLibrary(*uuid)) + } + pub fn library_exists(&self, uuid: &Uuid) -> bool { + for library in &self.libraries { + if &library.uuid == uuid { + return true + } + } + false } pub fn to_file(&self) -> Result<(), Error> { let mut writer = self.path.clone(); writer.set_extension("tmp"); - let mut file = OpenOptions::new().create(true).truncate(true).read(true).write(true).open(writer)?; + let mut file = OpenOptions::new().create(true).truncate(true).read(true).write(true).open(&writer)?; let config = to_string_pretty(self)?; file.write_all(&config.as_bytes())?; @@ -76,3 +96,11 @@ impl Config { Ok(ny) } } + +#[derive(Error, Debug)] +pub enum ConfigError { + #[error("No Library Found for {0}!")] + NoConfigLibrary(Uuid), + #[error("There is no Default Library for this Config")] + NoDefaultLibrary +} diff --git a/src/music_storage/library.rs b/src/music_storage/library.rs index a9ffe7a..2bd3145 100644 --- a/src/music_storage/library.rs +++ b/src/music_storage/library.rs @@ -2,6 +2,7 @@ use super::utils::{find_images, normalize, read_library, write_library}; use super::music_collection::MusicCollection; use crate::config::config::Config; +use crate::music_storage::library; // Various std things use std::collections::BTreeMap; @@ -321,52 +322,62 @@ pub struct MusicLibrary { pub uuid: Uuid, pub library: Vec<Song>, } - +#[test] + fn test_() { + let a = MusicLibrary::init(Arc::new(RwLock::from(Config::new())), None).unwrap(); + dbg!(a); + } impl MusicLibrary { - pub fn with_uuid(uuid: Uuid, path: PathBuf) -> Result<Self, Box<dyn Error>> { + pub fn new() -> Self { MusicLibrary { name: String::default(), + uuid: Uuid::default(), + library: Vec::new(), + } + } + pub fn with_uuid(uuid: Uuid, path: PathBuf) -> Result<Self, Box<dyn Error>> { + MusicLibrary { + name: String::new(), uuid, library: Vec::new(), }; todo!() } + /// Initialize the database /// /// 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 [MusicLibrary] Vec - pub fn init(config: Arc<RwLock<Config>>, uuid: Uuid) -> Result<Self, Box<dyn Error>> { + pub fn init(config: Arc<RwLock<Config>>, uuid: Option<Uuid>) -> Result<Self, Box<dyn Error>> { let global_config = &*config.read().unwrap(); - let mut library: Vec<Song> = Vec::new(); - let mut backup_path = global_config.db_path.clone(); - backup_path.unwrap().set_extension("bkp"); - match global_config.db_path.unwrap().exists() { - true => { - library = read_library(global_config.db_path.unwrap().clone())?; - } - false => { - // Create the database if it does not exist - // possibly from the backup file - if backup_path.unwrap().exists() { - library = read_library(backup_path.unwrap().clone())?; - write_library(&library, global_config.db_path.unwrap().to_path_buf(), false)?; - } else { - write_library(&library, global_config.db_path.unwrap().to_path_buf(), false)?; + let mut library = MusicLibrary::new(); + + if let Some(uuid) = uuid { + match global_config.library_exists(&uuid) { + true => { + library.library = read_library(global_config.get_library(&uuid)?.path)?; + }, + false => { + // Create the database if it does not exist + write_library(&library.library, global_config.path.clone())?; + library = MusicLibrary::with_uuid(uuid, global_config.path.parent().unwrap().to_path_buf())?; } - } - }; - - Ok(Self { library }) + }; + }else { + write_library(&library.library, global_config.path.clone())?; + } + Ok(library) } /// Serializes the database out to the file specified in the config pub fn save(&self, config: &Config) -> Result<(), Box<dyn Error>> { - match config.db_path.unwrap().try_exists() { - Ok(exists) => { - write_library(&self.library, config.db_path.unwrap().to_path_buf(), exists)?; + let path = config.get_library(&self.uuid)?.path; + match path.try_exists() { + Ok(_) => { + write_library(&self.library, path)?; } Err(error) => return Err(error.into()), } diff --git a/src/music_storage/utils.rs b/src/music_storage/utils.rs index a25313a..c42a6b5 100644 --- a/src/music_storage/utils.rs +++ b/src/music_storage/utils.rs @@ -38,13 +38,10 @@ pub(super) fn read_library(path: PathBuf) -> Result<Vec<Song>, Box<dyn Error>> { pub(super) fn write_library( library: &Vec<Song>, path: PathBuf, - take_backup: bool, ) -> Result<(), Box<dyn Error>> { // 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)?); @@ -58,10 +55,6 @@ pub(super) fn write_library( .with_little_endian() .with_variable_int_encoding(), )?; - - if path.exists() && take_backup { - fs::rename(&path, backup_name)?; - } fs::rename(writer_name, &path)?; Ok(()) From 0f49c50c42399a8e5bd01d04b0bbc9cb44222fd0 Mon Sep 17 00:00:00 2001 From: MrDulfin <Dulfinaminator@gmail.com> Date: Fri, 19 Jan 2024 06:59:27 -0500 Subject: [PATCH 077/136] Edited Config and added a test --- src/config/config.rs | 40 +++++++++++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/src/config/config.rs b/src/config/config.rs index b3ce860..8979907 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -32,18 +32,35 @@ impl Default for ConfigLibrary { } } #[derive(Debug, Default, Serialize, Deserialize)] +pub struct ConfigLibraries { + default_library: Uuid, + pub library_folder: PathBuf, + pub libraries: Vec<ConfigLibrary>, +} +#[derive(Debug, Default, Serialize, Deserialize)] pub struct Config { pub path: PathBuf, - default_library: Uuid, - pub libraries: Vec<ConfigLibrary>, - pub library_folder: PathBuf, + pub libraries: ConfigLibraries, volume: f32, } - +#[test] +fn config_test_() { + Config { + path: PathBuf::from("F:\\temp\\config.json"), + libraries: ConfigLibraries { + libraries: vec![ConfigLibrary::default(),ConfigLibrary::default(),ConfigLibrary::default()], + ..Default::default() + }, + ..Default::default() + }.to_file(); +} impl Config { pub fn new() -> Self { Config { - libraries: vec![ConfigLibrary::default()], + libraries: ConfigLibraries { + libraries: vec![ConfigLibrary::default()], + ..Default::default() + }, ..Default::default() } } @@ -52,18 +69,18 @@ impl Config { } //TODO: Add new function for test tube pub fn set_default_library(mut self, uuid: &Uuid) { - self.default_library = *uuid; + self.libraries.default_library = *uuid; } pub fn get_default_library(&self) -> Result<&ConfigLibrary, ConfigError> { - for library in &self.libraries { - if library.uuid == self.default_library { + for library in &self.libraries.libraries { + if library.uuid == self.libraries.default_library { return Ok(library) } } Err(ConfigError::NoDefaultLibrary) } pub fn get_library(&self, uuid: &Uuid) -> Result<ConfigLibrary, ConfigError> { - for library in &self.libraries { + for library in &self.libraries.libraries { if &library.uuid == uuid { return Ok(library.to_owned()) } @@ -71,7 +88,7 @@ impl Config { Err(ConfigError::NoConfigLibrary(*uuid)) } pub fn library_exists(&self, uuid: &Uuid) -> bool { - for library in &self.libraries { + for library in &self.libraries.libraries { if &library.uuid == uuid { return true } @@ -83,8 +100,9 @@ impl Config { writer.set_extension("tmp"); let mut file = OpenOptions::new().create(true).truncate(true).read(true).write(true).open(&writer)?; let config = to_string_pretty(self)?; + // dbg!(&config); - file.write_all(&config.as_bytes())?; + file.write_all(config.as_bytes())?; fs::rename(writer, self.path.as_path())?; Ok(()) } From 0f5eda5d1d6b239ebc68691f1fb7c46764a1f678 Mon Sep 17 00:00:00 2001 From: G2-Games <ke0bhogsg@gmail.com> Date: Fri, 19 Jan 2024 21:22:03 -0600 Subject: [PATCH 078/136] Implemented new reading/writing in utils, improved library init function --- .gitignore | 14 ++++-- src/config/config.rs | 97 ++++++++++++++++++++++-------------- src/lib.rs | 2 + src/music_storage/library.rs | 56 ++++++++++----------- src/music_storage/utils.rs | 17 ++++--- 5 files changed, 108 insertions(+), 78 deletions(-) diff --git a/.gitignore b/.gitignore index 62c5a2f..72106f3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,15 @@ +# Rust binary output dir target/ -music_database* -*.db3* -config.toml +# Rust configuration Cargo.lock + +# Database files +*.db3* +music_database* + +# Storage formats *.kate-swp* *.m3u -*.m3u8 \ No newline at end of file +*.m3u8 +*.json diff --git a/src/config/config.rs b/src/config/config.rs index 8979907..7c3ffa8 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -1,7 +1,11 @@ -use std::{path::{PathBuf, Path}, marker::PhantomData, fs::{File, OpenOptions, self}, io::{Error, Write, Read}, default}; +use std::{ + path::PathBuf, + fs::{File, OpenOptions, self}, + io::{Error, Write, Read}, +}; use serde::{Serialize, Deserialize}; -use serde_json::{to_string, to_string_pretty}; +use serde_json::to_string_pretty; use thiserror::Error; use uuid::Uuid; @@ -11,6 +15,7 @@ pub struct ConfigLibrary { pub path: PathBuf, pub uuid: Uuid } + impl ConfigLibrary { pub fn new() -> Self { ConfigLibrary::default() @@ -22,6 +27,7 @@ impl ConfigLibrary { } } } + impl Default for ConfigLibrary { fn default() -> Self { ConfigLibrary { @@ -31,29 +37,71 @@ impl Default for ConfigLibrary { } } } + #[derive(Debug, Default, Serialize, Deserialize)] pub struct ConfigLibraries { default_library: Uuid, pub library_folder: PathBuf, pub libraries: Vec<ConfigLibrary>, } + +impl ConfigLibraries { + //TODO: Add new function for test tube + pub fn set_default(mut self, uuid: &Uuid) { + self.default_library = *uuid; + } + + pub fn get_default(&self) -> Result<&ConfigLibrary, ConfigError> { + for library in &self.libraries { + if library.uuid == self.default_library { + return Ok(library) + } + } + Err(ConfigError::NoDefaultLibrary) + } + + pub fn get_library(&self, uuid: &Uuid) -> Result<ConfigLibrary, ConfigError> { + for library in &self.libraries { + if &library.uuid == uuid { + return Ok(library.to_owned()) + } + } + Err(ConfigError::NoConfigLibrary(*uuid)) + } + + pub fn uuid_exists(&self, uuid: &Uuid) -> bool { + for library in &self.libraries { + if &library.uuid == uuid { + return true + } + } + false + } +} + #[derive(Debug, Default, Serialize, Deserialize)] pub struct Config { pub path: PathBuf, pub libraries: ConfigLibraries, volume: f32, } + #[test] -fn config_test_() { - Config { - path: PathBuf::from("F:\\temp\\config.json"), +fn config_test() { + let _ = Config { + path: PathBuf::from("config_test.json"), libraries: ConfigLibraries { - libraries: vec![ConfigLibrary::default(),ConfigLibrary::default(),ConfigLibrary::default()], + libraries: vec![ + ConfigLibrary::default(), + ConfigLibrary::default(), + ConfigLibrary::default() + ], ..Default::default() }, ..Default::default() - }.to_file(); + }.write_file(); } + impl Config { pub fn new() -> Self { Config { @@ -64,38 +112,12 @@ impl Config { ..Default::default() } } + pub fn new_main() -> Self { Config::default() } - //TODO: Add new function for test tube - pub fn set_default_library(mut self, uuid: &Uuid) { - self.libraries.default_library = *uuid; - } - pub fn get_default_library(&self) -> Result<&ConfigLibrary, ConfigError> { - for library in &self.libraries.libraries { - if library.uuid == self.libraries.default_library { - return Ok(library) - } - } - Err(ConfigError::NoDefaultLibrary) - } - pub fn get_library(&self, uuid: &Uuid) -> Result<ConfigLibrary, ConfigError> { - for library in &self.libraries.libraries { - if &library.uuid == uuid { - return Ok(library.to_owned()) - } - } - Err(ConfigError::NoConfigLibrary(*uuid)) - } - pub fn library_exists(&self, uuid: &Uuid) -> bool { - for library in &self.libraries.libraries { - if &library.uuid == uuid { - return true - } - } - false - } - pub fn to_file(&self) -> Result<(), Error> { + + pub fn write_file(&self) -> Result<(), Error> { let mut writer = self.path.clone(); writer.set_extension("tmp"); let mut file = OpenOptions::new().create(true).truncate(true).read(true).write(true).open(&writer)?; @@ -106,7 +128,8 @@ impl Config { fs::rename(writer, self.path.as_path())?; Ok(()) } - pub fn load_file(path: PathBuf) -> Result<Self, Error> { + + pub fn read_file(path: PathBuf) -> Result<Self, Error> { let mut file: File = File::open(path)?; let mut bun: String = String::new(); _ = file.read_to_string(&mut bun); diff --git a/src/lib.rs b/src/lib.rs index 3222def..2c479ae 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,8 @@ pub mod music_storage { pub mod music_collection; pub mod playlist; mod utils; + + #[allow(dead_code)] pub mod db_reader; } diff --git a/src/music_storage/library.rs b/src/music_storage/library.rs index 2bd3145..4e4711b 100644 --- a/src/music_storage/library.rs +++ b/src/music_storage/library.rs @@ -1,8 +1,6 @@ // Crate things -use super::utils::{find_images, normalize, read_library, write_library}; -use super::music_collection::MusicCollection; +use super::utils::{find_images, normalize, read_file, write_file}; use crate::config::config::Config; -use crate::music_storage::library; // Various std things use std::collections::BTreeMap; @@ -316,25 +314,30 @@ impl Album<'_> { const BLOCKED_EXTENSIONS: [&str; 4] = ["vob", "log", "txt", "sf2"]; -#[derive(Debug)] +#[derive(Debug, Serialize, Deserialize)] pub struct MusicLibrary { pub name: String, pub uuid: Uuid, pub library: Vec<Song>, } + #[test] - fn test_() { - let a = MusicLibrary::init(Arc::new(RwLock::from(Config::new())), None).unwrap(); - dbg!(a); - } +fn library_init() { + let uuidv4 = Uuid::new_v4(); + let a = MusicLibrary::init(Arc::new(RwLock::from(Config::new())), uuidv4).unwrap(); + dbg!(a); +} + impl MusicLibrary { - pub fn new() -> Self { + pub fn new(name: String, uuid: Uuid) -> Self { MusicLibrary { - name: String::default(), - uuid: Uuid::default(), + name, + uuid, library: Vec::new(), } } + + /* pub fn with_uuid(uuid: Uuid, path: PathBuf) -> Result<Self, Box<dyn Error>> { MusicLibrary { name: String::new(), @@ -344,40 +347,35 @@ impl MusicLibrary { todo!() } + */ /// Initialize the database /// /// 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 [MusicLibrary] Vec - pub fn init(config: Arc<RwLock<Config>>, uuid: Option<Uuid>) -> Result<Self, Box<dyn Error>> { + pub fn init(config: Arc<RwLock<Config>>, uuid: Uuid) -> Result<Self, Box<dyn Error>> { let global_config = &*config.read().unwrap(); - let mut library = MusicLibrary::new(); + let library: MusicLibrary = match global_config.libraries.uuid_exists(&uuid) { + true => read_file(global_config.libraries.get_library(&uuid)?.path)?, + false => { + // If the library does not exist, re-create it + let lib = MusicLibrary::new(String::new(), uuid); + write_file(&lib, global_config.libraries.get_library(&uuid)?.path)?; + lib + } + }; - if let Some(uuid) = uuid { - match global_config.library_exists(&uuid) { - true => { - library.library = read_library(global_config.get_library(&uuid)?.path)?; - }, - false => { - // Create the database if it does not exist - write_library(&library.library, global_config.path.clone())?; - library = MusicLibrary::with_uuid(uuid, global_config.path.parent().unwrap().to_path_buf())?; - } - }; - }else { - write_library(&library.library, global_config.path.clone())?; - } Ok(library) } /// Serializes the database out to the file specified in the config pub fn save(&self, config: &Config) -> Result<(), Box<dyn Error>> { - let path = config.get_library(&self.uuid)?.path; + let path = config.libraries.get_library(&self.uuid)?.path; match path.try_exists() { Ok(_) => { - write_library(&self.library, path)?; + write_file(&self.library, path)?; } Err(error) => return Err(error.into()), } diff --git a/src/music_storage/utils.rs b/src/music_storage/utils.rs index c42a6b5..945aa4c 100644 --- a/src/music_storage/utils.rs +++ b/src/music_storage/utils.rs @@ -6,7 +6,7 @@ use walkdir::WalkDir; use snap; -use super::library::{AlbumArt, Song, URI}; +use super::library::{AlbumArt, URI}; use unidecode::unidecode; pub(super) fn normalize(input_string: &str) -> String { @@ -19,35 +19,36 @@ pub(super) fn normalize(input_string: &str) -> String { normalized } -pub(super) fn read_library(path: PathBuf) -> Result<Vec<Song>, Box<dyn Error>> { +pub(super) fn read_file<T: for<'de> serde::Deserialize<'de>>(path: PathBuf) -> Result<T, Box<dyn Error>> { // 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<Song> = bincode::serde::decode_from_std_read( + let library: T = bincode::serde::decode_from_std_read( &mut d, bincode::config::standard() .with_little_endian() .with_variable_int_encoding(), )?; + Ok(library) } -pub(super) fn write_library( - library: &Vec<Song>, +pub(super) fn write_file<T: serde::Serialize>( + library: &T, path: PathBuf, ) -> Result<(), Box<dyn Error>> { - // Create 2 new names for the file, a temporary one for writing out, and a backup + // Create a temporary name for writing out let mut writer_name = path.clone(); writer_name.set_extension("tmp"); - // Create a new BufWriter on the file and make a snap frame encoer for it too + // Create a new BufWriter on the file and a snap frame encoder let writer = BufWriter::new(fs::File::create(&writer_name)?); let mut e = snap::write::FrameEncoder::new(writer); - // Write out the data using bincode + // Write out the data bincode::serde::encode_into_std_write( library, &mut e, From 097cc1147ef3c3d929f2c1bec7a103f402b3aa07 Mon Sep 17 00:00:00 2001 From: MrDulfin <Dulfinaminator@gmail.com> Date: Fri, 19 Jan 2024 22:35:19 -0500 Subject: [PATCH 079/136] renamed xml to itunes --- .../db_reader/{xml => itunes}/reader.rs | 64 +++++-------------- src/music_storage/db_reader/mod.rs | 2 +- src/music_storage/playlist.rs | 4 +- 3 files changed, 19 insertions(+), 51 deletions(-) rename src/music_storage/db_reader/{xml => itunes}/reader.rs (86%) diff --git a/src/music_storage/db_reader/xml/reader.rs b/src/music_storage/db_reader/itunes/reader.rs similarity index 86% rename from src/music_storage/db_reader/xml/reader.rs rename to src/music_storage/db_reader/itunes/reader.rs index 1652551..c2063c0 100644 --- a/src/music_storage/db_reader/xml/reader.rs +++ b/src/music_storage/db_reader/itunes/reader.rs @@ -18,18 +18,18 @@ use crate::music_storage::utils; use urlencoding::decode; #[derive(Debug, Default, Clone)] -pub struct XmlLibrary { - tracks: Vec<XMLSong>, +pub struct ITunesLibrary { + tracks: Vec<ITunesSong>, } -impl XmlLibrary { +impl ITunesLibrary { fn new() -> Self { Default::default() } - pub fn tracks(self) -> Vec<XMLSong> { + pub fn tracks(self) -> Vec<ITunesSong> { self.tracks } } -impl ExternalLibrary for XmlLibrary { +impl ExternalLibrary for ITunesLibrary { fn from_file(file: &Path) -> Self { let mut reader = Reader::from_file(file).unwrap(); reader.trim_text(true); @@ -45,7 +45,7 @@ impl ExternalLibrary for XmlLibrary { let mut buf = Vec::new(); let mut skip = false; - let mut converted_songs: Vec<XMLSong> = Vec::new(); + let mut converted_songs: Vec<ITunesSong> = Vec::new(); let mut song_tags: HashMap<String, String> = HashMap::new(); let mut key: String = String::new(); @@ -63,7 +63,7 @@ impl ExternalLibrary for XmlLibrary { tagvalue.clear(); key_selected = false; - //end the song to start a new one, and turn turn current song map into XMLSong + //end the song to start a new one, and turn turn current song map into iTunesSong if song_tags.contains_key(&"Location".to_string()) { count3 += 1; //check for skipped IDs @@ -73,7 +73,7 @@ impl ExternalLibrary for XmlLibrary { count3 += 1; count4 += 1; } - converted_songs.push(XMLSong::from_hashmap(&mut song_tags).unwrap()); + converted_songs.push(ITunesSong::from_hashmap(&mut song_tags).unwrap()); song_tags.clear(); skip = true; } @@ -117,8 +117,8 @@ impl ExternalLibrary for XmlLibrary { buf.clear(); } let elasped = now.elapsed(); - println!("\n\nXMLReader grabbed {} songs in {:#?} seconds\nIDs Skipped: {}", count3, elasped.as_secs(), count4); - let mut lib = XmlLibrary::new(); + println!("\n\niTunesReader grabbed {} songs in {:#?} seconds\nIDs Skipped: {}", count3, elasped.as_secs(), count4); + let mut lib = ITunesLibrary::new(); lib.tracks.append(converted_songs.as_mut()); lib } @@ -253,7 +253,7 @@ fn get_art(file: &Path) -> Result<Vec<AlbumArt>, LoftyError> { } #[derive(Debug, Clone, Default)] -pub struct XMLSong { +pub struct ITunesSong { pub id: i32, pub plays: i32, pub favorited: bool, @@ -268,13 +268,13 @@ pub struct XMLSong { pub location: String, } -impl XMLSong { - pub fn new() -> XMLSong { +impl ITunesSong { + pub fn new() -> ITunesSong { Default::default() } - fn from_hashmap(map: &mut HashMap<String, String>) -> Result<XMLSong, LoftyError> { - let mut song = XMLSong::new(); + fn from_hashmap(map: &mut HashMap<String, String>) -> Result<ITunesSong, LoftyError> { + let mut song = ITunesSong::new(); //get the path with the first bit chopped off let path_: String = map.get_key_value("Location").unwrap().1.clone(); let track_type: String = map.get_key_value("Track Type").unwrap().1.clone(); @@ -320,36 +320,4 @@ impl XMLSong { // println!("{:.2?}", song); Ok(song) } -} - -// fn get_folder(file: &PathBuf) -> String { -// let mut reader = Reader::from_file(file).unwrap(); -// reader.trim_text(true); -// //count every event, for fun ig? -// let mut count = 0; -// let mut buf = Vec::new(); -// let mut folder = String::new(); -// loop { -// match reader.read_event_into(&mut buf) { -// Ok(Event::Start(_)) => { -// count += 1; -// } -// Ok(Event::Text(e)) => { -// if count == 10 { -// folder = String::from( -// e.unescape() -// .unwrap() -// .to_string() -// .strip_prefix("file://localhost/") -// .unwrap(), -// ); -// return folder; -// } -// } -// Err(_e) => { -// panic!("oh no! something happened in the public function `get_reader_from_xml()!`") -// } -// _ => (), -// } -// } -// } +} \ No newline at end of file diff --git a/src/music_storage/db_reader/mod.rs b/src/music_storage/db_reader/mod.rs index 6569a82..2a5b979 100644 --- a/src/music_storage/db_reader/mod.rs +++ b/src/music_storage/db_reader/mod.rs @@ -6,7 +6,7 @@ pub mod musicbee { pub mod reader; pub mod utils; } -pub mod xml { +pub mod itunes { pub mod reader; } pub mod common; diff --git a/src/music_storage/playlist.rs b/src/music_storage/playlist.rs index cc8166c..a35e9c1 100644 --- a/src/music_storage/playlist.rs +++ b/src/music_storage/playlist.rs @@ -6,7 +6,7 @@ use walkdir::Error; use super::{ library::{AlbumArt, Song, Tag}, music_collection::MusicCollection, db_reader::{ - xml::reader::XmlLibrary, + itunes::reader::ITunesLibrary, extern_library::ExternalLibrary }, }; @@ -136,7 +136,7 @@ impl Default for Playlist<'_> { #[test] fn list_to_m3u8() { - let lib = XmlLibrary::from_file(Path::new( + let lib = ITunesLibrary::from_file(Path::new( "F:\\Music\\Mp3\\Music Main\\iTunes Music Library.xml", )); let mut a = Playlist::new(); From e931c2b5286d2ec64844508d2dd18620b24594c6 Mon Sep 17 00:00:00 2001 From: G2-Games <ke0bhogsg@gmail.com> Date: Sat, 20 Jan 2024 20:38:50 -0600 Subject: [PATCH 080/136] Modified `library.rs` to properly save with bincode, added new functions to it as well --- Cargo.toml | 9 +++--- src/config/config.rs | 41 +++++++++++++++------------ src/music_storage/library.rs | 49 +++++++++++++++----------------- src/music_storage/utils.rs | 55 ++++++++++++++++++++---------------- 4 files changed, 80 insertions(+), 74 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index aaf4409..70c70e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,8 +14,7 @@ categories = [] [dependencies] file-format = { version = "0.23.0", features = ["reader-asf", "reader-ebml", "reader-mp4", "reader-rm", "reader-txt", "reader-xml", "serde"] } lofty = "0.18.0" -serde = { version = "1.0.191", features = ["derive"] } -toml = "0.8.8" +serde = { version = "1.0.195", features = ["derive"] } walkdir = "2.4.0" chrono = { version = "0.4.31", features = ["serde"] } bincode = { version = "2.0.0-rc.3", features = ["serde"] } @@ -23,10 +22,10 @@ unidecode = "0.3.0" rayon = "1.8.0" log = "0.4" base64 = "0.21.5" -snap = "1.1.0" +snap = "1" rcue = "0.1.3" -gstreamer = "0.21.2" -glib = "0.18.3" +gstreamer = "0.21.3" +glib = "0.18.5" crossbeam-channel = "0.5.8" crossbeam = "0.8.2" quick-xml = "0.31.0" diff --git a/src/config/config.rs b/src/config/config.rs index 7c3ffa8..206287a 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -16,10 +16,25 @@ pub struct ConfigLibrary { pub uuid: Uuid } -impl ConfigLibrary { - pub fn new() -> Self { - ConfigLibrary::default() +impl Default for ConfigLibrary { + fn default() -> Self { + ConfigLibrary { + name: String::new(), + path: PathBuf::from("library"), + uuid: Uuid::new_v4(), + } } +} + +impl ConfigLibrary { + pub fn new(path: PathBuf, name: String) -> Self { + ConfigLibrary { + name, + path, + uuid: Uuid::new_v4(), + } + } + pub fn open(&self) -> Result<File, Error> { match File::open(self.path.as_path()) { Ok(ok) => Ok(ok), @@ -28,19 +43,9 @@ impl ConfigLibrary { } } -impl Default for ConfigLibrary { - fn default() -> Self { - ConfigLibrary { - name: String::default(), - path: PathBuf::default(), - uuid: Uuid::new_v4() - } - } -} - #[derive(Debug, Default, Serialize, Deserialize)] pub struct ConfigLibraries { - default_library: Uuid, + pub default_library: Uuid, pub library_folder: PathBuf, pub libraries: Vec<ConfigLibrary>, } @@ -83,7 +88,7 @@ impl ConfigLibraries { pub struct Config { pub path: PathBuf, pub libraries: ConfigLibraries, - volume: f32, + pub volume: f32, } #[test] @@ -92,9 +97,9 @@ fn config_test() { path: PathBuf::from("config_test.json"), libraries: ConfigLibraries { libraries: vec![ - ConfigLibrary::default(), - ConfigLibrary::default(), - ConfigLibrary::default() + ConfigLibrary::new(PathBuf::from("library1"), String::from("library1")), + ConfigLibrary::new(PathBuf::from("library2"), String::from("library2")), + ConfigLibrary::new(PathBuf::from("library3"), String::from("library3")) ], ..Default::default() }, diff --git a/src/music_storage/library.rs b/src/music_storage/library.rs index 4e4711b..444532f 100644 --- a/src/music_storage/library.rs +++ b/src/music_storage/library.rs @@ -323,13 +323,15 @@ pub struct MusicLibrary { #[test] fn library_init() { - let uuidv4 = Uuid::new_v4(); - let a = MusicLibrary::init(Arc::new(RwLock::from(Config::new())), uuidv4).unwrap(); + let config = Config::read_file(PathBuf::from("config_test.json")).unwrap(); + let target_uuid = config.libraries.libraries[0].uuid; + let a = MusicLibrary::init(Arc::new(RwLock::from(config)), target_uuid).unwrap(); dbg!(a); } impl MusicLibrary { - pub fn new(name: String, uuid: Uuid) -> Self { + /// Create a new library from a name and [Uuid] + fn new(name: String, uuid: Uuid) -> Self { MusicLibrary { name, uuid, @@ -337,18 +339,6 @@ impl MusicLibrary { } } - /* - pub fn with_uuid(uuid: Uuid, path: PathBuf) -> Result<Self, Box<dyn Error>> { - MusicLibrary { - name: String::new(), - uuid, - library: Vec::new(), - }; - - todo!() - } - */ - /// Initialize the database /// /// If the database file already exists, return the [MusicLibrary], otherwise create @@ -357,7 +347,7 @@ impl MusicLibrary { pub fn init(config: Arc<RwLock<Config>>, uuid: Uuid) -> Result<Self, Box<dyn Error>> { let global_config = &*config.read().unwrap(); - let library: MusicLibrary = match global_config.libraries.uuid_exists(&uuid) { + let library: MusicLibrary = match global_config.libraries.get_library(&uuid)?.path.exists() { true => read_file(global_config.libraries.get_library(&uuid)?.path)?, false => { // If the library does not exist, re-create it @@ -370,13 +360,24 @@ impl MusicLibrary { Ok(library) } + //#[cfg(debug_assertions)] // We probably wouldn't want to use this for real, but maybe it would have some utility? + pub fn from_path(path: PathBuf) -> Result<Self, Box<dyn Error>> { + let library: MusicLibrary = match path.exists() { + true => read_file(path)?, + false => { + let lib = MusicLibrary::new(String::new(), Uuid::new_v4()); + write_file(&lib, path)?; + lib + } + }; + Ok(library) + } + /// Serializes the database out to the file specified in the config pub fn save(&self, config: &Config) -> Result<(), Box<dyn Error>> { let path = config.libraries.get_library(&self.uuid)?.path; match path.try_exists() { - Ok(_) => { - write_file(&self.library, path)?; - } + Ok(_) => write_file(self, path)?, Err(error) => return Err(error.into()), } @@ -384,12 +385,12 @@ impl MusicLibrary { } /// Returns the library size in number of tracks - pub fn size(&self) -> usize { + pub fn len_tracks(&self) -> usize { self.library.len() } /// Queries for a [Song] by its [URI], returning a single `Song` - /// with the `URI` that matches + /// with the `URI` that matches along with its position in the library pub fn query_uri(&self, path: &URI) -> Option<(&Song, usize)> { let result = self .library @@ -408,7 +409,7 @@ impl MusicLibrary { } } - /// Queries for a [Song] by its [PathBuf], returning a `Vec<Song>` + /// Queries for a [Song] by its [PathBuf], returning a `Vec<&Song>` /// with matching `PathBuf`s fn query_path(&self, path: PathBuf) -> Option<Vec<&Song>> { let result: Arc<Mutex<Vec<&Song>>> = Arc::new(Mutex::new(Vec::new())); @@ -428,7 +429,6 @@ impl MusicLibrary { pub fn scan_folder( &mut self, target_path: &str, - config: &Config, ) -> Result<i32, Box<dyn std::error::Error>> { let mut total = 0; let mut errors = 0; @@ -488,9 +488,6 @@ impl MusicLibrary { } } - // Save the database after scanning finishes - self.save(config).unwrap(); - println!("ERRORS: {}", errors); Ok(total) diff --git a/src/music_storage/utils.rs b/src/music_storage/utils.rs index 945aa4c..a14dbe1 100644 --- a/src/music_storage/utils.rs +++ b/src/music_storage/utils.rs @@ -1,15 +1,17 @@ -use file_format::{FileFormat, Kind}; +use std::fs::{File, self}; use std::io::{BufReader, BufWriter}; use std::path::{Path, PathBuf}; -use std::{error::Error, fs}; +use std::error::Error; + use walkdir::WalkDir; - +use file_format::{FileFormat, Kind}; use snap; - -use super::library::{AlbumArt, URI}; use unidecode::unidecode; +use super::library::{AlbumArt, URI}; + pub(super) fn normalize(input_string: &str) -> String { + // Normalize the string to latin characters... this needs a lot of work let mut normalized = unidecode(input_string); // Remove non alphanumeric characters @@ -19,25 +21,10 @@ pub(super) fn normalize(input_string: &str) -> String { normalized } -pub(super) fn read_file<T: for<'de> serde::Deserialize<'de>>(path: PathBuf) -> Result<T, Box<dyn Error>> { - // 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: T = bincode::serde::decode_from_std_read( - &mut d, - bincode::config::standard() - .with_little_endian() - .with_variable_int_encoding(), - )?; - - Ok(library) -} - +/// Write any data structure which implements [serde::Serialize] +/// out to a [bincode] encoded file compressed using [snap] pub(super) fn write_file<T: serde::Serialize>( - library: &T, + library: T, path: PathBuf, ) -> Result<(), Box<dyn Error>> { // Create a temporary name for writing out @@ -45,7 +32,7 @@ pub(super) fn write_file<T: serde::Serialize>( writer_name.set_extension("tmp"); // Create a new BufWriter on the file and a snap frame encoder - let writer = BufWriter::new(fs::File::create(&writer_name)?); + let writer = BufWriter::new(File::create(&writer_name)?); let mut e = snap::write::FrameEncoder::new(writer); // Write out the data @@ -61,6 +48,24 @@ pub(super) fn write_file<T: serde::Serialize>( Ok(()) } +/// Read a file serialized out with [write_file] and turn it into +/// the desired structure +pub(super) fn read_file<T: for<'de> serde::Deserialize<'de>>(path: PathBuf) -> Result<T, Box<dyn Error>> { + // Create a new snap reader over the file + let file_reader = BufReader::new(File::open(path)?); + let mut d = snap::read::FrameDecoder::new(file_reader); + + // Decode the library from the serialized data into the vec + let library: T = bincode::serde::decode_from_std_read( + &mut d, + bincode::config::standard() + .with_little_endian() + .with_variable_int_encoding(), + )?; + + Ok(library) +} + pub fn find_images(song_path: &Path) -> Result<Vec<AlbumArt>, Box<dyn Error>> { let mut images: Vec<AlbumArt> = Vec::new(); @@ -85,7 +90,7 @@ pub fn find_images(song_path: &Path) -> Result<Vec<AlbumArt>, Box<dyn Error>> { break; } - let image_uri = URI::Local(path.to_path_buf().canonicalize().unwrap()); + let image_uri = URI::Local(path.to_path_buf().canonicalize()?); images.push(AlbumArt::External(image_uri)); } From 7c5a9c282b156337d7de5ab9d4bacafbafa257f1 Mon Sep 17 00:00:00 2001 From: MrDulfin <Dulfinaminator@gmail.com> Date: Sun, 21 Jan 2024 05:24:10 -0500 Subject: [PATCH 081/136] added more tests --- .gitignore | 2 +- src/config/config.rs | 77 ++++++++++++++++++++++++++++++++------------ 2 files changed, 57 insertions(+), 22 deletions(-) diff --git a/.gitignore b/.gitignore index 72106f3..a9e4c0b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ # Rust binary output dir target/ - +test-config/ # Rust configuration Cargo.lock diff --git a/src/config/config.rs b/src/config/config.rs index 206287a..bc13836 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -1,7 +1,7 @@ use std::{ path::PathBuf, fs::{File, OpenOptions, self}, - io::{Error, Write, Read}, + io::{Error, Write, Read}, sync::{Arc, RwLock}, }; use serde::{Serialize, Deserialize}; @@ -9,11 +9,14 @@ use serde_json::to_string_pretty; use thiserror::Error; use uuid::Uuid; +use crate::music_storage::library::{MusicLibrary, self}; + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ConfigLibrary { pub name: String, pub path: PathBuf, - pub uuid: Uuid + pub uuid: Uuid, + pub scan_folders: Option<Vec<PathBuf>>, } impl Default for ConfigLibrary { @@ -22,16 +25,18 @@ impl Default for ConfigLibrary { name: String::new(), path: PathBuf::from("library"), uuid: Uuid::new_v4(), + scan_folders: None, } } } impl ConfigLibrary { - pub fn new(path: PathBuf, name: String) -> Self { + pub fn new(path: PathBuf, name: String, scan_folders: Option<Vec<PathBuf>>) -> Self { ConfigLibrary { name, path, uuid: Uuid::new_v4(), + scan_folders, } } @@ -43,7 +48,7 @@ impl ConfigLibrary { } } -#[derive(Debug, Default, Serialize, Deserialize)] +#[derive(Debug, Default, Serialize, Deserialize, Clone)] pub struct ConfigLibraries { pub default_library: Uuid, pub library_folder: PathBuf, @@ -84,29 +89,13 @@ impl ConfigLibraries { } } -#[derive(Debug, Default, Serialize, Deserialize)] +#[derive(Debug, Default, Serialize, Deserialize, Clone)] pub struct Config { pub path: PathBuf, pub libraries: ConfigLibraries, pub volume: f32, } -#[test] -fn config_test() { - let _ = Config { - path: PathBuf::from("config_test.json"), - libraries: ConfigLibraries { - libraries: vec![ - ConfigLibrary::new(PathBuf::from("library1"), String::from("library1")), - ConfigLibrary::new(PathBuf::from("library2"), String::from("library2")), - ConfigLibrary::new(PathBuf::from("library3"), String::from("library3")) - ], - ..Default::default() - }, - ..Default::default() - }.write_file(); -} - impl Config { pub fn new() -> Self { Config { @@ -150,3 +139,49 @@ pub enum ConfigError { #[error("There is no Default Library for this Config")] NoDefaultLibrary } + + +#[test] +fn config_test() { + let lib_a = ConfigLibrary::new(PathBuf::from("test-config/library1"), String::from("library1"), None); + let lib_b = ConfigLibrary::new(PathBuf::from("test-config/library2"), String::from("library2"), None); + let lib_c = ConfigLibrary::new(PathBuf::from("test-config/library3"), String::from("library3"), None); + let config = Config { + path: PathBuf::from("test-config/config_test.json"), + libraries: ConfigLibraries { + libraries: vec![ + lib_a.clone(), + lib_b.clone(), + lib_c.clone(), + ], + ..Default::default() + }, + ..Default::default() + }; + config.write_file(); + let arc = Arc::new(RwLock::from(config)); + MusicLibrary::init(arc.clone(), lib_a.uuid.clone()).unwrap(); + MusicLibrary::init(arc.clone(), lib_b.uuid.clone()).unwrap(); + MusicLibrary::init(arc.clone(), lib_c.uuid.clone()).unwrap(); + +} + +#[test] +fn test2() { + let config = Config::read_file(PathBuf::from("test-config/config_test.json")).unwrap(); + let uuid = config.libraries.get_default().unwrap().uuid.clone(); + let mut lib = MusicLibrary::init(Arc::new(RwLock::from(config.clone())), uuid).unwrap(); + lib.scan_folder("test-config/music/").unwrap(); + lib.save(&config).unwrap(); + dbg!(&lib); + dbg!(&config); +} + +#[test] +fn test3() { + let config = Config::read_file(PathBuf::from("test-config/config_test.json")).unwrap(); + let uuid = config.libraries.get_default().unwrap().uuid; + let mut lib = MusicLibrary::init(Arc::new(RwLock::from(config.clone())), uuid).unwrap(); + + dbg!(lib); +} \ No newline at end of file From 03c8d67b5eb3682390792b13801d6453408dee9b Mon Sep 17 00:00:00 2001 From: MrDulfin <Dulfinaminator@gmail.com> Date: Mon, 22 Jan 2024 14:06:13 -0500 Subject: [PATCH 082/136] s --- Cargo.toml | 1 + src/lib.rs | 2 ++ src/music_controller/connections.rs | 9 +++++++++ 3 files changed, 12 insertions(+) create mode 100644 src/music_controller/connections.rs diff --git a/Cargo.toml b/Cargo.toml index 70c70e1..7b4d63a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,3 +36,4 @@ thiserror = "1.0.56" font = "0.27.0" uuid = { version = "1.6.1", features = ["v4", "serde"]} serde_json = "1.0.111" +discord-presence = "0.5.18" \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 2c479ae..3119019 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,9 +10,11 @@ pub mod music_storage { pub mod music_controller{ pub mod controller; + pub mod connections; } pub mod music_player; +#[allow(clippy::module_inception)] pub mod config { pub mod config; pub mod other_settings; diff --git a/src/music_controller/connections.rs b/src/music_controller/connections.rs new file mode 100644 index 0000000..e383e1a --- /dev/null +++ b/src/music_controller/connections.rs @@ -0,0 +1,9 @@ +use std::{env, thread, time}; +use discord_presence::{Client, Event}; + +use super::controller::Controller; + + +impl Controller { + //more stuff goes here +} From fd82f3072caec5a496b70ece93804975d49337ad Mon Sep 17 00:00:00 2001 From: MrDulfin <Dulfinaminator@gmail.com> Date: Mon, 22 Jan 2024 14:07:04 -0500 Subject: [PATCH 083/136] removed rpc stuff --- Cargo.toml | 3 +-- src/music_controller/connections.rs | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7b4d63a..9c8d51a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,5 +35,4 @@ m3u8-rs = "5.0.5" thiserror = "1.0.56" font = "0.27.0" uuid = { version = "1.6.1", features = ["v4", "serde"]} -serde_json = "1.0.111" -discord-presence = "0.5.18" \ No newline at end of file +serde_json = "1.0.111" \ No newline at end of file diff --git a/src/music_controller/connections.rs b/src/music_controller/connections.rs index e383e1a..86d8f29 100644 --- a/src/music_controller/connections.rs +++ b/src/music_controller/connections.rs @@ -1,5 +1,4 @@ use std::{env, thread, time}; -use discord_presence::{Client, Event}; use super::controller::Controller; From ccbd11175ba54e646e3be73fd4460bd4bf5c9915 Mon Sep 17 00:00:00 2001 From: G2-Games <ke0bhogsg@gmail.com> Date: Tue, 23 Jan 2024 22:21:04 -0600 Subject: [PATCH 084/136] Updated `music_player.rs` to more properly handle its own thread --- src/music_player.rs | 170 +++++++++++++++++++---------------- src/music_storage/library.rs | 11 ++- 2 files changed, 103 insertions(+), 78 deletions(-) diff --git a/src/music_player.rs b/src/music_player.rs index 85563e9..dff9748 100644 --- a/src/music_player.rs +++ b/src/music_player.rs @@ -1,7 +1,7 @@ // Crate things //use crate::music_controller::config::Config; use crate::music_storage::library::URI; -use crossbeam_channel::bounded; +use crossbeam_channel::unbounded; use std::error::Error; use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}; @@ -23,7 +23,7 @@ pub enum PlayerCmd { AboutToFinish, } -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] pub enum PlayerState { Playing, Paused, @@ -76,18 +76,29 @@ pub enum PlayerError { General, } +#[derive(Debug, PartialEq, Eq)] +enum PlaybackStats { + Idle, + Switching, + Playing{ + start: Duration, + end: Duration, + } +} + /// An instance of a music player with a GStreamer backend pub struct Player { - source: Option<URI>, + source: Option<URI>, //pub message_tx: Sender<PlayerCmd>, pub message_rx: crossbeam::channel::Receiver<PlayerCmd>, - playbin: Arc<RwLock<Element>>, - volume: f64, - start: Arc<RwLock<Option<Duration>>>, - end: Arc<RwLock<Option<Duration>>>, - position: Arc<RwLock<Option<Duration>>>, - buffer: Arc<RwLock<Option<u8>>>, - paused: Arc<RwLock<bool>>, + + playback_tx: crossbeam::channel::Sender<PlaybackStats>, + playbin: Arc<RwLock<Element>>, + volume: f64, + start: Option<Duration>, + end: Option<Duration>, + paused: bool, + position: Arc<RwLock<Option<Duration>>>, } impl Player { @@ -118,69 +129,58 @@ impl Player { .build() .ok_or(PlayerError::Build)?; - playbin - .write() - .unwrap() - .set_property_from_value("flags", &flags); - + playbin.write().unwrap().set_property_from_value("flags", &flags); playbin.write().unwrap().set_property("instant-uri", true); let position = Arc::new(RwLock::new(None)); - let start = Arc::new(RwLock::new(None)); - let end: Arc<RwLock<Option<Duration>>> = Arc::new(RwLock::new(None)); - let buffer = Arc::new(RwLock::new(None)); - let paused = Arc::new(RwLock::new(false)); // Set up the thread to monitor the position - let position_update = position.clone(); - let start_update = Arc::clone(&start); - let end_update = Arc::clone(&end); - let (message_tx, message_rx) = bounded(1); //TODO: Maybe figure out a better method than making this bounded - std::thread::spawn(move || { //TODO: Figure out how to return errors nicely in threads + let (playback_tx, playback_rx) = unbounded(); + let (stat_tx, stat_rx) = unbounded::<PlaybackStats>(); + let position_update = Arc::clone(&position); + let _playback_monitor = std::thread::spawn(move || { //TODO: Figure out how to return errors nicely in threads + let mut stats = PlaybackStats::Idle; + let mut pos_temp; loop { - let mut pos_temp = playbin_arc + match stat_rx.recv_timeout(std::time::Duration::from_millis(100)) { + Ok(res) => stats = res, + Err(_) => {} + }; + + pos_temp = playbin_arc .read() .unwrap() .query_position::<ClockTime>() .map(|pos| Duration::nanoseconds(pos.nseconds() as i64)); - if pos_temp.is_some() - && start_update.read().unwrap().is_some() - && end_update.read().unwrap().is_some() - { - let atf = end_update.read().unwrap().unwrap() - Duration::milliseconds(250); - if pos_temp.unwrap() >= end_update.read().unwrap().unwrap() { - let _ = message_tx.try_send(PlayerCmd::Eos); - playbin_arc - .write() - .unwrap() - .set_state(gst::State::Ready) - .expect("Unable to set the pipeline state"); - *start_update.write().unwrap() = None; - *end_update.write().unwrap() = None; - } else if pos_temp.unwrap() >= atf { - let _ = message_tx.try_send(PlayerCmd::AboutToFinish); - } + match stats { + PlaybackStats::Playing{start, end} if pos_temp.is_some() => { + // Check if the current playback position is close to the end + let finish_point = end - Duration::milliseconds(250); + if pos_temp.unwrap() >= end { + let _ = playback_tx.try_send(PlayerCmd::Eos); + playbin_arc + .write() + .unwrap() + .set_state(gst::State::Ready) + .expect("Unable to set the pipeline state"); + } else if pos_temp.unwrap() >= finish_point { + let _ = playback_tx.try_send(PlayerCmd::AboutToFinish); + } - // This has to be done AFTER the current time in the file - // is calculated, or everything else is wrong - if let Some(time) = *start_update.read().unwrap() { - pos_temp = Some(pos_temp.unwrap() - time) + // This has to be done AFTER the current time in the file + // is calculated, or everything else is wrong + pos_temp = Some(pos_temp.unwrap() - start) } + _ => println!("waiting!") } - //println!("{:?}", pos_temp); - *position_update.write().unwrap() = pos_temp; - - std::thread::sleep(std::time::Duration::from_millis(100)); } }); // Set up the thread to monitor bus messages let playbin_bus_ctrl = Arc::clone(&playbin); - let buffer_bus_ctrl = Arc::clone(&buffer); - let paused_bus_ctrl = Arc::clone(&paused); let bus_watch = playbin .read() .unwrap() @@ -203,17 +203,16 @@ impl Player { .set_state(gst::State::Playing) .unwrap(); } + /* TODO: Fix buffering!! gst::MessageView::Buffering(buffering) => { let percent = buffering.percent(); if percent < 100 { - *buffer_bus_ctrl.write().unwrap() = Some(percent as u8); playbin_bus_ctrl .write() .unwrap() .set_state(gst::State::Paused) .unwrap(); - } else if !(*paused_bus_ctrl.read().unwrap()) { - *buffer_bus_ctrl.write().unwrap() = None; + } else if !(buffering) { playbin_bus_ctrl .write() .unwrap() @@ -221,6 +220,7 @@ impl Player { .unwrap(); } } + */ _ => (), } glib::ControlFlow::Continue @@ -237,13 +237,13 @@ impl Player { Ok(Self { source, playbin, - message_rx, + message_rx: playback_rx, + playback_tx: stat_tx, volume: 1.0, - start, - end, - paused, + start: None, + end: None, + paused: false, position, - buffer, }) } @@ -257,6 +257,9 @@ impl Player { /// Set the playback URI fn set_source(&mut self, source: &URI) { + // Make sure the playback tracker knows the stuff is stopped + self.playback_tx.send(PlaybackStats::Switching).unwrap(); + let uri = self.playbin.read().unwrap().property_value("current-uri"); self.source = Some(source.clone()); match source { @@ -267,12 +270,17 @@ impl Player { .set_property("uri", source.as_uri()); // Set the start and end positions of the CUE file - *self.start.write().unwrap() = Some(Duration::from_std(*start).unwrap()); - *self.end.write().unwrap() = Some(Duration::from_std(*end).unwrap()); + self.start = Some(Duration::from_std(*start).unwrap()); + self.end = Some(Duration::from_std(*end).unwrap()); - self.play().unwrap(); + // Send the updated position to the tracker + self.playback_tx.send(PlaybackStats::Playing{ + start: self.start.unwrap(), + end: self.end.unwrap() + }).unwrap(); // Wait for it to be ready, and then move to the proper position + self.play().unwrap(); let now = std::time::Instant::now(); while now.elapsed() < std::time::Duration::from_millis(20) { if self.seek_to(Duration::from_std(*start).unwrap()).is_ok() { @@ -297,8 +305,14 @@ impl Player { std::thread::sleep(std::time::Duration::from_millis(10)); } - *self.start.write().unwrap() = Some(Duration::seconds(0)); - *self.end.write().unwrap() = self.raw_duration(); + self.start = Some(Duration::seconds(0)); + self.end = self.raw_duration(); + + // Send the updated position to the tracker + self.playback_tx.send(PlaybackStats::Playing{ + start: self.start.unwrap(), + end: self.end.unwrap() + }).unwrap(); } } } @@ -361,13 +375,13 @@ impl Player { /// Pause, if playing pub fn pause(&mut self) -> Result<(), gst::StateChangeError> { - *self.paused.write().unwrap() = true; + //*self.paused.write().unwrap() = true; self.set_state(gst::State::Paused) } /// Resume from being paused pub fn resume(&mut self) -> Result<(), gst::StateChangeError> { - *self.paused.write().unwrap() = false; + //*self.paused.write().unwrap() = false; self.set_state(gst::State::Playing) } @@ -383,8 +397,8 @@ impl Player { /// Get the duration of the currently playing track pub fn duration(&mut self) -> Option<Duration> { - if self.end.read().unwrap().is_some() && self.start.read().unwrap().is_some() { - Some(self.end.read().unwrap().unwrap() - self.start.read().unwrap().unwrap()) + if self.end.is_some() && self.start.is_some() { + Some(self.end.unwrap() - self.start.unwrap()) } else { self.raw_duration() } @@ -411,16 +425,16 @@ impl Player { /// Seek absolutely pub fn seek_to(&mut self, target_pos: Duration) -> Result<(), Box<dyn Error>> { - let start = if self.start.read().unwrap().is_none() { + let start = if self.start.is_none() { return Err("Failed to seek: No START time".into()); } else { - self.start.read().unwrap().unwrap() + self.start.unwrap() }; - let end = if self.end.read().unwrap().is_none() { + let end = if self.end.is_none() { return Err("Failed to seek: No END time".into()); } else { - self.end.read().unwrap().unwrap() + self.end.unwrap() }; let adjusted_target = target_pos + start; @@ -439,10 +453,13 @@ impl Player { /// Get the current state of the playback pub fn state(&mut self) -> PlayerState { + self.playbin().unwrap().current_state().into() + /* match *self.buffer.read().unwrap() { None => self.playbin().unwrap().current_state().into(), Some(value) => PlayerState::Buffering(value), } + */ } pub fn property(&self, property: &str) -> glib::Value { @@ -454,10 +471,13 @@ impl Player { self.pause()?; self.ready()?; + // Send the updated position to the tracker + self.playback_tx.send(PlaybackStats::Idle).unwrap(); + // Set all positions to none *self.position.write().unwrap() = None; - *self.start.write().unwrap() = None; - *self.end.write().unwrap() = None; + self.start = None; + self.end = None; Ok(()) } } diff --git a/src/music_storage/library.rs b/src/music_storage/library.rs index 444532f..bf7620e 100644 --- a/src/music_storage/library.rs +++ b/src/music_storage/library.rs @@ -374,7 +374,7 @@ impl MusicLibrary { } /// Serializes the database out to the file specified in the config - pub fn save(&self, config: &Config) -> Result<(), Box<dyn Error>> { + pub fn save(&self, config: Config) -> Result<(), Box<dyn Error>> { let path = config.libraries.get_library(&self.uuid)?.path; match path.try_exists() { Ok(_) => write_file(self, path)?, @@ -389,6 +389,11 @@ impl MusicLibrary { self.library.len() } + /// Returns the library size in number of albums + pub fn len_albums(&self) -> usize { + self.albums().len() + } + /// Queries for a [Song] by its [URI], returning a single `Song` /// with the `URI` that matches along with its position in the library pub fn query_uri(&self, path: &URI) -> Option<(&Song, usize)> { @@ -641,7 +646,7 @@ impl MusicLibrary { } let duration = match next_track.next() { - Some(future) => match future.indices.get(0) { + Some(future) => match future.indices.first() { Some(val) => val.1 - start, None => Duration::from_secs(0), }, @@ -923,7 +928,7 @@ 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 album_art = result.album_art.first(); let new_album = Album { title, From 04c7ddb87698052fce8b959a654f48a6613a015a Mon Sep 17 00:00:00 2001 From: G2-Games <ke0bhogsg@gmail.com> Date: Wed, 24 Jan 2024 02:47:57 -0600 Subject: [PATCH 085/136] Cleaned up code, updated crates --- Cargo.toml | 6 +++--- src/music_player.rs | 24 ++++++++++++++++-------- src/music_storage/utils.rs | 4 ++-- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9c8d51a..0267611 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,12 +13,11 @@ categories = [] [dependencies] file-format = { version = "0.23.0", features = ["reader-asf", "reader-ebml", "reader-mp4", "reader-rm", "reader-txt", "reader-xml", "serde"] } -lofty = "0.18.0" +lofty = "0.18.2" serde = { version = "1.0.195", features = ["derive"] } walkdir = "2.4.0" chrono = { version = "0.4.31", features = ["serde"] } bincode = { version = "2.0.0-rc.3", features = ["serde"] } -unidecode = "0.3.0" rayon = "1.8.0" log = "0.4" base64 = "0.21.5" @@ -35,4 +34,5 @@ m3u8-rs = "5.0.5" thiserror = "1.0.56" font = "0.27.0" uuid = { version = "1.6.1", features = ["v4", "serde"]} -serde_json = "1.0.111" \ No newline at end of file +serde_json = "1.0.111" +deunicode = "1.4.2" diff --git a/src/music_player.rs b/src/music_player.rs index dff9748..4e7de23 100644 --- a/src/music_player.rs +++ b/src/music_player.rs @@ -83,7 +83,8 @@ enum PlaybackStats { Playing{ start: Duration, end: Duration, - } + }, + Finished // When this is sent, the thread will die! } /// An instance of a music player with a GStreamer backend @@ -142,10 +143,10 @@ impl Player { let mut stats = PlaybackStats::Idle; let mut pos_temp; loop { - match stat_rx.recv_timeout(std::time::Duration::from_millis(100)) { - Ok(res) => stats = res, - Err(_) => {} - }; + // Check for new messages or updates about how to proceed + if let Ok(res) = stat_rx.recv_timeout(std::time::Duration::from_millis(100)) { + stats = res + } pos_temp = playbin_arc .read() @@ -171,8 +172,13 @@ impl Player { // This has to be done AFTER the current time in the file // is calculated, or everything else is wrong pos_temp = Some(pos_temp.unwrap() - start) - } - _ => println!("waiting!") + }, + PlaybackStats::Finished => { + *position_update.write().unwrap() = None; + break + }, + PlaybackStats::Idle | PlaybackStats::Switching => println!("waiting!"), + _ => () } *position_update.write().unwrap() = pos_temp; @@ -483,11 +489,13 @@ impl Player { } impl Drop for Player { - /// Cleans up `GStreamer` pipeline when `Backend` is dropped. + /// Cleans up the `GStreamer` pipeline and the monitoring + /// thread when [Player] is dropped. fn drop(&mut self) { self.playbin_mut() .unwrap() .set_state(gst::State::Null) .expect("Unable to set the pipeline to the `Null` state"); + let _ = self.playback_tx.send(PlaybackStats::Finished); } } diff --git a/src/music_storage/utils.rs b/src/music_storage/utils.rs index a14dbe1..0d6fb47 100644 --- a/src/music_storage/utils.rs +++ b/src/music_storage/utils.rs @@ -6,13 +6,13 @@ use std::error::Error; use walkdir::WalkDir; use file_format::{FileFormat, Kind}; use snap; -use unidecode::unidecode; +use deunicode::deunicode_with_tofu; use super::library::{AlbumArt, URI}; pub(super) fn normalize(input_string: &str) -> String { // Normalize the string to latin characters... this needs a lot of work - let mut normalized = unidecode(input_string); + let mut normalized = deunicode_with_tofu(input_string, " "); // Remove non alphanumeric characters normalized.retain(|c| c.is_alphanumeric()); From 9bb1ed7ab5f2de959b2f186b8b1c9c4a3f2c9bd3 Mon Sep 17 00:00:00 2001 From: MrDulfin <Dulfinaminator@gmail.com> Date: Sun, 4 Feb 2024 03:47:02 -0500 Subject: [PATCH 086/136] made 2 tiny changes --- src/config/config.rs | 2 +- src/music_storage/playlist.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/config/config.rs b/src/config/config.rs index bc13836..283d9c6 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -172,7 +172,7 @@ fn test2() { let uuid = config.libraries.get_default().unwrap().uuid.clone(); let mut lib = MusicLibrary::init(Arc::new(RwLock::from(config.clone())), uuid).unwrap(); lib.scan_folder("test-config/music/").unwrap(); - lib.save(&config).unwrap(); + lib.save(config.clone()).unwrap(); dbg!(&lib); dbg!(&config); } diff --git a/src/music_storage/playlist.rs b/src/music_storage/playlist.rs index a35e9c1..730af56 100644 --- a/src/music_storage/playlist.rs +++ b/src/music_storage/playlist.rs @@ -104,7 +104,7 @@ impl<'a> Playlist<'a> { .unwrap(); m3u8.write_to(&mut file).unwrap(); } - pub fn from_file(file: std::fs::File) -> Playlist<'a> { + pub fn from_m3u8(file: std::fs::File) -> Playlist<'a> { todo!() } } From 0a68a12546b71618424427694153acbd9af6c8ed Mon Sep 17 00:00:00 2001 From: MrDulfin <Dulfinaminator@gmail.com> Date: Tue, 6 Feb 2024 20:36:31 -0500 Subject: [PATCH 087/136] moved Song creation functions to the `Song` struct --- src/config/config.rs | 6 +- src/music_storage/library.rs | 444 ++++++++++++++++++---------------- src/music_storage/playlist.rs | 83 ++++--- 3 files changed, 285 insertions(+), 248 deletions(-) diff --git a/src/config/config.rs b/src/config/config.rs index 283d9c6..e3f021f 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -137,7 +137,11 @@ pub enum ConfigError { #[error("No Library Found for {0}!")] NoConfigLibrary(Uuid), #[error("There is no Default Library for this Config")] - NoDefaultLibrary + NoDefaultLibrary, + //TODO: do something about playlists + #[error("Please provide a better m3u8 Playlist")] + BadPlaylist, + } diff --git a/src/music_storage/library.rs b/src/music_storage/library.rs index bf7620e..6b216e3 100644 --- a/src/music_storage/library.rs +++ b/src/music_storage/library.rs @@ -175,6 +175,225 @@ impl Song { pub fn remove_tag(&mut self, target_key: &Tag) { self.tags.remove(target_key); } + + /// Creates a `Song` from a song file + pub fn from_file(target_file: &Path) -> Result<Self, Box<dyn Error>> { + let normal_options = ParseOptions::new().parsing_mode(lofty::ParsingMode::Relaxed); + + let blank_tag = &lofty::Tag::new(TagType::Id3v2); + let tagged_file: lofty::TaggedFile; + let mut duration = Duration::from_secs(0); + let tag = match Probe::open(target_file)?.options(normal_options).read() { + Ok(file) => { + tagged_file = file; + + duration = tagged_file.properties().duration(); + + // Ensure the tags exist, if not, insert blank data + match tagged_file.primary_tag() { + Some(primary_tag) => primary_tag, + + None => match tagged_file.first_tag() { + Some(first_tag) => first_tag, + None => blank_tag, + }, + } + } + + Err(_) => blank_tag, + }; + + let mut tags: BTreeMap<Tag, String> = BTreeMap::new(); + for item in tag.items() { + let key = match item.key() { + 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" || unknown == "Acoustid Fingerprint" => + { + continue + } + ItemKey::Unknown(unknown) => Tag::Key(unknown.to_string()), + custom => Tag::Key(format!("{:?}", custom)), + }; + + let value = match item.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 from the file + let mut album_art: Vec<AlbumArt> = Vec::new(); + for (i, _art) in tag.pictures().iter().enumerate() { + let new_art = AlbumArt::Embedded(i); + + album_art.push(new_art) + } + + // Find images around the music file that can be used + let mut found_images = find_images(target_file).unwrap(); + album_art.append(&mut found_images); + + // Get the format as a string + let format: Option<FileFormat> = match FileFormat::from_file(target_file) { + Ok(fmt) => Some(fmt), + Err(_) => None, + }; + + // TODO: Fix error handling + let binding = fs::canonicalize(target_file).unwrap(); + + let new_song = Song { + location: URI::Local(binding), + 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, + }; + Ok(new_song) + } + + /// creates a `Vec<Song>` from a cue file + + pub fn from_cue(cuesheet: &Path) -> Result<(Vec<(Self, &PathBuf)>), Box<dyn Error>> { + let mut tracks = Vec::new(); + + let cue_data = parse_from_file(&cuesheet.to_string_lossy(), false).unwrap(); + + // Get album level information + 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 file in cue_data.files.iter() { + let audio_location = &parent_dir.join(file.file.clone()); + + if !audio_location.exists() { + continue; + } + + 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; + if track.indices.len() > 1 { + start = track.indices[1].1; + } else { + start = track.indices[0].1; + } + if !start.is_zero() { + start -= pregap; + } + + let duration = match next_track.next() { + Some(future) => match future.indices.first() { + Some(val) => val.1 - start, + None => Duration::from_secs(0), + }, + None => match lofty::read_from_path(audio_location) { + Ok(tagged_file) => tagged_file.properties().duration() - start, + + Err(_) => match Probe::open(audio_location)?.read() { + Ok(tagged_file) => tagged_file.properties().duration() - start, + + Err(_) => Duration::from_secs(0), + }, + }, + }; + let end = start + duration + postgap; + + // Get the format as a string + let format: Option<FileFormat> = match FileFormat::from_file(audio_location) { + Ok(fmt) => Some(fmt), + Err(_) => None, + }; + + // Get some useful tags + let mut tags: BTreeMap<Tag, String> = BTreeMap::new(); + match album_title { + Some(title) => { + tags.insert(Tag::Album, title.clone()); + } + None => (), + } + match album_artist { + Some(artist) => { + tags.insert(Tag::Artist, 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, + }; + tracks.push((new_song, audio_location)); + } + } + Ok((tracks)) + } } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] @@ -499,97 +718,8 @@ impl MusicLibrary { } pub fn add_file(&mut self, target_file: &Path) -> Result<(), Box<dyn Error>> { - let normal_options = ParseOptions::new().parsing_mode(lofty::ParsingMode::Relaxed); - - let blank_tag = &lofty::Tag::new(TagType::Id3v2); - let tagged_file: lofty::TaggedFile; - let mut duration = Duration::from_secs(0); - let tag = match Probe::open(target_file)?.options(normal_options).read() { - Ok(file) => { - tagged_file = file; - - duration = tagged_file.properties().duration(); - - // Ensure the tags exist, if not, insert blank data - match tagged_file.primary_tag() { - Some(primary_tag) => primary_tag, - - None => match tagged_file.first_tag() { - Some(first_tag) => first_tag, - None => blank_tag, - }, - } - } - - Err(_) => blank_tag, - }; - - let mut tags: BTreeMap<Tag, String> = BTreeMap::new(); - for item in tag.items() { - let key = match item.key() { - 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" || unknown == "Acoustid Fingerprint" => - { - continue - } - ItemKey::Unknown(unknown) => Tag::Key(unknown.to_string()), - custom => Tag::Key(format!("{:?}", custom)), - }; - - let value = match item.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 from the file - let mut album_art: Vec<AlbumArt> = Vec::new(); - for (i, _art) in tag.pictures().iter().enumerate() { - let new_art = AlbumArt::Embedded(i); - - album_art.push(new_art) - } - - // Find images around the music file that can be used - let mut found_images = find_images(target_file).unwrap(); - album_art.append(&mut found_images); - - // Get the format as a string - let format: Option<FileFormat> = match FileFormat::from_file(target_file) { - Ok(fmt) => Some(fmt), - Err(_) => None, - }; - - // TODO: Fix error handling - let binding = fs::canonicalize(target_file).unwrap(); - - let new_song = Song { - location: URI::Local(binding), - 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, - }; + let new_song = Song::from_file(target_file)?; match self.add_song(new_song) { Ok(_) => (), Err(_) => { @@ -601,137 +731,23 @@ impl MusicLibrary { } pub fn add_cuesheet(&mut self, cuesheet: &Path) -> Result<i32, Box<dyn Error>> { - let mut tracks_added = 0; + let tracks = Song::from_cue(cuesheet)?; + let mut tracks_added = tracks.len() as i32; - let cue_data = parse_from_file(&cuesheet.to_string_lossy(), false).unwrap(); - - // Get album level information - 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 file in cue_data.files.iter() { - let audio_location = &parent_dir.join(file.file.clone()); - - if !audio_location.exists() { - continue; - } + for (new_song, location) in tracks { // Try to remove the original audio file from the db if it exists - if self.remove_uri(&URI::Local(audio_location.clone())).is_ok() { + if self.remove_uri(&URI::Local(location.clone())).is_ok() { tracks_added -= 1 } - - 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; - if track.indices.len() > 1 { - start = track.indices[1].1; - } else { - start = track.indices[0].1; + match self.add_song(new_song) { + Ok(_) => {}, + Err(_error) => { + //println!("{}", _error); + continue; } - if !start.is_zero() { - start -= pregap; - } - - let duration = match next_track.next() { - Some(future) => match future.indices.first() { - Some(val) => val.1 - start, - None => Duration::from_secs(0), - }, - None => match lofty::read_from_path(audio_location) { - Ok(tagged_file) => tagged_file.properties().duration() - start, - - Err(_) => match Probe::open(audio_location)?.read() { - Ok(tagged_file) => tagged_file.properties().duration() - start, - - Err(_) => Duration::from_secs(0), - }, - }, - }; - let end = start + duration + postgap; - - // Get the format as a string - let format: Option<FileFormat> = match FileFormat::from_file(audio_location) { - Ok(fmt) => Some(fmt), - Err(_) => None, - }; - - // Get some useful tags - let mut tags: BTreeMap<Tag, String> = BTreeMap::new(); - match album_title { - Some(title) => { - tags.insert(Tag::Album, title.clone()); - } - None => (), - } - match album_artist { - Some(artist) => { - tags.insert(Tag::Artist, 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) } diff --git a/src/music_storage/playlist.rs b/src/music_storage/playlist.rs index 730af56..c9ad352 100644 --- a/src/music_storage/playlist.rs +++ b/src/music_storage/playlist.rs @@ -1,7 +1,9 @@ -use std::path::Path; +use std::{fs::File, path::{Path, PathBuf}, io::{Read, Error}}; +use bincode::config; use chrono::Duration; -use walkdir::Error; +use uuid::Uuid; +// use walkdir::Error; use super::{ library::{AlbumArt, Song, Tag}, @@ -11,14 +13,14 @@ use super::{ }, }; -use m3u8_rs::{MediaPlaylist, MediaPlaylistType, MediaSegment}; -// use nom::IResult; +use m3u8_rs::{MediaPlaylist, MediaPlaylistType, MediaSegment, Playlist as List2, MasterPlaylist}; + #[derive(Debug, Clone)] pub struct Playlist<'a> { title: String, cover: Option<&'a AlbumArt>, - tracks: Vec<Song>, + tracks: Vec<Uuid>, play_count: i32, play_time: Duration, } @@ -32,43 +34,37 @@ impl<'a> Playlist<'a> { pub fn play_time(&self) -> chrono::Duration { self.play_time } - pub fn set_tracks(&mut self, songs: Vec<Song>) -> Result<(), Error> { + pub fn set_tracks(&mut self, tracks: Vec<Uuid>) { self.tracks = songs; - Ok(()) } - pub fn add_track(&mut self, song: Song) -> Result<(), Error> { + pub fn add_track(&mut self, track: Uuid) -> Result<(), Error> { self.tracks.push(song); Ok(()) } pub fn remove_track(&mut self, index: i32) -> Result<(), Error> { - let bun: usize = index as usize; - let mut name = String::new(); - if self.tracks.len() >= bun { - name = String::from(self.tracks[bun].tags.get_key_value(&Tag::Title).unwrap().1); - self.tracks.remove(bun); + let index = index as usize; + if (self.tracks.len() - 1) >= index { + self.tracks.remove(index); } - dbg!(name); Ok(()) } - pub fn get_index(&self, song_name: &str) -> Option<usize> { - let mut index = 0; - if self.contains_value(&Tag::Title, song_name) { - for track in &self.tracks { - index += 1; - if song_name == track.tags.get_key_value(&Tag::Title).unwrap().1 { - dbg!("Index gotted! ", index); - return Some(index); - } - } - } - None - } + // pub fn get_index(&self, song_name: &str) -> Option<usize> { + // let mut index = 0; + // if self.contains_value(&Tag::Title, song_name) { + // for track in &self.tracks { + // index += 1; + // if song_name == track.tags.get_key_value(&Tag::Title).unwrap().1 { + // dbg!("Index gotted! ", index); + // return Some(index); + // } + // } + // } + // None + // } pub fn contains_value(&self, tag: &Tag, value: &str) -> bool { - for track in &self.tracks { - if value == track.tags.get_key_value(tag).unwrap().1 { - return true; - } - } + &self.tracks.iter().for_each(|track| { + + }); false } pub fn to_m3u8(&mut self) { @@ -104,7 +100,28 @@ impl<'a> Playlist<'a> { .unwrap(); m3u8.write_to(&mut file).unwrap(); } - pub fn from_m3u8(file: std::fs::File) -> Playlist<'a> { + pub fn from_m3u8(path: &str) -> Result<Playlist<'a>, Error> { + let mut file = match File::open(path) { + Ok(file) => file, + Err(e) => return Err(e), + }; + let mut bytes = Vec::new(); + file.read_to_end(&mut bytes).unwrap(); + + let parsed = m3u8_rs::parse_playlist(&bytes); + + let playlist = match parsed { + Result::Ok((i, playlist)) => playlist, + Result::Err(e) => panic!("Parsing error: \n{}", e), + }; + + match playlist { + List2::MasterPlaylist(_) => panic!(), + List2::MediaPlaylist(pl) => { + let values = pl.segments.iter().map(|seg| seg.uri.to_owned() ).collect::<Vec<String>>(); + } + } + todo!() } } From 4a7a6096d644f66e0673ed48a78ec85928f7e0ab Mon Sep 17 00:00:00 2001 From: MrDulfin <Dulfinaminator@gmail.com> Date: Fri, 9 Feb 2024 23:52:24 -0500 Subject: [PATCH 088/136] addded small changes --- src/config/config.rs | 2 +- src/music_storage/library.rs | 6 ++-- src/music_storage/playlist.rs | 61 +++++++++++++++++++++-------------- 3 files changed, 41 insertions(+), 28 deletions(-) diff --git a/src/config/config.rs b/src/config/config.rs index e3f021f..9314d65 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -92,6 +92,7 @@ impl ConfigLibraries { #[derive(Debug, Default, Serialize, Deserialize, Clone)] pub struct Config { pub path: PathBuf, + pub backup_folder: Option<PathBuf>, pub libraries: ConfigLibraries, pub volume: f32, } @@ -144,7 +145,6 @@ pub enum ConfigError { } - #[test] fn config_test() { let lib_a = ConfigLibrary::new(PathBuf::from("test-config/library1"), String::from("library1"), None); diff --git a/src/music_storage/library.rs b/src/music_storage/library.rs index 6b216e3..69acb27 100644 --- a/src/music_storage/library.rs +++ b/src/music_storage/library.rs @@ -6,6 +6,7 @@ use crate::config::config::Config; use std::collections::BTreeMap; use std::error::Error; use std::ops::ControlFlow::{Break, Continue}; +use std::ops::Deref; // Files use file_format::{FileFormat, Kind}; @@ -273,7 +274,7 @@ impl Song { /// creates a `Vec<Song>` from a cue file - pub fn from_cue(cuesheet: &Path) -> Result<(Vec<(Self, &PathBuf)>), Box<dyn Error>> { + pub fn from_cue(cuesheet: &Path) -> Result<(Vec<(Self, PathBuf)>), Box<dyn Error>> { let mut tracks = Vec::new(); let cue_data = parse_from_file(&cuesheet.to_string_lossy(), false).unwrap(); @@ -286,6 +287,7 @@ impl Song { for file in cue_data.files.iter() { let audio_location = &parent_dir.join(file.file.clone()); + if !audio_location.exists() { continue; } @@ -389,7 +391,7 @@ impl Song { tags, album_art, }; - tracks.push((new_song, audio_location)); + tracks.push((new_song, audio_location.clone())); } } Ok((tracks)) diff --git a/src/music_storage/playlist.rs b/src/music_storage/playlist.rs index c9ad352..c2c6b74 100644 --- a/src/music_storage/playlist.rs +++ b/src/music_storage/playlist.rs @@ -5,6 +5,8 @@ use chrono::Duration; use uuid::Uuid; // use walkdir::Error; +use crate::music_controller::controller::Controller; + use super::{ library::{AlbumArt, Song, Tag}, music_collection::MusicCollection, db_reader::{ @@ -15,12 +17,17 @@ use super::{ use m3u8_rs::{MediaPlaylist, MediaPlaylistType, MediaSegment, Playlist as List2, MasterPlaylist}; - +#[derive(Debug, Clone)] +pub enum SortOrder { + Manual, + Tag(Tag) +} #[derive(Debug, Clone)] pub struct Playlist<'a> { title: String, cover: Option<&'a AlbumArt>, tracks: Vec<Uuid>, + sort_order: SortOrder, play_count: i32, play_time: Duration, } @@ -35,10 +42,10 @@ impl<'a> Playlist<'a> { self.play_time } pub fn set_tracks(&mut self, tracks: Vec<Uuid>) { - self.tracks = songs; + self.tracks = tracks; } pub fn add_track(&mut self, track: Uuid) -> Result<(), Error> { - self.tracks.push(song); + self.tracks.push(track); Ok(()) } pub fn remove_track(&mut self, index: i32) -> Result<(), Error> { @@ -67,16 +74,18 @@ impl<'a> Playlist<'a> { }); false } - pub fn to_m3u8(&mut self) { - let seg = &self - .tracks + pub fn to_m3u8(&mut self, tracks: Vec<Song>) { + let seg = tracks .iter() .map({ - |track| MediaSegment { - uri: track.location.to_string().into(), - duration: track.duration.as_millis() as f32, - title: Some(track.tags.get_key_value(&Tag::Title).unwrap().1.into()), - ..Default::default() + |track| { + + MediaSegment { + uri: track.location.to_string().into(), + duration: track.duration.as_millis() as f32, + title: Some(track.tags.get_key_value(&Tag::Title).unwrap().1.into()), + ..Default::default() + } } }) .collect::<Vec<MediaSegment>>(); @@ -124,8 +133,6 @@ impl<'a> Playlist<'a> { todo!() } -} -impl MusicCollection for Playlist<'_> { fn title(&self) -> &String { &self.title } @@ -135,30 +142,34 @@ impl MusicCollection for Playlist<'_> { None => None, } } - fn tracks(&self) -> Vec<Song> { + fn tracks(&self) -> Vec<Uuid> { self.tracks.to_owned() } } + + + impl Default for Playlist<'_> { fn default() -> Self { Playlist { title: String::default(), cover: None, tracks: Vec::default(), + sort_order: SortOrder::Manual, play_count: 0, play_time: Duration::zero(), } } } -#[test] -fn list_to_m3u8() { - let lib = ITunesLibrary::from_file(Path::new( - "F:\\Music\\Mp3\\Music Main\\iTunes Music Library.xml", - )); - let mut a = Playlist::new(); - let c = lib.to_songs(); - let mut b = c.iter().map(|song| song.to_owned()).collect::<Vec<Song>>(); - a.tracks.append(&mut b); - a.to_m3u8() -} +// #[test] +// fn list_to_m3u8() { +// let lib = ITunesLibrary::from_file(Path::new( +// "F:\\Music\\Mp3\\Music Main\\iTunes Music Library.xml", +// )); +// let mut a = Playlist::new(); +// let c = lib.to_songs(); +// let mut b = c.iter().map(|song| song.to_owned()).collect::<Vec<Song>>(); +// a.tracks.append(&mut b); +// a.to_m3u8() +// } From 96069fd9bc912d4e74bb073eec145a1d1af68d71 Mon Sep 17 00:00:00 2001 From: G2-Games <ke0bhogsg@gmail.com> Date: Fri, 9 Feb 2024 23:23:08 -0600 Subject: [PATCH 089/136] Added `.exists()` for `URI` --- src/music_storage/library.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/music_storage/library.rs b/src/music_storage/library.rs index 69acb27..502429b 100644 --- a/src/music_storage/library.rs +++ b/src/music_storage/library.rs @@ -460,6 +460,14 @@ impl URI { }; path_str.to_string() } + + pub fn exists(&self) -> Result<bool, std::io::Error> { + match self { + URI::Local(loc) => loc.try_exists(), + URI::Cue {location, ..} => location.try_exists(), + URI::Remote(_, _loc) => todo!(), + } + } } impl ToString for URI { @@ -699,7 +707,7 @@ impl MusicLibrary { Ok(_) => total += 1, Err(_error) => { errors += 1; - println!("{}, {:?}: {}", format, target_file.file_name(), _error) + //println!("{}, {:?}: {}", format, target_file.file_name(), _error) } // TODO: Handle more of these errors }; } else if extension == "cue" { @@ -707,15 +715,13 @@ impl MusicLibrary { Ok(added) => added, Err(error) => { errors += 1; - println!("{}", error); + //println!("{}", error); 0 } } } } - println!("ERRORS: {}", errors); - Ok(total) } From 3ad8b78e9dc760ca11eb0001a0347272ceb13c12 Mon Sep 17 00:00:00 2001 From: MrDulfin <Dulfinaminator@gmail.com> Date: Sun, 11 Feb 2024 15:54:11 -0500 Subject: [PATCH 090/136] started work on controller protoype --- src/music_controller/controller.rs | 206 ++++++++++++++++++- src/music_storage/db_reader/itunes/reader.rs | 21 +- src/music_storage/playlist.rs | 12 +- 3 files changed, 226 insertions(+), 13 deletions(-) diff --git a/src/music_controller/controller.rs b/src/music_controller/controller.rs index 7f62871..98c3ca8 100644 --- a/src/music_controller/controller.rs +++ b/src/music_controller/controller.rs @@ -2,25 +2,219 @@ //! player. It manages queues, playback, library access, and //! other functions +use std::path::PathBuf; use std::sync::{Arc, RwLock}; +use std::time::Duration; +use crossbeam_channel::{Sender, Receiver}; +// use std::sync::mpsc; +use crossbeam_channel; +use gstreamer::format::Default; +use gstreamer::query::Uri; +use std::thread::{self, sleep, spawn}; +use std::error::Error; +use crossbeam_channel::unbounded; +use rayon::iter::Rev; +use uuid::Uuid; + +use crate::config; +use crate::music_storage::library::Tag; +use crate::music_storage::playlist::Playlist; use crate::{ music_player::Player, - music_storage::library::Song, - config::config::Config + music_storage::{ + library::{MusicLibrary, Song} + }, + config::config::Config, }; struct Queue { player: Player, name: String, - songs: Vec<Song>, + songs: Playlist, +} +impl Queue { + fn new() -> Result<Self, Box<dyn Error>> { + Ok( + Queue { + player: Player::new()?, + name: String::new(), + songs: Playlist::new() + } + ) + } } pub struct Controller { - queues: Vec<Queue>, + // queues: Vec<Queue>, config: Arc<RwLock<Config>>, + // library: MusicLibrary, + controller_mail: MailMan<ControllerCommand, ControllerResponse>, + db_mail: MailMan<DatabaseCommand, DatabaseResponse>, + queue_mail: Vec<MailMan<QueueCommand, QueueResponse>>, +} +#[derive(Debug)] + +pub enum ControllerCommand { + Default, + Test +} +#[derive(Debug)] + +enum ControllerResponse { + Empty, + QueueMailMan(MailMan<QueueCommand, QueueResponse>), + +} +#[derive(Debug)] + +pub enum DatabaseCommand { + Default, + Test, + GetSongs, + +} +#[derive(Debug)] + +enum DatabaseResponse { + Empty, + Songs(Vec<Song>), +} +#[derive(Debug)] +enum QueueCommand { + Default, + Test, + Play, + Pause, +} +#[derive(Debug)] +enum QueueResponse { + Default, + Test, } -impl Controller { - // more stuff to come +#[derive(Debug)] + +struct MailMan<T, U> { + pub tx: Sender<T>, + rx: Receiver<U> } +impl<T> MailMan<T, T> { + pub fn new() -> Self { + let (tx, rx) = unbounded::<T>(); + MailMan { tx, rx } + } +} +impl<T, U> MailMan<T, U> { + pub fn double() -> (MailMan<T, U>, MailMan<U, T>) { + let (tx, rx) = unbounded::<T>(); + let (tx1, rx1) = unbounded::<U>(); + + ( + MailMan { tx, rx: rx1 }, + MailMan { tx: tx1, rx } + ) + } + + pub fn send(&self, mail: T) -> Result<(), Box<dyn Error>> { + &self.tx.send(mail).unwrap(); + Ok(()) + } + + pub fn recv(&self) -> Result<U, Box<dyn Error>> { + let u = self.rx.recv().unwrap(); + Ok(u) + } +} + +#[allow(unused_variables)] +impl Controller { + pub fn start(config: PathBuf) -> Result<Self, Box<dyn Error>> { + let config = Config::read_file(config)?; + let uuid = config.libraries.get_default()?.uuid; + + let config = Arc::new(RwLock::from(config)); + let lib = MusicLibrary::init(config.clone(), uuid)?; + + let (out_thread_controller, in_thread) = MailMan::double(); + let monitor_thread = spawn(move || { + use ControllerCommand::*; + loop { + let command = in_thread.recv().unwrap(); + + match command { + Default => (), + Test => { + in_thread.send(ControllerResponse::Empty).unwrap(); + }, + } + } + }); + + + let (out_thread_db, in_thread) = MailMan::double(); + let db_monitor = spawn(move || { + use DatabaseCommand::*; + loop { + let command = in_thread.recv().unwrap(); + + match command { + Default => {}, + Test => { + in_thread.send(DatabaseResponse::Empty).unwrap(); + }, + GetSongs => { + let songs = lib.query_tracks(&String::from(""), &(vec![Tag::Title]), &(vec![Tag::Title])).unwrap().iter().cloned().cloned().collect(); + in_thread.send(DatabaseResponse::Songs(songs)).unwrap(); + }, + + } + } + }); + + + + Ok( + Controller { + // queues: Vec::new(), + config, + controller_mail: out_thread_controller, + db_mail: out_thread_db, + queue_mail: Vec::new(), + } + ) + } + fn get_db_songs(&self) -> Vec<Song> { + self.db_mail.send(DatabaseCommand::GetSongs); + match self.db_mail.recv().unwrap() { + DatabaseResponse::Songs(songs) => songs, + _ => Vec::new() + } + + } + pub fn new_queue(&mut self) { + let (out_thread_queue, in_thread) = MailMan::<QueueCommand, QueueResponse>::double(); + let queues_monitor = spawn(move || { + use QueueCommand::*; + loop { + let command = in_thread.recv().unwrap(); + match command { + Default => {}, + Test => {}, + Play => {}, + Pause => {}, + } + } + }); + self.queue_mail.push(out_thread_queue); + } +} + +#[test] +fn name() { + let a = Controller::start(PathBuf::from("test-config/config_test.json")).unwrap(); + // sleep(Duration::from_millis(5000)); + _ = a.controller_mail.send(ControllerCommand::Test); + // dbg!(a.get_db_songs()); + // sleep(Duration::from_secs(6)); +} \ No newline at end of file diff --git a/src/music_storage/db_reader/itunes/reader.rs b/src/music_storage/db_reader/itunes/reader.rs index c2063c0..3bc73f6 100644 --- a/src/music_storage/db_reader/itunes/reader.rs +++ b/src/music_storage/db_reader/itunes/reader.rs @@ -2,17 +2,20 @@ use file_format::FileFormat; use lofty::{AudioFile, LoftyError, ParseOptions, Probe, TagType, TaggedFileExt}; use quick_xml::events::Event; use quick_xml::reader::Reader; +use uuid::Uuid; use std::collections::{BTreeMap, HashMap}; use std::fs::File; use std::path::{Path, PathBuf}; use std::str::FromStr; +use std::sync::{Arc, RwLock}; use std::time::Duration as StdDur; use std::vec::Vec; use chrono::prelude::*; +use crate::config::config::{Config, ConfigLibrary}; use crate::music_storage::db_reader::extern_library::ExternalLibrary; -use crate::music_storage::library::{AlbumArt, Service, Song, Tag, URI}; +use crate::music_storage::library::{AlbumArt, MusicLibrary, Service, Song, Tag, URI}; use crate::music_storage::utils; use urlencoding::decode; @@ -320,4 +323,20 @@ impl ITunesSong { // println!("{:.2?}", song); Ok(song) } +} + +#[test] +fn itunes_lib_test() { + let mut config = Config::read_file(PathBuf::from("test-config/config_test.json")).unwrap(); + let config_lib = ConfigLibrary::new(PathBuf::from("test-config/library2"), String::from("library2"), None); + config.libraries.libraries.push(config_lib.clone()); + + let songs = ITunesLibrary::from_file(Path::new("test-config\\iTunesLib.xml")).to_songs(); + + let mut library = MusicLibrary::init(Arc::new(RwLock::from(config.clone())), config_lib.uuid).unwrap(); + + songs.iter().for_each(|song| library.add_song(song.to_owned()).unwrap()); + + config.write_file().unwrap(); + library.save(config).unwrap(); } \ No newline at end of file diff --git a/src/music_storage/playlist.rs b/src/music_storage/playlist.rs index c2c6b74..69ae390 100644 --- a/src/music_storage/playlist.rs +++ b/src/music_storage/playlist.rs @@ -23,15 +23,15 @@ pub enum SortOrder { Tag(Tag) } #[derive(Debug, Clone)] -pub struct Playlist<'a> { +pub struct Playlist { title: String, - cover: Option<&'a AlbumArt>, + cover: Option<AlbumArt>, tracks: Vec<Uuid>, sort_order: SortOrder, play_count: i32, play_time: Duration, } -impl<'a> Playlist<'a> { +impl Playlist { pub fn new() -> Self { Default::default() } @@ -109,7 +109,7 @@ impl<'a> Playlist<'a> { .unwrap(); m3u8.write_to(&mut file).unwrap(); } - pub fn from_m3u8(path: &str) -> Result<Playlist<'a>, Error> { + pub fn from_m3u8(path: &str) -> Result<Playlist, Error> { let mut file = match File::open(path) { Ok(file) => file, Err(e) => return Err(e), @@ -137,7 +137,7 @@ impl<'a> Playlist<'a> { &self.title } fn cover(&self) -> Option<&AlbumArt> { - match self.cover { + match &self.cover { Some(e) => Some(e), None => None, } @@ -149,7 +149,7 @@ impl<'a> Playlist<'a> { -impl Default for Playlist<'_> { +impl Default for Playlist { fn default() -> Self { Playlist { title: String::default(), From 599ddc584c6e5041f107c543a06aba82ece6def4 Mon Sep 17 00:00:00 2001 From: MrDulfin <Dulfinaminator@gmail.com> Date: Mon, 12 Feb 2024 13:43:30 -0500 Subject: [PATCH 091/136] Finished Controller Prototype --- .gitignore | 2 + src/config/config.rs | 6 +- src/music_controller/controller.rs | 120 ++++++++++++++++--- src/music_storage/db_reader/foobar/reader.rs | 3 + src/music_storage/db_reader/itunes/reader.rs | 1 + src/music_storage/library.rs | 32 ++++- 6 files changed, 139 insertions(+), 25 deletions(-) diff --git a/.gitignore b/.gitignore index a9e4c0b..7a0594f 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ music_database* *.m3u *.m3u8 *.json +*.zip +*.xml diff --git a/src/config/config.rs b/src/config/config.rs index 9314d65..15b7121 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -71,8 +71,10 @@ impl ConfigLibraries { } pub fn get_library(&self, uuid: &Uuid) -> Result<ConfigLibrary, ConfigError> { + dbg!(&uuid); for library in &self.libraries { if &library.uuid == uuid { + dbg!(&library.uuid); return Ok(library.to_owned()) } } @@ -128,8 +130,8 @@ impl Config { let mut file: File = File::open(path)?; let mut bun: String = String::new(); _ = file.read_to_string(&mut bun); - let ny: Config = serde_json::from_str::<Config>(&bun)?; - Ok(ny) + let config: Config = serde_json::from_str::<Config>(&bun)?; + Ok(config) } } diff --git a/src/music_controller/controller.rs b/src/music_controller/controller.rs index 98c3ca8..da2ea0f 100644 --- a/src/music_controller/controller.rs +++ b/src/music_controller/controller.rs @@ -2,7 +2,7 @@ //! player. It manages queues, playback, library access, and //! other functions -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::{Arc, RwLock}; use std::time::Duration; use crossbeam_channel::{Sender, Receiver}; @@ -18,7 +18,7 @@ use rayon::iter::Rev; use uuid::Uuid; use crate::config; -use crate::music_storage::library::Tag; +use crate::music_storage::library::{Tag, URI}; use crate::music_storage::playlist::Playlist; use crate::{ music_player::Player, @@ -31,7 +31,7 @@ use crate::{ struct Queue { player: Player, name: String, - songs: Playlist, + songs: Vec<Song>, } impl Queue { fn new() -> Result<Self, Box<dyn Error>> { @@ -39,10 +39,15 @@ impl Queue { Queue { player: Player::new()?, name: String::new(), - songs: Playlist::new() + songs: Vec::new() } ) } + fn set_tracks(&mut self, tracks: Vec<Song>) { + let mut tracks = tracks; + self.songs.clear(); + self.songs.append(&mut tracks); + } } pub struct Controller { @@ -54,39 +59,47 @@ pub struct Controller { queue_mail: Vec<MailMan<QueueCommand, QueueResponse>>, } #[derive(Debug)] - pub enum ControllerCommand { Default, Test } -#[derive(Debug)] +#[derive(Debug)] enum ControllerResponse { Empty, QueueMailMan(MailMan<QueueCommand, QueueResponse>), } -#[derive(Debug)] +#[derive(Debug)] pub enum DatabaseCommand { Default, Test, GetSongs, + QueryUuid(Uuid), + QueryUuids(Vec<Uuid>), + ReadFolder(String), } -#[derive(Debug)] +#[derive(Debug)] enum DatabaseResponse { Empty, + Song(Song), Songs(Vec<Song>), } + #[derive(Debug)] enum QueueCommand { Default, Test, Play, Pause, + SetSongs(Vec<Song>), + // SetLocation(URI), + Enqueue(URI), } + #[derive(Debug)] enum QueueResponse { Default, @@ -94,11 +107,11 @@ enum QueueResponse { } #[derive(Debug)] - struct MailMan<T, U> { pub tx: Sender<T>, rx: Receiver<U> } + impl<T> MailMan<T, T> { pub fn new() -> Self { let (tx, rx) = unbounded::<T>(); @@ -129,12 +142,13 @@ impl<T, U> MailMan<T, U> { #[allow(unused_variables)] impl Controller { - pub fn start(config: PathBuf) -> Result<Self, Box<dyn Error>> { - let config = Config::read_file(config)?; + pub fn start(config_path: String) -> Result<Self, Box<dyn Error>> { + let config_path = PathBuf::from(config_path); + let config = Config::read_file(config_path)?; let uuid = config.libraries.get_default()?.uuid; let config = Arc::new(RwLock::from(config)); - let lib = MusicLibrary::init(config.clone(), uuid)?; + let mut lib = MusicLibrary::init(config.clone(), uuid)?; let (out_thread_controller, in_thread) = MailMan::double(); let monitor_thread = spawn(move || { @@ -167,6 +181,26 @@ impl Controller { let songs = lib.query_tracks(&String::from(""), &(vec![Tag::Title]), &(vec![Tag::Title])).unwrap().iter().cloned().cloned().collect(); in_thread.send(DatabaseResponse::Songs(songs)).unwrap(); }, + QueryUuid(uuid) => { + match lib.query_uuid(&uuid) { + Some(song) => in_thread.send(DatabaseResponse::Song(song.0.clone())).unwrap(), + None => in_thread.send(DatabaseResponse::Empty).unwrap(), + } + }, + QueryUuids(uuids) => { + let mut vec = Vec::new(); + for uuid in uuids { + match lib.query_uuid(&uuid) { + Some(song) => vec.push(song.0.clone()), + None => unimplemented!() + } + } + in_thread.send(DatabaseResponse::Songs(vec)).unwrap(); + }, + ReadFolder(folder) => { + lib.scan_folder(&folder).unwrap(); + in_thread.send(DatabaseResponse::Empty).unwrap(); + } } } @@ -184,6 +218,7 @@ impl Controller { } ) } + fn get_db_songs(&self) -> Vec<Song> { self.db_mail.send(DatabaseCommand::GetSongs); match self.db_mail.recv().unwrap() { @@ -192,29 +227,76 @@ impl Controller { } } + pub fn new_queue(&mut self) { let (out_thread_queue, in_thread) = MailMan::<QueueCommand, QueueResponse>::double(); let queues_monitor = spawn(move || { use QueueCommand::*; + let mut queue = Queue::new().unwrap(); loop { let command = in_thread.recv().unwrap(); match command { Default => {}, - Test => {}, - Play => {}, + Test => { in_thread.send(QueueResponse::Test).unwrap() }, + Play => { + queue.player.play().unwrap(); + in_thread.send(QueueResponse::Default).unwrap(); + }, Pause => {}, + SetSongs(songs) => { + queue.set_tracks(songs); + in_thread.send(QueueResponse::Default).unwrap(); + }, + Enqueue(uri) => { + queue.player.enqueue_next(&uri); + } } } }); self.queue_mail.push(out_thread_queue); } + + fn play(&self, index: usize) -> Result<(), Box<dyn Error>> { + let mail = &self.queue_mail[index]; + mail.send(QueueCommand::Play)?; + dbg!(mail.recv()?); + Ok(()) + } + + fn set_songs(&self, index: usize, songs: Vec<Song>) -> Result<(), Box<dyn Error>> { + let mail = &self.queue_mail[index]; + mail.send(QueueCommand::SetSongs(songs))?; + dbg!(mail.recv()?); + Ok(()) + } + + fn enqueue(&self, index: usize, uri: URI) -> Result<(), Box<dyn Error>> { + let mail = &self.queue_mail[index]; + mail.send(QueueCommand::Enqueue(uri))?; + dbg!(mail.recv()?); + Ok(()) + } + fn scan_folder(&self, folder: String) -> Result<(), Box<dyn Error>> { + let mail = &self.db_mail; + mail.send(DatabaseCommand::ReadFolder(folder))?; + dbg!(mail.recv()?); + Ok(()) + } + } #[test] fn name() { - let a = Controller::start(PathBuf::from("test-config/config_test.json")).unwrap(); - // sleep(Duration::from_millis(5000)); - _ = a.controller_mail.send(ControllerCommand::Test); - // dbg!(a.get_db_songs()); - // sleep(Duration::from_secs(6)); + let mut a = match Controller::start("test-config/config_test.json".to_string()) { + Ok(c) => c, + Err(e) => panic!("{e}") + }; + sleep(Duration::from_millis(500)); + a.scan_folder("test-config/music/".to_string()); + a.new_queue(); + let songs = a.get_db_songs(); + a.enqueue(0, songs[0].location.clone()); + a.play(0).unwrap(); + + sleep(Duration::from_secs(10)); } \ No newline at end of file diff --git a/src/music_storage/db_reader/foobar/reader.rs b/src/music_storage/db_reader/foobar/reader.rs index 3cb417d..324b863 100644 --- a/src/music_storage/db_reader/foobar/reader.rs +++ b/src/music_storage/db_reader/foobar/reader.rs @@ -1,6 +1,8 @@ use std::collections::BTreeMap; use std::{fs::File, io::Read, path::Path, time::Duration}; +use uuid::Uuid; + use super::utils::meta_offset; use crate::music_storage::db_reader::common::{get_bytes, get_bytes_vec}; use crate::music_storage::db_reader::extern_library::ExternalLibrary; @@ -177,6 +179,7 @@ impl FoobarPlaylistTrack { Song { location, + uuid: Uuid::new_v4(), plays: 0, skips: 0, favorited: false, diff --git a/src/music_storage/db_reader/itunes/reader.rs b/src/music_storage/db_reader/itunes/reader.rs index 3bc73f6..15f89a9 100644 --- a/src/music_storage/db_reader/itunes/reader.rs +++ b/src/music_storage/db_reader/itunes/reader.rs @@ -170,6 +170,7 @@ impl ExternalLibrary for ITunesLibrary { let ny: Song = Song { location: sug, + uuid: Uuid::new_v4(), plays: track.plays, skips: 0, favorited: track.favorited, diff --git a/src/music_storage/library.rs b/src/music_storage/library.rs index 502429b..e788639 100644 --- a/src/music_storage/library.rs +++ b/src/music_storage/library.rs @@ -119,6 +119,7 @@ impl ToString for Field { #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] pub struct Song { pub location: URI, + pub uuid: Uuid, pub plays: i32, pub skips: i32, pub favorited: bool, @@ -256,6 +257,7 @@ impl Song { let new_song = Song { location: URI::Local(binding), + uuid: Uuid::new_v4(), plays: 0, skips: 0, favorited: false, @@ -378,6 +380,7 @@ impl Song { start, end, }, + uuid: Uuid::new_v4(), plays: 0, skips: 0, favorited: false, @@ -552,7 +555,7 @@ pub struct MusicLibrary { #[test] fn library_init() { - let config = Config::read_file(PathBuf::from("config_test.json")).unwrap(); + let config = Config::read_file(PathBuf::from("test_config/config_test.json")).unwrap(); let target_uuid = config.libraries.libraries[0].uuid; let a = MusicLibrary::init(Arc::new(RwLock::from(config)), target_uuid).unwrap(); dbg!(a); @@ -575,13 +578,14 @@ impl MusicLibrary { /// the [MusicLibrary] Vec pub fn init(config: Arc<RwLock<Config>>, uuid: Uuid) -> Result<Self, Box<dyn Error>> { let global_config = &*config.read().unwrap(); + let path = global_config.libraries.get_library(&uuid)?.path; - let library: MusicLibrary = match global_config.libraries.get_library(&uuid)?.path.exists() { - true => read_file(global_config.libraries.get_library(&uuid)?.path)?, + let library: MusicLibrary = match path.exists() { + true => read_file(path)?, false => { // If the library does not exist, re-create it let lib = MusicLibrary::new(String::new(), uuid); - write_file(&lib, global_config.libraries.get_library(&uuid)?.path)?; + write_file(&lib, path)?; lib } }; @@ -643,6 +647,26 @@ impl MusicLibrary { } } + /// Queries for a [Song] by its [Uuid], returning a single `Song` + /// with the `Uuid` that matches along with its position in the library + pub fn query_uuid(&self, uuid: &Uuid) -> Option<(&Song, usize)> { + let result = self + .library + .par_iter() + .enumerate() + .try_for_each(|(i, track)| { + if uuid == &track.uuid { + return std::ops::ControlFlow::Break((track, i)); + } + Continue(()) + }); + + match result { + Break(song) => Some(song), + Continue(_) => None, + } + } + /// Queries for a [Song] by its [PathBuf], returning a `Vec<&Song>` /// with matching `PathBuf`s fn query_path(&self, path: PathBuf) -> Option<Vec<&Song>> { From e25a7bfcc8862ca5fc5b643b0738b04ae1c2a193 Mon Sep 17 00:00:00 2001 From: MrDulfin <Dulfinaminator@gmail.com> Date: Mon, 12 Feb 2024 14:12:51 -0500 Subject: [PATCH 092/136] playing 2 queues at once works --- src/music_controller/controller.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/music_controller/controller.rs b/src/music_controller/controller.rs index da2ea0f..d887ccd 100644 --- a/src/music_controller/controller.rs +++ b/src/music_controller/controller.rs @@ -249,6 +249,7 @@ impl Controller { }, Enqueue(uri) => { queue.player.enqueue_next(&uri); + // in_thread.send(QueueResponse::Default).unwrap(); } } } @@ -273,7 +274,7 @@ impl Controller { fn enqueue(&self, index: usize, uri: URI) -> Result<(), Box<dyn Error>> { let mail = &self.queue_mail[index]; mail.send(QueueCommand::Enqueue(uri))?; - dbg!(mail.recv()?); + // dbg!(mail.recv()?); Ok(()) } fn scan_folder(&self, folder: String) -> Result<(), Box<dyn Error>> { @@ -294,9 +295,12 @@ fn name() { sleep(Duration::from_millis(500)); a.scan_folder("test-config/music/".to_string()); a.new_queue(); + a.new_queue(); let songs = a.get_db_songs(); - a.enqueue(0, songs[0].location.clone()); + a.enqueue(0, songs[1].location.clone()); + a.enqueue(1, songs[2].location.clone()); a.play(0).unwrap(); + a.play(1).unwrap(); sleep(Duration::from_secs(10)); } \ No newline at end of file From c842bf0b9cfabadab4e915f625198cd6ea73f3ef Mon Sep 17 00:00:00 2001 From: MrDulfin <mrdulfin@mrdulfin.com> Date: Fri, 16 Feb 2024 00:26:07 -0500 Subject: [PATCH 093/136] edited the Play command and the test function --- src/music_controller/controller.rs | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/music_controller/controller.rs b/src/music_controller/controller.rs index d887ccd..777b7e2 100644 --- a/src/music_controller/controller.rs +++ b/src/music_controller/controller.rs @@ -239,8 +239,11 @@ impl Controller { Default => {}, Test => { in_thread.send(QueueResponse::Test).unwrap() }, Play => { - queue.player.play().unwrap(); - in_thread.send(QueueResponse::Default).unwrap(); + match queue.player.play() { + Ok(_) => in_thread.send(QueueResponse::Default).unwrap(), + Err(_) => unimplemented!() + }; + }, Pause => {}, SetSongs(songs) => { @@ -248,7 +251,10 @@ impl Controller { in_thread.send(QueueResponse::Default).unwrap(); }, Enqueue(uri) => { - queue.player.enqueue_next(&uri); + if uri.exists().unwrap() { + queue.player.enqueue_next(&uri); + } + // in_thread.send(QueueResponse::Default).unwrap(); } } @@ -295,12 +301,12 @@ fn name() { sleep(Duration::from_millis(500)); a.scan_folder("test-config/music/".to_string()); a.new_queue(); - a.new_queue(); + // a.new_queue(); let songs = a.get_db_songs(); - a.enqueue(0, songs[1].location.clone()); - a.enqueue(1, songs[2].location.clone()); + a.enqueue(0, songs[4].location.clone()); + // a.enqueue(1, songs[2].location.clone()); a.play(0).unwrap(); - a.play(1).unwrap(); + // a.play(1).unwrap(); sleep(Duration::from_secs(10)); -} \ No newline at end of file +} From 3c9311003731292c8f2a7025e9959ba9b1a5fc2c Mon Sep 17 00:00:00 2001 From: MrDulfin <mrdulfin@mrdulfin.com> Date: Fri, 16 Feb 2024 00:56:38 -0500 Subject: [PATCH 094/136] Added #![allow(unused)] --- src/lib.rs | 2 ++ src/music_controller/controller.rs | 4 +--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 3119019..41894fa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,5 @@ +#![allow(unused)] + pub mod music_storage { pub mod library; pub mod music_collection; diff --git a/src/music_controller/controller.rs b/src/music_controller/controller.rs index 777b7e2..11a6fd4 100644 --- a/src/music_controller/controller.rs +++ b/src/music_controller/controller.rs @@ -22,9 +22,7 @@ use crate::music_storage::library::{Tag, URI}; use crate::music_storage::playlist::Playlist; use crate::{ music_player::Player, - music_storage::{ - library::{MusicLibrary, Song} - }, + music_storage::library::{MusicLibrary, Song}, config::config::Config, }; From c6b561c7af509411f01bbf1baa5b701dc215abb3 Mon Sep 17 00:00:00 2001 From: MrDulfin <mrdulfin@mrdulfin.com> Date: Fri, 16 Feb 2024 22:24:02 -0500 Subject: [PATCH 095/136] made prepatory changes to the library --- src/config/config.rs | 31 ++++++++++++++---- src/music_controller/controller.rs | 34 ++++++++++---------- src/music_storage/db_reader/foobar/reader.rs | 1 + src/music_storage/db_reader/itunes/reader.rs | 11 +++++-- src/music_storage/library.rs | 25 +++++++++++++- 5 files changed, 75 insertions(+), 27 deletions(-) diff --git a/src/config/config.rs b/src/config/config.rs index 15b7121..918af12 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -4,7 +4,7 @@ use std::{ io::{Error, Write, Read}, sync::{Arc, RwLock}, }; -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Serialize}; use serde_json::to_string_pretty; use thiserror::Error; use uuid::Uuid; @@ -126,6 +126,23 @@ impl Config { Ok(()) } + pub fn save_backup(&self) -> Result<(), Box<dyn std::error::Error>> { + match &self.backup_folder { + Some(path) => { + let mut writer = path.clone(); + writer.set_extension("tmp"); + let mut file = OpenOptions::new().create(true).truncate(true).read(true).write(true).open(&writer)?; + let config = to_string_pretty(self)?; + // dbg!(&config); + + file.write_all(config.as_bytes())?; + fs::rename(writer, self.path.as_path())?; + Ok(()) + }, + None => Err(ConfigError::NoBackupLibrary.into()) + } + } + pub fn read_file(path: PathBuf) -> Result<Self, Error> { let mut file: File = File::open(path)?; let mut bun: String = String::new(); @@ -144,6 +161,8 @@ pub enum ConfigError { //TODO: do something about playlists #[error("Please provide a better m3u8 Playlist")] BadPlaylist, + #[error("No backup Config folder present")] + NoBackupLibrary, } @@ -166,16 +185,16 @@ fn config_test() { }; config.write_file(); let arc = Arc::new(RwLock::from(config)); - MusicLibrary::init(arc.clone(), lib_a.uuid.clone()).unwrap(); - MusicLibrary::init(arc.clone(), lib_b.uuid.clone()).unwrap(); - MusicLibrary::init(arc.clone(), lib_c.uuid.clone()).unwrap(); + MusicLibrary::init(arc.clone(), lib_a.uuid).unwrap(); + MusicLibrary::init(arc.clone(), lib_b.uuid).unwrap(); + MusicLibrary::init(arc.clone(), lib_c.uuid).unwrap(); } #[test] fn test2() { let config = Config::read_file(PathBuf::from("test-config/config_test.json")).unwrap(); - let uuid = config.libraries.get_default().unwrap().uuid.clone(); + let uuid = config.libraries.get_default().unwrap().uuid; let mut lib = MusicLibrary::init(Arc::new(RwLock::from(config.clone())), uuid).unwrap(); lib.scan_folder("test-config/music/").unwrap(); lib.save(config.clone()).unwrap(); @@ -187,7 +206,7 @@ fn test2() { fn test3() { let config = Config::read_file(PathBuf::from("test-config/config_test.json")).unwrap(); let uuid = config.libraries.get_default().unwrap().uuid; - let mut lib = MusicLibrary::init(Arc::new(RwLock::from(config.clone())), uuid).unwrap(); + let lib = MusicLibrary::init(Arc::new(RwLock::from(config.clone())), uuid).unwrap(); dbg!(lib); } \ No newline at end of file diff --git a/src/music_controller/controller.rs b/src/music_controller/controller.rs index 11a6fd4..750ee02 100644 --- a/src/music_controller/controller.rs +++ b/src/music_controller/controller.rs @@ -52,12 +52,12 @@ pub struct Controller { // queues: Vec<Queue>, config: Arc<RwLock<Config>>, // library: MusicLibrary, - controller_mail: MailMan<ControllerCommand, ControllerResponse>, - db_mail: MailMan<DatabaseCommand, DatabaseResponse>, - queue_mail: Vec<MailMan<QueueCommand, QueueResponse>>, + controller_mail: MailMan<ControllerCmd, ControllerResponse>, + db_mail: MailMan<DatabaseCmd, DatabaseResponse>, + queue_mail: Vec<MailMan<QueueCmd, QueueResponse>>, } #[derive(Debug)] -pub enum ControllerCommand { +pub enum ControllerCmd { Default, Test } @@ -65,12 +65,12 @@ pub enum ControllerCommand { #[derive(Debug)] enum ControllerResponse { Empty, - QueueMailMan(MailMan<QueueCommand, QueueResponse>), + QueueMailMan(MailMan<QueueCmd, QueueResponse>), } #[derive(Debug)] -pub enum DatabaseCommand { +pub enum DatabaseCmd { Default, Test, GetSongs, @@ -88,7 +88,7 @@ enum DatabaseResponse { } #[derive(Debug)] -enum QueueCommand { +enum QueueCmd { Default, Test, Play, @@ -128,7 +128,7 @@ impl<T, U> MailMan<T, U> { } pub fn send(&self, mail: T) -> Result<(), Box<dyn Error>> { - &self.tx.send(mail).unwrap(); + self.tx.send(mail).unwrap(); Ok(()) } @@ -150,7 +150,7 @@ impl Controller { let (out_thread_controller, in_thread) = MailMan::double(); let monitor_thread = spawn(move || { - use ControllerCommand::*; + use ControllerCmd::*; loop { let command = in_thread.recv().unwrap(); @@ -166,7 +166,7 @@ impl Controller { let (out_thread_db, in_thread) = MailMan::double(); let db_monitor = spawn(move || { - use DatabaseCommand::*; + use DatabaseCmd::*; loop { let command = in_thread.recv().unwrap(); @@ -218,7 +218,7 @@ impl Controller { } fn get_db_songs(&self) -> Vec<Song> { - self.db_mail.send(DatabaseCommand::GetSongs); + self.db_mail.send(DatabaseCmd::GetSongs); match self.db_mail.recv().unwrap() { DatabaseResponse::Songs(songs) => songs, _ => Vec::new() @@ -227,9 +227,9 @@ impl Controller { } pub fn new_queue(&mut self) { - let (out_thread_queue, in_thread) = MailMan::<QueueCommand, QueueResponse>::double(); + let (out_thread_queue, in_thread) = MailMan::<QueueCmd, QueueResponse>::double(); let queues_monitor = spawn(move || { - use QueueCommand::*; + use QueueCmd::*; let mut queue = Queue::new().unwrap(); loop { let command = in_thread.recv().unwrap(); @@ -263,27 +263,27 @@ impl Controller { fn play(&self, index: usize) -> Result<(), Box<dyn Error>> { let mail = &self.queue_mail[index]; - mail.send(QueueCommand::Play)?; + mail.send(QueueCmd::Play)?; dbg!(mail.recv()?); Ok(()) } fn set_songs(&self, index: usize, songs: Vec<Song>) -> Result<(), Box<dyn Error>> { let mail = &self.queue_mail[index]; - mail.send(QueueCommand::SetSongs(songs))?; + mail.send(QueueCmd::SetSongs(songs))?; dbg!(mail.recv()?); Ok(()) } fn enqueue(&self, index: usize, uri: URI) -> Result<(), Box<dyn Error>> { let mail = &self.queue_mail[index]; - mail.send(QueueCommand::Enqueue(uri))?; + mail.send(QueueCmd::Enqueue(uri))?; // dbg!(mail.recv()?); Ok(()) } fn scan_folder(&self, folder: String) -> Result<(), Box<dyn Error>> { let mail = &self.db_mail; - mail.send(DatabaseCommand::ReadFolder(folder))?; + mail.send(DatabaseCmd::ReadFolder(folder))?; dbg!(mail.recv()?); Ok(()) } diff --git a/src/music_storage/db_reader/foobar/reader.rs b/src/music_storage/db_reader/foobar/reader.rs index 324b863..815f9a7 100644 --- a/src/music_storage/db_reader/foobar/reader.rs +++ b/src/music_storage/db_reader/foobar/reader.rs @@ -183,6 +183,7 @@ impl FoobarPlaylistTrack { plays: 0, skips: 0, favorited: false, + // banned: None, rating: None, format: None, duration: self.duration, diff --git a/src/music_storage/db_reader/itunes/reader.rs b/src/music_storage/db_reader/itunes/reader.rs index 15f89a9..decf390 100644 --- a/src/music_storage/db_reader/itunes/reader.rs +++ b/src/music_storage/db_reader/itunes/reader.rs @@ -15,7 +15,7 @@ use chrono::prelude::*; use crate::config::config::{Config, ConfigLibrary}; use crate::music_storage::db_reader::extern_library::ExternalLibrary; -use crate::music_storage::library::{AlbumArt, MusicLibrary, Service, Song, Tag, URI}; +use crate::music_storage::library::{AlbumArt, MusicLibrary, Service, Song, Tag, URI, BannedType}; use crate::music_storage::utils; use urlencoding::decode; @@ -149,7 +149,7 @@ impl ExternalLibrary for ITunesLibrary { continue; } - let sug: URI = if track.location.contains("file://localhost/") { + let location: URI = if track.location.contains("file://localhost/") { URI::Local(PathBuf::from( decode(track.location.strip_prefix("file://localhost/").unwrap()) .unwrap() @@ -169,11 +169,16 @@ impl ExternalLibrary for ITunesLibrary { let play_time_ = StdDur::from_secs(track.plays as u64 * dur.as_secs()); let ny: Song = Song { - location: sug, + location, uuid: Uuid::new_v4(), plays: track.plays, skips: 0, favorited: track.favorited, + // banned: if track.banned { + // Some(BannedType::All) + // }else { + // None + // }, rating: track.rating, format: match FileFormat::from_file(PathBuf::from(&loc)) { Ok(e) => Some(e), diff --git a/src/music_storage/library.rs b/src/music_storage/library.rs index e788639..93b9991 100644 --- a/src/music_storage/library.rs +++ b/src/music_storage/library.rs @@ -115,6 +115,26 @@ impl ToString for Field { } } +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +pub enum BannedType { + Shuffle, + All, +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +pub enum DoNotTrack { + // TODO: add services to not track +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +enum SongType { + // TODO: add song types + Main, + Instrumental, + Remix, + Custom(String) +} + /// Stores information about a single song #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] pub struct Song { @@ -123,6 +143,7 @@ pub struct Song { pub plays: i32, pub skips: i32, pub favorited: bool, + // pub banned: Option<BannedType>, pub rating: Option<u8>, pub format: Option<FileFormat>, pub duration: Duration, @@ -261,6 +282,7 @@ impl Song { plays: 0, skips: 0, favorited: false, + // banned: None, rating: None, format, duration, @@ -384,6 +406,7 @@ impl Song { plays: 0, skips: 0, favorited: false, + // banned: None, rating: None, format, duration, @@ -397,7 +420,7 @@ impl Song { tracks.push((new_song, audio_location.clone())); } } - Ok((tracks)) + Ok(tracks) } } From d2822bbb37a3ffb772e4c3f161a21e4dc2b2ab97 Mon Sep 17 00:00:00 2001 From: G2-Games <ke0bhogsg@gmail.com> Date: Sat, 17 Feb 2024 21:30:53 -0600 Subject: [PATCH 096/136] Moved location of `uri.exists()` to in `Player` --- src/music_controller/controller.rs | 4 +--- src/music_player.rs | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/music_controller/controller.rs b/src/music_controller/controller.rs index 750ee02..d6d1398 100644 --- a/src/music_controller/controller.rs +++ b/src/music_controller/controller.rs @@ -249,9 +249,7 @@ impl Controller { in_thread.send(QueueResponse::Default).unwrap(); }, Enqueue(uri) => { - if uri.exists().unwrap() { - queue.player.enqueue_next(&uri); - } + queue.player.enqueue_next(&uri).unwrap(); // in_thread.send(QueueResponse::Default).unwrap(); } diff --git a/src/music_player.rs b/src/music_player.rs index 4e7de23..12af300 100644 --- a/src/music_player.rs +++ b/src/music_player.rs @@ -68,6 +68,8 @@ pub enum PlayerError { Factory(#[from] glib::BoolError), #[error("could not change playback state")] StateChange(#[from] gst::StateChangeError), + #[error("the file or source is not found")] + NotFound, #[error("failed to build gstreamer item")] Build, #[error("poison error")] @@ -257,12 +259,17 @@ impl Player { &self.source } - pub fn enqueue_next(&mut self, next_track: &URI) { - self.set_source(next_track); + pub fn enqueue_next(&mut self, next_track: &URI) -> Result<(), PlayerError> { + self.set_source(next_track) } /// Set the playback URI - fn set_source(&mut self, source: &URI) { + fn set_source(&mut self, source: &URI) -> Result<(), PlayerError> { + if !source.exists().is_ok_and(|x| x) { + // If the source doesn't exist, gstreamer will crash! + return Err(PlayerError::NotFound) + } + // Make sure the playback tracker knows the stuff is stopped self.playback_tx.send(PlaybackStats::Switching).unwrap(); @@ -290,7 +297,7 @@ impl Player { let now = std::time::Instant::now(); while now.elapsed() < std::time::Duration::from_millis(20) { if self.seek_to(Duration::from_std(*start).unwrap()).is_ok() { - return; + return Ok(()); } std::thread::sleep(std::time::Duration::from_millis(1)); } @@ -321,6 +328,8 @@ impl Player { }).unwrap(); } } + + Ok(()) } /// Gets a mutable reference to the playbin element From 74e2933de19609ee90f88f3c16852421d7c4723c Mon Sep 17 00:00:00 2001 From: MrDulfin <mrdulfin@mrdulfin.com> Date: Fri, 23 Feb 2024 20:51:59 -0500 Subject: [PATCH 097/136] Added more functions for testing --- src/music_controller/controller.rs | 101 ++++++++++++++++++++++------- 1 file changed, 77 insertions(+), 24 deletions(-) diff --git a/src/music_controller/controller.rs b/src/music_controller/controller.rs index d6d1398..2d6a383 100644 --- a/src/music_controller/controller.rs +++ b/src/music_controller/controller.rs @@ -41,6 +41,7 @@ impl Queue { } ) } + fn set_tracks(&mut self, tracks: Vec<Song>) { let mut tracks = tracks; self.songs.clear(); @@ -73,6 +74,7 @@ enum ControllerResponse { pub enum DatabaseCmd { Default, Test, + SaveLibrary, GetSongs, QueryUuid(Uuid), QueryUuids(Vec<Uuid>), @@ -85,6 +87,7 @@ enum DatabaseResponse { Empty, Song(Song), Songs(Vec<Song>), + Library(MusicLibrary), } #[derive(Debug)] @@ -96,12 +99,14 @@ enum QueueCmd { SetSongs(Vec<Song>), // SetLocation(URI), Enqueue(URI), + SetVolume(f64), } #[derive(Debug)] enum QueueResponse { Default, Test, + Index(i32), } #[derive(Debug)] @@ -145,9 +150,10 @@ impl Controller { let config = Config::read_file(config_path)?; let uuid = config.libraries.get_default()?.uuid; - let config = Arc::new(RwLock::from(config)); - let mut lib = MusicLibrary::init(config.clone(), uuid)?; + let config_ = Arc::new(RwLock::from(config)); + let mut lib = MusicLibrary::init(config_.clone(), uuid)?; + let config = config_.clone(); let (out_thread_controller, in_thread) = MailMan::double(); let monitor_thread = spawn(move || { use ControllerCmd::*; @@ -163,7 +169,7 @@ impl Controller { } }); - + let config = config_.clone(); let (out_thread_db, in_thread) = MailMan::double(); let db_monitor = spawn(move || { use DatabaseCmd::*; @@ -179,6 +185,10 @@ impl Controller { let songs = lib.query_tracks(&String::from(""), &(vec![Tag::Title]), &(vec![Tag::Title])).unwrap().iter().cloned().cloned().collect(); in_thread.send(DatabaseResponse::Songs(songs)).unwrap(); }, + SaveLibrary => { + //TODO: make this send lib ref to the function to save instead + lib.save(config.read().unwrap().to_owned()).unwrap(); + }, QueryUuid(uuid) => { match lib.query_uuid(&uuid) { Some(song) => in_thread.send(DatabaseResponse::Song(song.0.clone())).unwrap(), @@ -209,7 +219,7 @@ impl Controller { Ok( Controller { // queues: Vec::new(), - config, + config: config_.clone(), controller_mail: out_thread_controller, db_mail: out_thread_db, queue_mail: Vec::new(), @@ -217,16 +227,27 @@ impl Controller { ) } - fn get_db_songs(&self) -> Vec<Song> { + fn lib_get_songs(&self) -> Vec<Song> { self.db_mail.send(DatabaseCmd::GetSongs); match self.db_mail.recv().unwrap() { DatabaseResponse::Songs(songs) => songs, _ => Vec::new() } - } - pub fn new_queue(&mut self) { + fn lib_scan_folder(&self, folder: String) -> Result<(), Box<dyn Error>> { + let mail = &self.db_mail; + mail.send(DatabaseCmd::ReadFolder(folder))?; + dbg!(mail.recv()?); + Ok(()) + } + + pub fn lib_save(&self) -> Result<(), Box<dyn Error>> { + self.db_mail.send(DatabaseCmd::SaveLibrary); + Ok(()) + } + + pub fn q_new(&mut self) -> Result<usize, Box<dyn Error>> { let (out_thread_queue, in_thread) = MailMan::<QueueCmd, QueueResponse>::double(); let queues_monitor = spawn(move || { use QueueCmd::*; @@ -239,11 +260,16 @@ impl Controller { Play => { match queue.player.play() { Ok(_) => in_thread.send(QueueResponse::Default).unwrap(), - Err(_) => unimplemented!() + Err(_) => todo!() }; }, - Pause => {}, + Pause => { + match queue.player.pause() { + Ok(_) => in_thread.send(QueueResponse::Default).unwrap(), + Err(_) => todo!() + } + }, SetSongs(songs) => { queue.set_tracks(songs); in_thread.send(QueueResponse::Default).unwrap(); @@ -252,57 +278,84 @@ impl Controller { queue.player.enqueue_next(&uri).unwrap(); // in_thread.send(QueueResponse::Default).unwrap(); + }, + SetVolume(vol) => { + queue.player.set_volume(vol); } } } }); self.queue_mail.push(out_thread_queue); + Ok((self.queue_mail.len() - 1)) } - fn play(&self, index: usize) -> Result<(), Box<dyn Error>> { + fn q_play(&self, index: usize) -> Result<(), Box<dyn Error>> { let mail = &self.queue_mail[index]; mail.send(QueueCmd::Play)?; dbg!(mail.recv()?); Ok(()) } - fn set_songs(&self, index: usize, songs: Vec<Song>) -> Result<(), Box<dyn Error>> { + fn q_pause(&self, index: usize) -> Result<(), Box<dyn Error>> { + let mail = &self.queue_mail[index]; + mail.send(QueueCmd::Pause)?; + dbg!(mail.recv()?); + Ok(()) + } + + pub fn q_set_volume(&self, index: usize, volume: f64) -> Result<(), Box<dyn Error>> { + let mail = &self.queue_mail[index]; + mail.send(QueueCmd::SetVolume(volume))?; + Ok(()) + } + + fn q_set_songs(&self, index: usize, songs: Vec<Song>) -> Result<(), Box<dyn Error>> { let mail = &self.queue_mail[index]; mail.send(QueueCmd::SetSongs(songs))?; dbg!(mail.recv()?); Ok(()) } - fn enqueue(&self, index: usize, uri: URI) -> Result<(), Box<dyn Error>> { + fn q_enqueue(&self, index: usize, uri: URI) -> Result<(), Box<dyn Error>> { let mail = &self.queue_mail[index]; mail.send(QueueCmd::Enqueue(uri))?; // dbg!(mail.recv()?); Ok(()) } - fn scan_folder(&self, folder: String) -> Result<(), Box<dyn Error>> { - let mail = &self.db_mail; - mail.send(DatabaseCmd::ReadFolder(folder))?; - dbg!(mail.recv()?); - Ok(()) - } + } #[test] -fn name() { +fn play_test() { let mut a = match Controller::start("test-config/config_test.json".to_string()) { Ok(c) => c, Err(e) => panic!("{e}") }; sleep(Duration::from_millis(500)); - a.scan_folder("test-config/music/".to_string()); - a.new_queue(); + + let i = a.q_new().unwrap(); + a.q_set_volume(i, 0.04); // a.new_queue(); - let songs = a.get_db_songs(); - a.enqueue(0, songs[4].location.clone()); + let songs = a.lib_get_songs(); + a.q_enqueue(i, songs[2].location.clone()); // a.enqueue(1, songs[2].location.clone()); - a.play(0).unwrap(); + a.q_play(i).unwrap(); // a.play(1).unwrap(); sleep(Duration::from_secs(10)); + a.q_pause(i); + sleep(Duration::from_secs(10)); + a.q_play(i); + sleep(Duration::from_secs(1000)); +} + +#[test] +fn test_() { + let a = match Controller::start("test-config/config_test.json".to_string()) { + Ok(c) => c, + Err(e) => panic!("{e}") + }; + a.lib_scan_folder("F:/Music/Mp3".to_string()); + a.lib_save(); } From 0e6a676e09352f73c916da93dcc8ffc23048e38f Mon Sep 17 00:00:00 2001 From: MrDulfin <mrdulfin@mrdulfin.com> Date: Sat, 24 Feb 2024 20:25:56 -0500 Subject: [PATCH 098/136] moved queue to its own file --- src/lib.rs | 1 + src/music_controller/controller.rs | 24 +----------------------- src/music_controller/queue.rs | 25 +++++++++++++++++++++++++ 3 files changed, 27 insertions(+), 23 deletions(-) create mode 100644 src/music_controller/queue.rs diff --git a/src/lib.rs b/src/lib.rs index 41894fa..a9d0db0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,6 +13,7 @@ pub mod music_storage { pub mod music_controller{ pub mod controller; pub mod connections; + pub mod queue; } pub mod music_player; diff --git a/src/music_controller/controller.rs b/src/music_controller/controller.rs index 2d6a383..42f8f0e 100644 --- a/src/music_controller/controller.rs +++ b/src/music_controller/controller.rs @@ -24,31 +24,9 @@ use crate::{ music_player::Player, music_storage::library::{MusicLibrary, Song}, config::config::Config, + music_controller::queue::Queue, }; -struct Queue { - player: Player, - name: String, - songs: Vec<Song>, -} -impl Queue { - fn new() -> Result<Self, Box<dyn Error>> { - Ok( - Queue { - player: Player::new()?, - name: String::new(), - songs: Vec::new() - } - ) - } - - fn set_tracks(&mut self, tracks: Vec<Song>) { - let mut tracks = tracks; - self.songs.clear(); - self.songs.append(&mut tracks); - } -} - pub struct Controller { // queues: Vec<Queue>, config: Arc<RwLock<Config>>, diff --git a/src/music_controller/queue.rs b/src/music_controller/queue.rs new file mode 100644 index 0000000..7eaace7 --- /dev/null +++ b/src/music_controller/queue.rs @@ -0,0 +1,25 @@ +use crate::{music_player::Player, music_storage::library::Song}; +use std::error::Error; + +pub struct Queue { + pub player: Player, + pub name: String, + pub songs: Vec<Song>, +} +impl Queue { + pub fn new() -> Result<Self, Box<dyn Error>> { + Ok( + Queue { + player: Player::new()?, + name: String::new(), + songs: Vec::new() + } + ) + } + + pub fn set_tracks(&mut self, tracks: Vec<Song>) { + let mut tracks = tracks; + self.songs.clear(); + self.songs.append(&mut tracks); + } +} From 79ec5aa1ef3e2ba9d3c0e810a06fc8e9186805ff Mon Sep 17 00:00:00 2001 From: MrDulfin <mrdulfin@mrdulfin.com> Date: Mon, 26 Feb 2024 20:17:37 -0500 Subject: [PATCH 099/136] Added function to add items to the queue. currently does not work with songs that have the `Played` State --- src/music_controller/controller.rs | 24 ++++---- src/music_controller/queue.rs | 97 +++++++++++++++++++++++++++--- src/music_player.rs | 1 + 3 files changed, 102 insertions(+), 20 deletions(-) diff --git a/src/music_controller/controller.rs b/src/music_controller/controller.rs index 42f8f0e..8d7f08e 100644 --- a/src/music_controller/controller.rs +++ b/src/music_controller/controller.rs @@ -27,6 +27,8 @@ use crate::{ music_controller::queue::Queue, }; +use super::queue::{QueueItem, QueueState}; + pub struct Controller { // queues: Vec<Queue>, config: Arc<RwLock<Config>>, @@ -74,7 +76,7 @@ enum QueueCmd { Test, Play, Pause, - SetSongs(Vec<Song>), + // SetSongs(Vec<QueueItem<QueueState>>), // SetLocation(URI), Enqueue(URI), SetVolume(f64), @@ -248,10 +250,10 @@ impl Controller { Err(_) => todo!() } }, - SetSongs(songs) => { - queue.set_tracks(songs); - in_thread.send(QueueResponse::Default).unwrap(); - }, + // SetSongs(songs) => { + // queue.set_tracks(songs); + // in_thread.send(QueueResponse::Default).unwrap(); + // }, Enqueue(uri) => { queue.player.enqueue_next(&uri).unwrap(); @@ -287,12 +289,12 @@ impl Controller { Ok(()) } - fn q_set_songs(&self, index: usize, songs: Vec<Song>) -> Result<(), Box<dyn Error>> { - let mail = &self.queue_mail[index]; - mail.send(QueueCmd::SetSongs(songs))?; - dbg!(mail.recv()?); - Ok(()) - } + // fn q_set_songs(&self, index: usize, songs: Vec<QueueItem<QueueState>>) -> Result<(), Box<dyn Error>> { + // let mail = &self.queue_mail[index]; + // mail.send(QueueCmd::SetSongs(songs))?; + // dbg!(mail.recv()?); + // Ok(()) + // } fn q_enqueue(&self, index: usize, uri: URI) -> Result<(), Box<dyn Error>> { let mail = &self.queue_mail[index]; diff --git a/src/music_controller/queue.rs b/src/music_controller/queue.rs index 7eaace7..7785d4d 100644 --- a/src/music_controller/queue.rs +++ b/src/music_controller/queue.rs @@ -1,25 +1,104 @@ -use crate::{music_player::Player, music_storage::library::Song}; -use std::error::Error; +use uuid::Uuid; -pub struct Queue { +use crate::{music_player::Player, music_storage::library::{Album, Song}}; +use std::{error::Error, path::Path}; + +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum QueueState { + Played, + Current, + AddHere, + None, +} +#[derive(Debug, Clone)] +pub enum QueueItemType<'a> { + Song(Uuid), + Album(Album<'a>) +} +#[derive(Debug, Clone)] +#[non_exhaustive] +pub struct QueueItem<'a> { + item: QueueItemType<'a>, + state: QueueState +} + + +#[derive(Debug)] +pub struct Queue<'a> { pub player: Player, pub name: String, - pub songs: Vec<Song>, + pub items: Vec<QueueItem<'a>>, } -impl Queue { + +impl<'a> Queue<'a> { pub fn new() -> Result<Self, Box<dyn Error>> { Ok( Queue { player: Player::new()?, name: String::new(), - songs: Vec::new() + items: Vec::new() } ) } - pub fn set_tracks(&mut self, tracks: Vec<Song>) { + pub fn set_items(&mut self, tracks: Vec<QueueItem<'a>>) { let mut tracks = tracks; - self.songs.clear(); - self.songs.append(&mut tracks); + self.items.clear(); + self.items.append(&mut tracks); + } + + pub fn current_index(&self) -> i16 { + let e = self.items.iter().filter(|song| song.state == QueueState::Played ).collect::<Vec<&QueueItem>>().len(); + dbg!(&e); + e as i16 - 1 + } + + pub fn add_item(&mut self, item: QueueItemType<'a>) -> Result<(), Box<dyn Error>> { + use QueueState::*; + let ind = self.current_index(); + let mut i: i16 = 1; + self.items = self.items.iter().enumerate().map(|(j, item_)| { + let mut item_ = item_.to_owned(); + if item_.state == AddHere { + i = j as i16 + 2; + item_.state = None; + } + if item_.state == Current { + i = j as i16 + 2; + } + item_ + }).collect::<Vec<QueueItem>>(); + let pos = (ind + i) as usize; + dbg!(&pos); + self.items.insert( + pos, + QueueItem { + item: item.clone(), + state: if pos == self.items.len() && i == 1 { + Current + }else { + AddHere + } + } + ); + Ok(()) } } + + +#[test] +fn itemaddtest() { + let mut q = Queue::new().unwrap(); + q.add_item(QueueItemType::Song(Uuid::new_v4())).unwrap(); + dbg!(&q.items); + q.add_item(QueueItemType::Song(Uuid::new_v4())).unwrap(); + dbg!(&q.items); + q.add_item(QueueItemType::Song(Uuid::new_v4())).unwrap(); + dbg!(&q.items); + q.add_item(QueueItemType::Song(Uuid::new_v4())).unwrap(); + dbg!(&q.items); + q.add_item(QueueItemType::Song(Uuid::new_v4())).unwrap(); + dbg!(&q.items); + q.add_item(QueueItemType::Song(Uuid::new_v4())).unwrap(); + dbg!(&q.items); +} \ No newline at end of file diff --git a/src/music_player.rs b/src/music_player.rs index 12af300..9893088 100644 --- a/src/music_player.rs +++ b/src/music_player.rs @@ -90,6 +90,7 @@ enum PlaybackStats { } /// An instance of a music player with a GStreamer backend +#[derive(Debug)] pub struct Player { source: Option<URI>, //pub message_tx: Sender<PlayerCmd>, From e175fe733778d67d0aa01c1d6bafef0c7e24ee8a Mon Sep 17 00:00:00 2001 From: MrDulfin <mrdulfin@mrdulfin.com> Date: Tue, 27 Feb 2024 01:23:09 -0500 Subject: [PATCH 100/136] added `remove_item()` function to Queue --- src/music_controller/queue.rs | 96 +++++++++++++++++++++++++---------- src/music_storage/playlist.rs | 2 + 2 files changed, 71 insertions(+), 27 deletions(-) diff --git a/src/music_controller/queue.rs b/src/music_controller/queue.rs index 7785d4d..05bb35e 100644 --- a/src/music_controller/queue.rs +++ b/src/music_controller/queue.rs @@ -1,7 +1,7 @@ use uuid::Uuid; -use crate::{music_player::Player, music_storage::library::{Album, Song}}; -use std::{error::Error, path::Path}; +use crate::{music_player::Player, music_storage::library::{Album, Song, URI}}; +use std::{error::Error, ops::Add, path::Path}; #[derive(Debug, PartialEq, Clone, Copy)] pub enum QueueState { @@ -11,15 +11,31 @@ pub enum QueueState { None, } #[derive(Debug, Clone)] +#[non_exhaustive] pub enum QueueItemType<'a> { Song(Uuid), - Album(Album<'a>) + ExternalSong(URI), + Album{ + album: Album<'a>, + shuffled: bool, + } +} + +#[derive(Debug, Clone)] +#[non_exhaustive] +pub enum QueueSource { + Library, + Playlist(Uuid), + Search, + Queue, + File, } #[derive(Debug, Clone)] #[non_exhaustive] pub struct QueueItem<'a> { item: QueueItemType<'a>, - state: QueueState + state: QueueState, + source: QueueSource } @@ -47,29 +63,34 @@ impl<'a> Queue<'a> { self.items.append(&mut tracks); } - pub fn current_index(&self) -> i16 { - let e = self.items.iter().filter(|song| song.state == QueueState::Played ).collect::<Vec<&QueueItem>>().len(); - dbg!(&e); - e as i16 - 1 + pub fn current_index(&mut self) -> i16 { + let mut i = 1; + let mut e = self.items.iter().filter(|song| song.state == QueueState::Played ).collect::<Vec<&QueueItem>>().len(); + while e >= 51 { + self.items.remove(0); + e = self.items.iter().filter(|song| song.state == QueueState::Played ).collect::<Vec<&QueueItem>>().len(); + i+=1; + } + e as i16 - 1 } - pub fn add_item(&mut self, item: QueueItemType<'a>) -> Result<(), Box<dyn Error>> { + pub fn add_item(&mut self, item: QueueItemType<'a>, source: QueueSource) -> Result<(), Box<dyn Error>> { use QueueState::*; let ind = self.current_index(); let mut i: i16 = 1; - self.items = self.items.iter().enumerate().map(|(j, item_)| { + self.items = self.items.iter().enumerate().map(|(j, item_)| { let mut item_ = item_.to_owned(); if item_.state == AddHere { - i = j as i16 + 2; + i = j as i16 + 1 - ind; item_.state = None; } if item_.state == Current { - i = j as i16 + 2; + i = j as i16 + 1 - ind; } item_ }).collect::<Vec<QueueItem>>(); let pos = (ind + i) as usize; - dbg!(&pos); + // dbg!(&pos, &i, &ind); self.items.insert( pos, QueueItem { @@ -78,27 +99,48 @@ impl<'a> Queue<'a> { Current }else { AddHere - } + }, + source } ); Ok(()) } + + pub fn remove_item(&mut self, index: usize) -> Result<(), Box<dyn Error>> { + use QueueState::*; + let ind = (self.current_index() + index as i16 + 1) as usize; + + if ind < self.items.len() { + // update the state of the next item to replace the item being removed + if self.items.get(ind + 1).is_some() { + self.items[ind + 1].state = self.items[ind].state; + } + self.items[ind].state = None; + self.items.remove(ind); + + Ok(()) + }else { + Err("No Songs to remove!".into()) + } + } } #[test] -fn itemaddtest() { +fn item_add_test() { let mut q = Queue::new().unwrap(); - q.add_item(QueueItemType::Song(Uuid::new_v4())).unwrap(); - dbg!(&q.items); - q.add_item(QueueItemType::Song(Uuid::new_v4())).unwrap(); - dbg!(&q.items); - q.add_item(QueueItemType::Song(Uuid::new_v4())).unwrap(); - dbg!(&q.items); - q.add_item(QueueItemType::Song(Uuid::new_v4())).unwrap(); - dbg!(&q.items); - q.add_item(QueueItemType::Song(Uuid::new_v4())).unwrap(); - dbg!(&q.items); - q.add_item(QueueItemType::Song(Uuid::new_v4())).unwrap(); - dbg!(&q.items); + for _ in 0..5 { + q.items.push(QueueItem { item: QueueItemType::Song(Uuid::new_v4()), state: QueueState::Played, source: QueueSource::Queue }); + } + for _ in 0..3 { + q.add_item(QueueItemType::Song(Uuid::new_v4()), QueueSource::Library).unwrap(); + } + dbg!(&q.items, &q.items.len()); + + for _ in 0..1 { + q.remove_item(0).inspect_err(|e| println!("{e:?}")); + dbg!(&q.items.len()); + } + + dbg!(&q.items, &q.items.len()); } \ No newline at end of file diff --git a/src/music_storage/playlist.rs b/src/music_storage/playlist.rs index 69ae390..ca1832b 100644 --- a/src/music_storage/playlist.rs +++ b/src/music_storage/playlist.rs @@ -24,6 +24,7 @@ pub enum SortOrder { } #[derive(Debug, Clone)] pub struct Playlist { + uuid: Uuid, title: String, cover: Option<AlbumArt>, tracks: Vec<Uuid>, @@ -152,6 +153,7 @@ impl Playlist { impl Default for Playlist { fn default() -> Self { Playlist { + uuid: Uuid::new_v4(), title: String::default(), cover: None, tracks: Vec::default(), From 1c14bc5ebbfa93dc448f63b57ca9e8558cdaaa52 Mon Sep 17 00:00:00 2001 From: MrDulfin <mrdulfin@mrdulfin.com> Date: Tue, 27 Feb 2024 23:28:52 -0500 Subject: [PATCH 101/136] added queue clearing functions --- src/music_controller/queue.rs | 86 ++++++++++++++++++++++++++++------- src/music_storage/library.rs | 2 +- 2 files changed, 70 insertions(+), 18 deletions(-) diff --git a/src/music_controller/queue.rs b/src/music_controller/queue.rs index 05bb35e..8764d8f 100644 --- a/src/music_controller/queue.rs +++ b/src/music_controller/queue.rs @@ -1,7 +1,8 @@ +use font::opentype::tables::font_variations::InstanceFlags; use uuid::Uuid; use crate::{music_player::Player, music_storage::library::{Album, Song, URI}}; -use std::{error::Error, ops::Add, path::Path}; +use std::{error::Error, path::Path}; #[derive(Debug, PartialEq, Clone, Copy)] pub enum QueueState { @@ -10,7 +11,7 @@ pub enum QueueState { AddHere, None, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] #[non_exhaustive] pub enum QueueItemType<'a> { Song(Uuid), @@ -18,10 +19,11 @@ pub enum QueueItemType<'a> { Album{ album: Album<'a>, shuffled: bool, - } + }, + None } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] #[non_exhaustive] pub enum QueueSource { Library, @@ -30,12 +32,23 @@ pub enum QueueSource { Queue, File, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] #[non_exhaustive] pub struct QueueItem<'a> { item: QueueItemType<'a>, state: QueueState, - source: QueueSource + source: QueueSource, + by_human: bool +} +impl QueueItem<'_> { + fn new() -> Self { + QueueItem { + item: QueueItemType::None, + state: QueueState::None, + source: QueueSource::Library, + by_human: false + } + } } @@ -66,7 +79,8 @@ impl<'a> Queue<'a> { pub fn current_index(&mut self) -> i16 { let mut i = 1; let mut e = self.items.iter().filter(|song| song.state == QueueState::Played ).collect::<Vec<&QueueItem>>().len(); - while e >= 51 { + // TODO: make the max number of past songs modular + while e > 50 { self.items.remove(0); e = self.items.iter().filter(|song| song.state == QueueState::Played ).collect::<Vec<&QueueItem>>().len(); i+=1; @@ -74,7 +88,7 @@ impl<'a> Queue<'a> { e as i16 - 1 } - pub fn add_item(&mut self, item: QueueItemType<'a>, source: QueueSource) -> Result<(), Box<dyn Error>> { + pub fn add_item(&mut self, item: QueueItemType<'a>, source: QueueSource, by_human: bool) -> Result<(), Box<dyn Error>> { use QueueState::*; let ind = self.current_index(); let mut i: i16 = 1; @@ -100,7 +114,8 @@ impl<'a> Queue<'a> { }else { AddHere }, - source + source, + by_human } ); Ok(()) @@ -114,15 +129,52 @@ impl<'a> Queue<'a> { // update the state of the next item to replace the item being removed if self.items.get(ind + 1).is_some() { self.items[ind + 1].state = self.items[ind].state; + }else if self.items[ind].state != Current { + self.items[ind - 1].state = self.items[ind].state; } self.items[ind].state = None; self.items.remove(ind); - Ok(()) }else { Err("No Songs to remove!".into()) } } + + pub fn clear(&mut self) { + self.items.retain(|item| item.state == QueueState::Played ); + } + + pub fn clear_except(&mut self, index: usize) -> Result<(), Box<dyn Error>> { + let mut index = index; + let ind = self.current_index(); + + if ind != -1 { + index += ind as usize; + }else { + index -=1 + } + + if !self.is_empty() && index < self.items.len() { + let i = self.items[index].clone(); + self.items.retain(|item| item.state == QueueState::Played || *item == i ); + self.items[(ind+1) as usize].state = QueueState::Current + }else { + return Err("index out of bounds!".into()); + } + Ok(()) + } + + pub fn clear_played(&mut self) { + self.items.retain(|item| item.state != QueueState::Played ); + } + + pub fn clear_all(&mut self) { + self.items.clear() + } + + fn is_empty(&self) -> bool { + self.items.iter().filter(|item| item.state != QueueState::Played).collect::<Vec<_>>().len() == 0 + } } @@ -130,17 +182,17 @@ impl<'a> Queue<'a> { fn item_add_test() { let mut q = Queue::new().unwrap(); for _ in 0..5 { - q.items.push(QueueItem { item: QueueItemType::Song(Uuid::new_v4()), state: QueueState::Played, source: QueueSource::Queue }); + q.items.push(QueueItem { item: QueueItemType::Song(Uuid::new_v4()), state: QueueState::Played, source: QueueSource::Queue, by_human: false }); } + q.clear(); + for _ in 0..5 { + q.add_item(QueueItemType::Song(Uuid::new_v4()), QueueSource::Library, true).unwrap(); + } + // q.clear_played(); for _ in 0..3 { - q.add_item(QueueItemType::Song(Uuid::new_v4()), QueueSource::Library).unwrap(); - } - dbg!(&q.items, &q.items.len()); - - for _ in 0..1 { q.remove_item(0).inspect_err(|e| println!("{e:?}")); - dbg!(&q.items.len()); } + q.clear_except(4).inspect_err(|e| println!("{e:?}")); dbg!(&q.items, &q.items.len()); } \ No newline at end of file diff --git a/src/music_storage/library.rs b/src/music_storage/library.rs index 93b9991..9915682 100644 --- a/src/music_storage/library.rs +++ b/src/music_storage/library.rs @@ -515,7 +515,7 @@ pub enum Service { None, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct Album<'a> { title: &'a String, artist: Option<&'a String>, From 588a9cbd94ef06e1f5a42d8881bcebb34f00ce8b Mon Sep 17 00:00:00 2001 From: MrDulfin <mrdulfin@mrdulfin.com> Date: Sun, 10 Mar 2024 19:22:31 -0400 Subject: [PATCH 102/136] Added more queue functions --- src/music_controller/queue.rs | 192 ++++++++++++++++++++++++++++++---- src/music_player.rs | 2 +- 2 files changed, 175 insertions(+), 19 deletions(-) diff --git a/src/music_controller/queue.rs b/src/music_controller/queue.rs index 8764d8f..f727fea 100644 --- a/src/music_controller/queue.rs +++ b/src/music_controller/queue.rs @@ -1,8 +1,8 @@ use font::opentype::tables::font_variations::InstanceFlags; use uuid::Uuid; -use crate::{music_player::Player, music_storage::library::{Album, Song, URI}}; -use std::{error::Error, path::Path}; +use crate::{music_player::Player, music_storage::library::{Album, MusicLibrary, Song, URI}}; +use std::{error::Error, ops::Add, path::Path, sync::{Arc, RwLock}, thread::sleep, time::Duration}; #[derive(Debug, PartialEq, Clone, Copy)] pub enum QueueState { @@ -19,8 +19,36 @@ pub enum QueueItemType<'a> { Album{ album: Album<'a>, shuffled: bool, + // disc #, track # + current: (i32, i32) }, - None + None, + Test +} +impl QueueItemType<'_> { + fn get_uri(&self, lib: Arc<RwLock<MusicLibrary>>) -> Option<URI> { + use QueueItemType::*; + + let lib = lib.read().unwrap(); + match self { + Song(uuid) => { + if let Some((song, _)) = lib.query_uuid(uuid) { + Some(song.location.clone()) + }else { + Option::None + } + }, + Album{album, shuffled, current: (disc, index)} => { + if !shuffled { + Some(album.track(*disc as usize, *index as usize).unwrap().location.clone()) + }else { + todo!() + } + }, + ExternalSong(uri) => { Some(uri.clone()) }, + _ => { Option::None } + } + } } #[derive(Debug, Clone, PartialEq)] @@ -70,12 +98,6 @@ impl<'a> Queue<'a> { ) } - pub fn set_items(&mut self, tracks: Vec<QueueItem<'a>>) { - let mut tracks = tracks; - self.items.clear(); - self.items.append(&mut tracks); - } - pub fn current_index(&mut self) -> i16 { let mut i = 1; let mut e = self.items.iter().filter(|song| song.state == QueueState::Played ).collect::<Vec<&QueueItem>>().len(); @@ -88,12 +110,27 @@ impl<'a> Queue<'a> { e as i16 - 1 } + fn contains_state(&self, state: QueueState) -> bool { + !self.items.iter().filter(|item| item.state == state ).collect::<Vec<_>>().is_empty() + } + + fn is_empty(&self) -> bool { + self.items.iter().filter(|item| item.state != QueueState::Played).collect::<Vec<_>>().is_empty() + } + + pub fn set_items(&mut self, tracks: Vec<QueueItem<'a>>) { + let mut tracks = tracks; + self.items.clear(); + self.items.append(&mut tracks); + } + pub fn add_item(&mut self, item: QueueItemType<'a>, source: QueueSource, by_human: bool) -> Result<(), Box<dyn Error>> { use QueueState::*; let ind = self.current_index(); let mut i: i16 = 1; - self.items = self.items.iter().enumerate().map(|(j, item_)| { + self.items = self.items.iter().enumerate().map(|(j, item_)| { let mut item_ = item_.to_owned(); + // get the index of the current AddHere item and give it to i if item_.state == AddHere { i = j as i16 + 1 - ind; item_.state = None; @@ -121,6 +158,33 @@ impl<'a> Queue<'a> { Ok(()) } + pub fn add_item_next(&mut self, item: QueueItemType<'a>, source: QueueSource) { + use QueueState::*; + let ind = self.current_index(); + let empty = self.is_empty(); + + self.items.insert( + // index would go out of bounds if empty ( current index = -1 ) + if empty { + (ind + 1) as usize + }else { + (ind + 2) as usize + }, + QueueItem { + item, + state: if empty { + Current + }else if self.items.get((ind + 1) as usize).is_none() || (!self.contains_state(AddHere) && self.items.get((ind + 1) as usize).is_some()) { + AddHere + }else { + None + }, + source, + by_human: true + } + ) + } + pub fn remove_item(&mut self, index: usize) -> Result<(), Box<dyn Error>> { use QueueState::*; let ind = (self.current_index() + index as i16 + 1) as usize; @@ -147,14 +211,15 @@ impl<'a> Queue<'a> { pub fn clear_except(&mut self, index: usize) -> Result<(), Box<dyn Error>> { let mut index = index; let ind = self.current_index(); + let empty = self.is_empty(); - if ind != -1 { + if !empty { index += ind as usize; }else { index -=1 } - if !self.is_empty() && index < self.items.len() { + if !empty && index < self.items.len() { let i = self.items[index].clone(); self.items.retain(|item| item.state == QueueState::Played || *item == i ); self.items[(ind+1) as usize].state = QueueState::Current @@ -172,8 +237,65 @@ impl<'a> Queue<'a> { self.items.clear() } - fn is_empty(&self) -> bool { - self.items.iter().filter(|item| item.state != QueueState::Played).collect::<Vec<_>>().len() == 0 + fn move_to(&mut self, index: usize) -> Result<(), Box<dyn Error>> { + let mut index = index; + let empty = self.is_empty(); + let ind = self.current_index(); + + if !empty { + index += ind as usize; + }else { + return Err("Nothing in the queue to move to!".into()); + } + + dbg!(1); + if !empty && index < self.items.len() -1 { + // TODO: make this check for player position + let pos = self.player.position(); + if pos.is_some_and(|dur| !dur.is_zero() ) { + self.items[ind as usize].state = QueueState::Played + } + dbg!(2); + + let to_item = self.items[index].clone(); + let new_ind = self.current_index() as usize; + dbg!(3); + + // dbg!(&self.items, &new_ind, &to_item.item, &self.items[new_ind + 1].item, &self.items.len()); + loop { + dbg!(4); + + if self.items[new_ind + 1].item != to_item.item { + self.remove_item(0); + dbg!(&self.items, &new_ind, &to_item.item, &self.items[new_ind + 1].item, &self.items.len()); + sleep(Duration::from_millis(1000)); + }else { + break; + } + } + }else { + return Err("index out of bounds!".into()); + } + Ok(()) + } + + pub fn swap(&mut self, index1: usize, index2: usize) {} + + pub fn move_item(&mut self, item: usize, to_index: usize) {} + + pub fn next() {} + + pub fn prev() {} + + pub fn enqueue_item(&mut self, item: QueueItem, lib: Arc<RwLock<MusicLibrary>>) -> Result<(), Box<dyn Error>> { + use QueueItemType::*; + + if let Some(uri) = item.item.get_uri(lib) { + self.player.enqueue_next(&uri)?; + }else { + return Err("this item does not exist!".into()); + } + Ok(()) } } @@ -185,14 +307,48 @@ fn item_add_test() { q.items.push(QueueItem { item: QueueItemType::Song(Uuid::new_v4()), state: QueueState::Played, source: QueueSource::Queue, by_human: false }); } q.clear(); - for _ in 0..5 { + for _ in 0..1 { q.add_item(QueueItemType::Song(Uuid::new_v4()), QueueSource::Library, true).unwrap(); } // q.clear_played(); - for _ in 0..3 { - q.remove_item(0).inspect_err(|e| println!("{e:?}")); + // for _ in 0..3 { + // q.remove_item(0).inspect_err(|e| println!("{e:?}")); + // } + for _ in 0..2 { + q.items.push(QueueItem { item: QueueItemType::Test, state: QueueState::None, source: QueueSource::Queue, by_human: false }); } - q.clear_except(4).inspect_err(|e| println!("{e:?}")); + q.add_item_next(QueueItemType::Test, QueueSource::File); + dbg!(&q.items, &q.items.len()); +} + +#[test] +fn test_() { + let mut q = Queue::new().unwrap(); + for _ in 0..100 { + q.items.push(QueueItem { item: QueueItemType::Song(Uuid::new_v4()), state: QueueState::Played, source: QueueSource::Queue, by_human: false }); + } + for _ in 0..2 { + q.add_item(QueueItemType::Song(Uuid::new_v4()), QueueSource::Library, true).unwrap(); + } + q.add_item_next(QueueItemType::Test, QueueSource::Queue); + + dbg!(&q.items, &q.items.len()); + +} + +#[test] +fn move_test() { + let mut q = Queue::new().unwrap(); + // for _ in 0..1 { + // q.items.push(QueueItem { item: QueueItemType::Song(Uuid::new_v4()), state: QueueState::Played, source: QueueSource::Queue, by_human: false }); + // } + for _ in 0..5 { + q.add_item(QueueItemType::Song(Uuid::new_v4()), QueueSource::Library, true).unwrap(); + } + q.add_item(QueueItemType::Test, QueueSource::Library, true).unwrap(); + dbg!(&q.items, &q.items.len()); + + q.move_to(3).inspect_err(|e| {dbg!(e);}); dbg!(&q.items, &q.items.len()); } \ No newline at end of file diff --git a/src/music_player.rs b/src/music_player.rs index 9893088..e310403 100644 --- a/src/music_player.rs +++ b/src/music_player.rs @@ -180,7 +180,7 @@ impl Player { *position_update.write().unwrap() = None; break }, - PlaybackStats::Idle | PlaybackStats::Switching => println!("waiting!"), + PlaybackStats::Idle | PlaybackStats::Switching => {}, _ => () } From 96cfbe9a50fb3dcdfaceff8184a1ecf47788438f Mon Sep 17 00:00:00 2001 From: MrDulfin <mrdulfin@mrdulfin.com> Date: Mon, 11 Mar 2024 20:29:28 -0400 Subject: [PATCH 103/136] test commit --- src/music_controller/queue.rs | 164 ++++++++++++++++++---------------- 1 file changed, 86 insertions(+), 78 deletions(-) diff --git a/src/music_controller/queue.rs b/src/music_controller/queue.rs index f727fea..516d04b 100644 --- a/src/music_controller/queue.rs +++ b/src/music_controller/queue.rs @@ -9,7 +9,7 @@ pub enum QueueState { Played, Current, AddHere, - None, + NoState, } #[derive(Debug, Clone, PartialEq)] #[non_exhaustive] @@ -56,8 +56,6 @@ impl QueueItemType<'_> { pub enum QueueSource { Library, Playlist(Uuid), - Search, - Queue, File, } #[derive(Debug, Clone, PartialEq)] @@ -72,7 +70,7 @@ impl QueueItem<'_> { fn new() -> Self { QueueItem { item: QueueItemType::None, - state: QueueState::None, + state: QueueState::NoState, source: QueueSource::Library, by_human: false } @@ -88,6 +86,9 @@ pub struct Queue<'a> { } impl<'a> Queue<'a> { + fn dbg_items(&self) { + dbg!(self.items.iter().map(|item| item.item.clone() ).collect::<Vec<QueueItemType>>(), self.items.len()); + } pub fn new() -> Result<Self, Box<dyn Error>> { Ok( Queue { @@ -98,16 +99,18 @@ impl<'a> Queue<'a> { ) } - pub fn current_index(&mut self) -> i16 { - let mut i = 1; + pub fn current_index(&mut self/* , max: usize */) -> Option<usize> { let mut e = self.items.iter().filter(|song| song.state == QueueState::Played ).collect::<Vec<&QueueItem>>().len(); // TODO: make the max number of past songs modular while e > 50 { self.items.remove(0); - e = self.items.iter().filter(|song| song.state == QueueState::Played ).collect::<Vec<&QueueItem>>().len(); - i+=1; + e -=1; + } + if e == 0 { + None + }else { + Some(e - 1) } - e as i16 - 1 } fn contains_state(&self, state: QueueState) -> bool { @@ -126,27 +129,28 @@ impl<'a> Queue<'a> { pub fn add_item(&mut self, item: QueueItemType<'a>, source: QueueSource, by_human: bool) -> Result<(), Box<dyn Error>> { use QueueState::*; - let ind = self.current_index(); - let mut i: i16 = 1; + let mut i: usize = 0; + let ind = self.current_index(); + self.items = self.items.iter().enumerate().map(|(j, item_)| { let mut item_ = item_.to_owned(); // get the index of the current AddHere item and give it to i if item_.state == AddHere { - i = j as i16 + 1 - ind; - item_.state = None; - } - if item_.state == Current { - i = j as i16 + 1 - ind; + i = j - ind.unwrap_or(0); + item_.state = NoState; + } else if item_.state == Current { + i = j - ind.unwrap_or(0); } item_ }).collect::<Vec<QueueItem>>(); - let pos = (ind + i) as usize; + + let pos = ind.unwrap_or(0) + i + if !self.is_empty() || (self.is_empty() && ind == None) { 0 } else { 1 }; // dbg!(&pos, &i, &ind); self.items.insert( pos, QueueItem { item: item.clone(), - state: if pos == self.items.len() && i == 1 { + state: if pos == self.items.len() && i == 0 { Current }else { AddHere @@ -160,24 +164,20 @@ impl<'a> Queue<'a> { pub fn add_item_next(&mut self, item: QueueItemType<'a>, source: QueueSource) { use QueueState::*; - let ind = self.current_index(); + let ind_ = self.current_index(); + let ind = ind_.unwrap_or(0); let empty = self.is_empty(); self.items.insert( - // index would go out of bounds if empty ( current index = -1 ) - if empty { - (ind + 1) as usize - }else { - (ind + 2) as usize - }, + (ind + if !empty && ind_ == None { 1 } else { 2 }), QueueItem { item, state: if empty { Current - }else if self.items.get((ind + 1) as usize).is_none() || (!self.contains_state(AddHere) && self.items.get((ind + 1) as usize).is_some()) { + }else if self.items.get(ind + 1).is_none() || (!self.contains_state(AddHere) && self.items.get(ind + 1).is_some()) { AddHere }else { - None + NoState }, source, by_human: true @@ -187,17 +187,19 @@ impl<'a> Queue<'a> { pub fn remove_item(&mut self, index: usize) -> Result<(), Box<dyn Error>> { use QueueState::*; - let ind = (self.current_index() + index as i16 + 1) as usize; + let remove_index: usize = (if let Some(current_index) = self.current_index() { dbg!(¤t_index); current_index } else { 0 } + index ); - if ind < self.items.len() { + // dbg!(/*&remove_index, self.current_index(), &index,*/ &self.items[remove_index]); + + if remove_index < self.items.len() { // update the state of the next item to replace the item being removed - if self.items.get(ind + 1).is_some() { - self.items[ind + 1].state = self.items[ind].state; - }else if self.items[ind].state != Current { - self.items[ind - 1].state = self.items[ind].state; + if self.items.get(remove_index + 1).is_some() { + self.items[remove_index + 1].state = self.items[remove_index].state; + }else if self.items[remove_index].state != Current { + self.items[remove_index - 1].state = self.items[remove_index].state; } - self.items[ind].state = None; - self.items.remove(ind); + self.items[remove_index].state = NoState; + self.items.remove(remove_index); Ok(()) }else { Err("No Songs to remove!".into()) @@ -210,11 +212,14 @@ impl<'a> Queue<'a> { pub fn clear_except(&mut self, index: usize) -> Result<(), Box<dyn Error>> { let mut index = index; - let ind = self.current_index(); + let ind = match self.current_index() { + Some(e) => e, + None => return Err("nothing to clear!".into()) + }; let empty = self.is_empty(); if !empty { - index += ind as usize; + index += ind; }else { index -=1 } @@ -222,7 +227,7 @@ impl<'a> Queue<'a> { if !empty && index < self.items.len() { let i = self.items[index].clone(); self.items.retain(|item| item.state == QueueState::Played || *item == i ); - self.items[(ind+1) as usize].state = QueueState::Current + self.items[ind+1].state = QueueState::Current }else { return Err("index out of bounds!".into()); } @@ -238,37 +243,26 @@ impl<'a> Queue<'a> { } fn move_to(&mut self, index: usize) -> Result<(), Box<dyn Error>> { - let mut index = index; let empty = self.is_empty(); - let ind = self.current_index(); + let nothing_error = Err("Nothing in the queue to move to!".into()); + let ind = self.current_index().unwrap_or(0); + let index = if !empty { index + ind } else { return nothing_error; }; - if !empty { - index += ind as usize; - }else { - return Err("Nothing in the queue to move to!".into()); - } - - dbg!(1); if !empty && index < self.items.len() -1 { - // TODO: make this check for player position - let pos = self.player.position(); - if pos.is_some_and(|dur| !dur.is_zero() ) { - self.items[ind as usize].state = QueueState::Played + let position = self.player.position(); + if position.is_some_and(|dur| !dur.is_zero() ) { + self.items[ind].state = QueueState::Played; } - dbg!(2); let to_item = self.items[index].clone(); - let new_ind = self.current_index() as usize; - dbg!(3); + let ind = self.current_index().unwrap_or(0); - // dbg!(&self.items, &new_ind, &to_item.item, &self.items[new_ind + 1].item, &self.items.len()); loop { - dbg!(4); - - if self.items[new_ind + 1].item != to_item.item { - self.remove_item(0); - dbg!(&self.items, &new_ind, &to_item.item, &self.items[new_ind + 1].item, &self.items.len()); - sleep(Duration::from_millis(1000)); + if self.items[ind].item != to_item.item { + if let Err(e) = self.remove_item(0) { + dbg!(&e); self.dbg_items(); return Err(e); + } + // dbg!(&to_item.item, &self.items[ind].item); }else { break; } @@ -303,21 +297,34 @@ impl<'a> Queue<'a> { #[test] fn item_add_test() { let mut q = Queue::new().unwrap(); - for _ in 0..5 { - q.items.push(QueueItem { item: QueueItemType::Song(Uuid::new_v4()), state: QueueState::Played, source: QueueSource::Queue, by_human: false }); - } - q.clear(); + dbg!(1); for _ in 0..1 { + q.items.push(QueueItem { item: QueueItemType::Song(Uuid::new_v4()), state: QueueState::Played, source: QueueSource::File, by_human: false }); + } + dbg!(2); + + // q.clear(); + dbg!(3); + + for _ in 0..5 { + // dbg!("tick!"); q.add_item(QueueItemType::Song(Uuid::new_v4()), QueueSource::Library, true).unwrap(); + // dbg!(&q.items, &q.items.len()); } + dbg!(4); + dbg!(&q.items, &q.items.len()); + // q.clear_played(); - // for _ in 0..3 { - // q.remove_item(0).inspect_err(|e| println!("{e:?}")); - // } - for _ in 0..2 { - q.items.push(QueueItem { item: QueueItemType::Test, state: QueueState::None, source: QueueSource::Queue, by_human: false }); + for _ in 0..1 { + q.remove_item(0).inspect_err(|e| println!("{e:?}")); } - q.add_item_next(QueueItemType::Test, QueueSource::File); + // for _ in 0..2 { + // q.items.push(QueueItem { item: QueueItemType::Test, state: QueueState::NoState, source: QueueSource::Library, by_human: false }); + // } + // dbg!(5); + + // q.add_item_next(QueueItemType::Test, QueueSource::File); + // dbg!(6); dbg!(&q.items, &q.items.len()); } @@ -325,13 +332,13 @@ fn item_add_test() { #[test] fn test_() { let mut q = Queue::new().unwrap(); - for _ in 0..100 { - q.items.push(QueueItem { item: QueueItemType::Song(Uuid::new_v4()), state: QueueState::Played, source: QueueSource::Queue, by_human: false }); + for _ in 0..1 { + q.items.push(QueueItem { item: QueueItemType::Song(Uuid::new_v4()), state: QueueState::Played, source: QueueSource::File, by_human: false }); } for _ in 0..2 { q.add_item(QueueItemType::Song(Uuid::new_v4()), QueueSource::Library, true).unwrap(); } - q.add_item_next(QueueItemType::Test, QueueSource::Queue); + q.add_item_next(QueueItemType::Test, QueueSource::File); dbg!(&q.items, &q.items.len()); @@ -340,15 +347,16 @@ fn test_() { #[test] fn move_test() { let mut q = Queue::new().unwrap(); - // for _ in 0..1 { - // q.items.push(QueueItem { item: QueueItemType::Song(Uuid::new_v4()), state: QueueState::Played, source: QueueSource::Queue, by_human: false }); - // } + for _ in 0..1 { + q.items.push(QueueItem { item: QueueItemType::Song(Uuid::new_v4()), state: QueueState::Played, source: QueueSource::File, by_human: false }); + } for _ in 0..5 { q.add_item(QueueItemType::Song(Uuid::new_v4()), QueueSource::Library, true).unwrap(); } - q.add_item(QueueItemType::Test, QueueSource::Library, true).unwrap(); + // q.add_item(QueueItemType::Test, QueueSource::Library, true).unwrap(); dbg!(&q.items, &q.items.len()); q.move_to(3).inspect_err(|e| {dbg!(e);}); dbg!(&q.items, &q.items.len()); + // q.dbg_items(); } \ No newline at end of file From 7dcca941749c739cd9674cd6b2bfe2a0c54da7e5 Mon Sep 17 00:00:00 2001 From: MrDulfin <mrdulfin@mrdulfin.com> Date: Fri, 15 Mar 2024 21:23:23 -0400 Subject: [PATCH 104/136] committing possibly broken code as a backup --- src/music_controller/queue.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/music_controller/queue.rs b/src/music_controller/queue.rs index 516d04b..fec5209 100644 --- a/src/music_controller/queue.rs +++ b/src/music_controller/queue.rs @@ -332,10 +332,10 @@ fn item_add_test() { #[test] fn test_() { let mut q = Queue::new().unwrap(); - for _ in 0..1 { + for _ in 0..400 { q.items.push(QueueItem { item: QueueItemType::Song(Uuid::new_v4()), state: QueueState::Played, source: QueueSource::File, by_human: false }); } - for _ in 0..2 { + for _ in 0..50000 { q.add_item(QueueItemType::Song(Uuid::new_v4()), QueueSource::Library, true).unwrap(); } q.add_item_next(QueueItemType::Test, QueueSource::File); From f7960518ca186eb96901213a5fb5ac0104bdbda8 Mon Sep 17 00:00:00 2001 From: G2-Games <ke0bhogsg@gmail.com> Date: Sat, 23 Mar 2024 16:07:35 -0500 Subject: [PATCH 105/136] Fixes album art discovery --- src/music_storage/library.rs | 2 +- src/music_storage/utils.rs | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/music_storage/library.rs b/src/music_storage/library.rs index 502429b..84fc2e0 100644 --- a/src/music_storage/library.rs +++ b/src/music_storage/library.rs @@ -274,7 +274,7 @@ impl Song { /// creates a `Vec<Song>` from a cue file - pub fn from_cue(cuesheet: &Path) -> Result<(Vec<(Self, PathBuf)>), Box<dyn Error>> { + pub fn from_cue(cuesheet: &Path) -> Result<Vec<(Self, PathBuf)>, Box<dyn Error>> { let mut tracks = Vec::new(); let cue_data = parse_from_file(&cuesheet.to_string_lossy(), false).unwrap(); diff --git a/src/music_storage/utils.rs b/src/music_storage/utils.rs index 0d6fb47..f4665a3 100644 --- a/src/music_storage/utils.rs +++ b/src/music_storage/utils.rs @@ -74,12 +74,9 @@ pub fn find_images(song_path: &Path) -> Result<Vec<AlbumArt>, Box<dyn Error>> { .follow_links(true) .into_iter() .filter_map(|e| e.ok()) + .filter(|e| e.depth() < 3) // Don't recurse very deep { - if target_file.depth() >= 3 { - // Don't recurse very deep - break; - } - + println!("{:?}", target_file); let path = target_file.path(); if !path.is_file() { continue; @@ -87,7 +84,7 @@ pub fn find_images(song_path: &Path) -> Result<Vec<AlbumArt>, Box<dyn Error>> { let format = FileFormat::from_file(path)?.kind(); if format != Kind::Image { - break; + continue; } let image_uri = URI::Local(path.to_path_buf().canonicalize()?); From c4c842e637e03da7d8bb2a3827433bd7cc3cca3d Mon Sep 17 00:00:00 2001 From: MrDulfin <mrdulfin@mrdulfin.com> Date: Sat, 23 Mar 2024 23:40:10 -0400 Subject: [PATCH 106/136] added function to show album art anf other small changes --- Cargo.toml | 3 ++ src/music_storage/library.rs | 85 ++++++++++++++++++++++++++++++++++-- src/music_storage/utils.rs | 18 +++++--- 3 files changed, 95 insertions(+), 11 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0267611..a3c737b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,3 +36,6 @@ font = "0.27.0" uuid = { version = "1.6.1", features = ["v4", "serde"]} serde_json = "1.0.111" deunicode = "1.4.2" +opener = { version = "0.7.0", features = ["reveal"]} +image = "0.25.0" +tempfile = "3.10.1" diff --git a/src/music_storage/library.rs b/src/music_storage/library.rs index 9915682..f91b590 100644 --- a/src/music_storage/library.rs +++ b/src/music_storage/library.rs @@ -5,16 +5,19 @@ use crate::config::config::Config; // Various std things use std::collections::BTreeMap; use std::error::Error; +use std::io::BufWriter; use std::ops::ControlFlow::{Break, Continue}; use std::ops::Deref; // Files use file_format::{FileFormat, Kind}; use glib::filename_to_uri; +use image::guess_format; use lofty::{AudioFile, ItemKey, ItemValue, ParseOptions, Probe, TagType, TaggedFileExt}; use rcue::parser::parse_from_file; +use tempfile::tempfile; use uuid::Uuid; -use std::fs; +use std::fs::{self, OpenOptions}; use std::path::{Path, PathBuf}; use walkdir::WalkDir; @@ -158,6 +161,15 @@ pub struct Song { pub tags: BTreeMap<Tag, String>, } +#[test] +fn get_art_test() { + use urlencoding::decode; + + let s = Song::from_file(Path::new("F:\\Music\\Mp3\\ななひら\\Colory Starry\\05 - Fly Away!.mp3")).unwrap(); + s.open_album_art(0).inspect_err(|e| println!("{e:?}")); + +} + impl Song { /// Get a tag's value /// @@ -298,7 +310,7 @@ impl Song { /// creates a `Vec<Song>` from a cue file - pub fn from_cue(cuesheet: &Path) -> Result<(Vec<(Self, PathBuf)>), Box<dyn Error>> { + pub fn from_cue(cuesheet: &Path) -> Result<Vec<(Self, PathBuf)>, Box<dyn Error>> { let mut tracks = Vec::new(); let cue_data = parse_from_file(&cuesheet.to_string_lossy(), false).unwrap(); @@ -422,6 +434,71 @@ impl Song { } Ok(tracks) } + + pub fn open_album_art(&self, index: usize) -> Result<(), Box<dyn Error>> { + use opener::open; + use urlencoding::decode; + + if index >= self.album_art.len() { + return Err("index out of bounds?".into()); + } + + let uri: String = match &self.album_art[index] { + AlbumArt::External(uri) => { + decode(match uri.as_uri().strip_prefix("file:///") { Some(e) => e, None => return Err("Invalid path?".into()) })?.into_owned() + }, + AlbumArt::Embedded(_) => { + + let normal_options = ParseOptions::new().parsing_mode(lofty::ParsingMode::Relaxed); + let blank_tag = &lofty::Tag::new(TagType::Id3v2); + let tagged_file: lofty::TaggedFile; + + let uri = dbg!(urlencoding::decode(self.location.as_uri().strip_prefix("file:///").unwrap())?.into_owned()); + + let tag = match Probe::open(uri)?.options(normal_options).read() { + Ok(file) => { + tagged_file = file; + + match tagged_file.primary_tag() { + Some(primary_tag) => primary_tag, + + None => match tagged_file.first_tag() { + Some(first_tag) => first_tag, + None => blank_tag, + }, + } + } + + Err(_) => blank_tag, + }; + + let data = tag.pictures()[index].data(); + let format = dbg!(guess_format(data)?); + let img = image::load_from_memory(data)?; + + let mut location = String::new(); + let i: u32 = 0; + loop { + use image::ImageFormat::*; + //TODO: create a place for temporary images + let fmt = match format { + Jpeg => "jpeg", + Png => "png", + _ => todo!(), + }; + + location = format!("./test-config/images/tempcover{i}.{fmt}.tmp"); + break; + }; + img.save_with_format(&location, format)?; + + location.to_string() + }, + }; + open(uri)?; + + Ok(()) + } } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] @@ -526,7 +603,7 @@ pub struct Album<'a> { #[allow(clippy::len_without_is_empty)] impl Album<'_> { //returns the Album title - fn title(&self) -> &String { + pub fn title(&self) -> &String { self.title } @@ -1060,4 +1137,4 @@ impl MusicLibrary { Ok(albums) } -} +} \ No newline at end of file diff --git a/src/music_storage/utils.rs b/src/music_storage/utils.rs index 0d6fb47..9d832e1 100644 --- a/src/music_storage/utils.rs +++ b/src/music_storage/utils.rs @@ -1,5 +1,7 @@ +use std::any::Any; use std::fs::{File, self}; use std::io::{BufReader, BufWriter}; +use std::os::windows::fs::MetadataExt; use std::path::{Path, PathBuf}; use std::error::Error; @@ -74,20 +76,22 @@ pub fn find_images(song_path: &Path) -> Result<Vec<AlbumArt>, Box<dyn Error>> { .follow_links(true) .into_iter() .filter_map(|e| e.ok()) + .filter(|e| e.depth() < 3) // Don't recurse very deep { - if target_file.depth() >= 3 { - // Don't recurse very deep - break; - } - + // println!("{:?}", target_file); let path = target_file.path(); - if !path.is_file() { + if !path.is_file() || !path.exists() { continue; } let format = FileFormat::from_file(path)?.kind(); if format != Kind::Image { - break; + continue; + } + + #[cfg(target_family = "windows")] + if (4 & path.metadata().unwrap().file_attributes()) == 4 { + continue; } let image_uri = URI::Local(path.to_path_buf().canonicalize()?); From 9da9b00befa600d6694e5b7ec7989580848f9008 Mon Sep 17 00:00:00 2001 From: G2-Games <ke0bhogsg@gmail.com> Date: Sat, 23 Mar 2024 23:34:47 -0500 Subject: [PATCH 107/136] Removed unused imports, moved some tests to `mod tests` --- src/config/config.rs | 93 +++++++++++--------- src/config/other_settings.rs | 4 - src/lib.rs | 2 +- src/music_controller/connections.rs | 2 - src/music_controller/controller.rs | 83 +++++++++-------- src/music_controller/queue.rs | 18 ++-- src/music_storage/db_reader/itunes/reader.rs | 35 +++++--- src/music_storage/library.rs | 56 ++++++------ src/music_storage/playlist.rs | 17 +--- src/music_storage/utils.rs | 6 +- 10 files changed, 152 insertions(+), 164 deletions(-) diff --git a/src/config/config.rs b/src/config/config.rs index 918af12..76c59cc 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -1,7 +1,7 @@ use std::{ path::PathBuf, fs::{File, OpenOptions, self}, - io::{Error, Write, Read}, sync::{Arc, RwLock}, + io::{Error, Write, Read}, }; use serde::{Deserialize, Serialize}; @@ -9,8 +9,6 @@ use serde_json::to_string_pretty; use thiserror::Error; use uuid::Uuid; -use crate::music_storage::library::{MusicLibrary, self}; - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ConfigLibrary { pub name: String, @@ -166,47 +164,54 @@ pub enum ConfigError { } -#[test] -fn config_test() { - let lib_a = ConfigLibrary::new(PathBuf::from("test-config/library1"), String::from("library1"), None); - let lib_b = ConfigLibrary::new(PathBuf::from("test-config/library2"), String::from("library2"), None); - let lib_c = ConfigLibrary::new(PathBuf::from("test-config/library3"), String::from("library3"), None); - let config = Config { - path: PathBuf::from("test-config/config_test.json"), - libraries: ConfigLibraries { - libraries: vec![ - lib_a.clone(), - lib_b.clone(), - lib_c.clone(), - ], +#[cfg(test)] +mod tests { + use std::{path::PathBuf, sync::{Arc, RwLock}}; + use crate::music_storage::library::MusicLibrary; + use super::{Config, ConfigLibraries, ConfigLibrary}; + + #[test] + fn config_test() { + let lib_a = ConfigLibrary::new(PathBuf::from("test-config/library1"), String::from("library1"), None); + let lib_b = ConfigLibrary::new(PathBuf::from("test-config/library2"), String::from("library2"), None); + let lib_c = ConfigLibrary::new(PathBuf::from("test-config/library3"), String::from("library3"), None); + let config = Config { + path: PathBuf::from("test-config/config_test.json"), + libraries: ConfigLibraries { + libraries: vec![ + lib_a.clone(), + lib_b.clone(), + lib_c.clone(), + ], + ..Default::default() + }, ..Default::default() - }, - ..Default::default() - }; - config.write_file(); - let arc = Arc::new(RwLock::from(config)); - MusicLibrary::init(arc.clone(), lib_a.uuid).unwrap(); - MusicLibrary::init(arc.clone(), lib_b.uuid).unwrap(); - MusicLibrary::init(arc.clone(), lib_c.uuid).unwrap(); + }; + config.write_file(); + let arc = Arc::new(RwLock::from(config)); + MusicLibrary::init(arc.clone(), lib_a.uuid).unwrap(); + MusicLibrary::init(arc.clone(), lib_b.uuid).unwrap(); + MusicLibrary::init(arc.clone(), lib_c.uuid).unwrap(); + } + + #[test] + fn test2() { + let config = Config::read_file(PathBuf::from("test-config/config_test.json")).unwrap(); + let uuid = config.libraries.get_default().unwrap().uuid; + let mut lib = MusicLibrary::init(Arc::new(RwLock::from(config.clone())), uuid).unwrap(); + lib.scan_folder("test-config/music/").unwrap(); + lib.save(config.clone()).unwrap(); + dbg!(&lib); + dbg!(&config); + } + + #[test] + fn test3() { + let config = Config::read_file(PathBuf::from("test-config/config_test.json")).unwrap(); + let uuid = config.libraries.get_default().unwrap().uuid; + let lib = MusicLibrary::init(Arc::new(RwLock::from(config.clone())), uuid).unwrap(); + + dbg!(lib); + } } - -#[test] -fn test2() { - let config = Config::read_file(PathBuf::from("test-config/config_test.json")).unwrap(); - let uuid = config.libraries.get_default().unwrap().uuid; - let mut lib = MusicLibrary::init(Arc::new(RwLock::from(config.clone())), uuid).unwrap(); - lib.scan_folder("test-config/music/").unwrap(); - lib.save(config.clone()).unwrap(); - dbg!(&lib); - dbg!(&config); -} - -#[test] -fn test3() { - let config = Config::read_file(PathBuf::from("test-config/config_test.json")).unwrap(); - let uuid = config.libraries.get_default().unwrap().uuid; - let lib = MusicLibrary::init(Arc::new(RwLock::from(config.clone())), uuid).unwrap(); - - dbg!(lib); -} \ No newline at end of file diff --git a/src/config/other_settings.rs b/src/config/other_settings.rs index da94670..164221f 100644 --- a/src/config/other_settings.rs +++ b/src/config/other_settings.rs @@ -1,7 +1,3 @@ -use std::{marker::PhantomData, fs::File, path::PathBuf}; - -use font::Font; - pub enum Setting { String { name: String, diff --git a/src/lib.rs b/src/lib.rs index a9d0db0..379a56c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -#![allow(unused)] +#[allow(dead_code)] pub mod music_storage { pub mod library; diff --git a/src/music_controller/connections.rs b/src/music_controller/connections.rs index 86d8f29..ac4abd9 100644 --- a/src/music_controller/connections.rs +++ b/src/music_controller/connections.rs @@ -1,5 +1,3 @@ -use std::{env, thread, time}; - use super::controller::Controller; diff --git a/src/music_controller/controller.rs b/src/music_controller/controller.rs index 8d7f08e..ebcc27f 100644 --- a/src/music_controller/controller.rs +++ b/src/music_controller/controller.rs @@ -2,33 +2,23 @@ //! player. It manages queues, playback, library access, and //! other functions -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use std::sync::{Arc, RwLock}; -use std::time::Duration; use crossbeam_channel::{Sender, Receiver}; -// use std::sync::mpsc; use crossbeam_channel; -use gstreamer::format::Default; -use gstreamer::query::Uri; -use std::thread::{self, sleep, spawn}; +use std::thread::spawn; use std::error::Error; use crossbeam_channel::unbounded; -use rayon::iter::Rev; use uuid::Uuid; -use crate::config; use crate::music_storage::library::{Tag, URI}; -use crate::music_storage::playlist::Playlist; use crate::{ - music_player::Player, music_storage::library::{MusicLibrary, Song}, config::config::Config, music_controller::queue::Queue, }; -use super::queue::{QueueItem, QueueState}; - pub struct Controller { // queues: Vec<Queue>, config: Arc<RwLock<Config>>, @@ -266,7 +256,7 @@ impl Controller { } }); self.queue_mail.push(out_thread_queue); - Ok((self.queue_mail.len() - 1)) + Ok(self.queue_mail.len() - 1) } fn q_play(&self, index: usize) -> Result<(), Box<dyn Error>> { @@ -306,36 +296,43 @@ impl Controller { } -#[test] -fn play_test() { - let mut a = match Controller::start("test-config/config_test.json".to_string()) { - Ok(c) => c, - Err(e) => panic!("{e}") - }; - sleep(Duration::from_millis(500)); +#[cfg(test)] +mod tests { + use std::{thread::sleep, time::Duration}; - let i = a.q_new().unwrap(); - a.q_set_volume(i, 0.04); - // a.new_queue(); - let songs = a.lib_get_songs(); - a.q_enqueue(i, songs[2].location.clone()); - // a.enqueue(1, songs[2].location.clone()); - a.q_play(i).unwrap(); - // a.play(1).unwrap(); + use super::Controller; - sleep(Duration::from_secs(10)); - a.q_pause(i); - sleep(Duration::from_secs(10)); - a.q_play(i); - sleep(Duration::from_secs(1000)); -} - -#[test] -fn test_() { - let a = match Controller::start("test-config/config_test.json".to_string()) { - Ok(c) => c, - Err(e) => panic!("{e}") - }; - a.lib_scan_folder("F:/Music/Mp3".to_string()); - a.lib_save(); + #[test] + fn play_test() { + let mut a = match Controller::start("test-config/config_test.json".to_string()) { + Ok(c) => c, + Err(e) => panic!("{e}") + }; + sleep(Duration::from_millis(500)); + + let i = a.q_new().unwrap(); + a.q_set_volume(i, 0.04); + // a.new_queue(); + let songs = a.lib_get_songs(); + a.q_enqueue(i, songs[2].location.clone()); + // a.enqueue(1, songs[2].location.clone()); + a.q_play(i).unwrap(); + // a.play(1).unwrap(); + + sleep(Duration::from_secs(10)); + a.q_pause(i); + sleep(Duration::from_secs(10)); + a.q_play(i); + sleep(Duration::from_secs(1000)); + } + + #[test] + fn test_() { + let a = match Controller::start("test-config/config_test.json".to_string()) { + Ok(c) => c, + Err(e) => panic!("{e}") + }; + a.lib_scan_folder("F:/Music/Mp3".to_string()); + a.lib_save(); + } } diff --git a/src/music_controller/queue.rs b/src/music_controller/queue.rs index fec5209..8dfae31 100644 --- a/src/music_controller/queue.rs +++ b/src/music_controller/queue.rs @@ -1,8 +1,12 @@ -use font::opentype::tables::font_variations::InstanceFlags; use uuid::Uuid; - -use crate::{music_player::Player, music_storage::library::{Album, MusicLibrary, Song, URI}}; -use std::{error::Error, ops::Add, path::Path, sync::{Arc, RwLock}, thread::sleep, time::Duration}; +use crate::{ + music_player::Player, + music_storage::library::{Album, MusicLibrary, URI} +}; +use std::{ + error::Error, + sync::{Arc, RwLock} +}; #[derive(Debug, PartialEq, Clone, Copy)] pub enum QueueState { @@ -169,7 +173,7 @@ impl<'a> Queue<'a> { let empty = self.is_empty(); self.items.insert( - (ind + if !empty && ind_ == None { 1 } else { 2 }), + ind + if !empty && ind_ == None { 1 } else { 2 }, QueueItem { item, state: if empty { @@ -282,8 +286,6 @@ impl<'a> Queue<'a> { pub fn prev() {} pub fn enqueue_item(&mut self, item: QueueItem, lib: Arc<RwLock<MusicLibrary>>) -> Result<(), Box<dyn Error>> { - use QueueItemType::*; - if let Some(uri) = item.item.get_uri(lib) { self.player.enqueue_next(&uri)?; }else { @@ -359,4 +361,4 @@ fn move_test() { q.move_to(3).inspect_err(|e| {dbg!(e);}); dbg!(&q.items, &q.items.len()); // q.dbg_items(); -} \ No newline at end of file +} diff --git a/src/music_storage/db_reader/itunes/reader.rs b/src/music_storage/db_reader/itunes/reader.rs index decf390..90a48f2 100644 --- a/src/music_storage/db_reader/itunes/reader.rs +++ b/src/music_storage/db_reader/itunes/reader.rs @@ -7,15 +7,13 @@ use std::collections::{BTreeMap, HashMap}; use std::fs::File; use std::path::{Path, PathBuf}; use std::str::FromStr; -use std::sync::{Arc, RwLock}; use std::time::Duration as StdDur; use std::vec::Vec; use chrono::prelude::*; -use crate::config::config::{Config, ConfigLibrary}; use crate::music_storage::db_reader::extern_library::ExternalLibrary; -use crate::music_storage::library::{AlbumArt, MusicLibrary, Service, Song, Tag, URI, BannedType}; +use crate::music_storage::library::{AlbumArt, Service, Song, Tag, URI}; use crate::music_storage::utils; use urlencoding::decode; @@ -331,18 +329,27 @@ impl ITunesSong { } } -#[test] -fn itunes_lib_test() { - let mut config = Config::read_file(PathBuf::from("test-config/config_test.json")).unwrap(); - let config_lib = ConfigLibrary::new(PathBuf::from("test-config/library2"), String::from("library2"), None); - config.libraries.libraries.push(config_lib.clone()); +#[cfg(test)] +mod tests { + use std::{path::{Path, PathBuf}, sync::{Arc, RwLock}}; - let songs = ITunesLibrary::from_file(Path::new("test-config\\iTunesLib.xml")).to_songs(); + use crate::{config::config::{Config, ConfigLibrary}, music_storage::{db_reader::extern_library::ExternalLibrary, library::MusicLibrary}}; - let mut library = MusicLibrary::init(Arc::new(RwLock::from(config.clone())), config_lib.uuid).unwrap(); + use super::ITunesLibrary; - songs.iter().for_each(|song| library.add_song(song.to_owned()).unwrap()); + #[test] + fn itunes_lib_test() { + let mut config = Config::read_file(PathBuf::from("test-config/config_test.json")).unwrap(); + let config_lib = ConfigLibrary::new(PathBuf::from("test-config/library2"), String::from("library2"), None); + config.libraries.libraries.push(config_lib.clone()); - config.write_file().unwrap(); - library.save(config).unwrap(); -} \ No newline at end of file + let songs = ITunesLibrary::from_file(Path::new("test-config\\iTunesLib.xml")).to_songs(); + + let mut library = MusicLibrary::init(Arc::new(RwLock::from(config.clone())), config_lib.uuid).unwrap(); + + songs.iter().for_each(|song| library.add_song(song.to_owned()).unwrap()); + + config.write_file().unwrap(); + library.save(config).unwrap(); + } +} diff --git a/src/music_storage/library.rs b/src/music_storage/library.rs index f91b590..8c547c0 100644 --- a/src/music_storage/library.rs +++ b/src/music_storage/library.rs @@ -5,9 +5,7 @@ use crate::config::config::Config; // Various std things use std::collections::BTreeMap; use std::error::Error; -use std::io::BufWriter; use std::ops::ControlFlow::{Break, Continue}; -use std::ops::Deref; // Files use file_format::{FileFormat, Kind}; @@ -15,11 +13,12 @@ use glib::filename_to_uri; use image::guess_format; use lofty::{AudioFile, ItemKey, ItemValue, ParseOptions, Probe, TagType, TaggedFileExt}; use rcue::parser::parse_from_file; -use tempfile::tempfile; use uuid::Uuid; -use std::fs::{self, OpenOptions}; +use std::fs; +use tempfile::TempDir; use std::path::{Path, PathBuf}; use walkdir::WalkDir; +use image::ImageFormat::*; // Time use chrono::{serde::ts_milliseconds_option, DateTime, Utc}; @@ -163,11 +162,8 @@ pub struct Song { #[test] fn get_art_test() { - use urlencoding::decode; - let s = Song::from_file(Path::new("F:\\Music\\Mp3\\ななひら\\Colory Starry\\05 - Fly Away!.mp3")).unwrap(); - s.open_album_art(0).inspect_err(|e| println!("{e:?}")); - + s.open_album_art(0).inspect_err(|e| println!("{e:?}")).unwrap(); } impl Song { @@ -440,22 +436,24 @@ impl Song { use urlencoding::decode; if index >= self.album_art.len() { - return Err("index out of bounds?".into()); + return Err("Index out of bounds".into()); } - let uri: String = match &self.album_art[index] { + let uri = match &self.album_art[index] { AlbumArt::External(uri) => { - decode(match uri.as_uri().strip_prefix("file:///") { Some(e) => e, None => return Err("Invalid path?".into()) })?.into_owned() + PathBuf::from(decode(match uri.as_uri().strip_prefix("file:///") { + Some(e) => e, + None => return Err("Invalid path?".into()) + })?.to_owned().to_string()) }, AlbumArt::Embedded(_) => { - let normal_options = ParseOptions::new().parsing_mode(lofty::ParsingMode::Relaxed); let blank_tag = &lofty::Tag::new(TagType::Id3v2); let tagged_file: lofty::TaggedFile; - let uri = dbg!(urlencoding::decode(self.location.as_uri().strip_prefix("file:///").unwrap())?.into_owned()); + let uri = urlencoding::decode(self.location.as_uri().strip_prefix("file://").unwrap())?.into_owned(); - let tag = match Probe::open(uri)?.options(normal_options).read() { + let tag = match Probe::open(uri).unwrap().options(normal_options).read() { Ok(file) => { tagged_file = file; @@ -476,26 +474,22 @@ impl Song { let format = dbg!(guess_format(data)?); let img = image::load_from_memory(data)?; - let mut location = String::new(); - let i: u32 = 0; - loop { - use image::ImageFormat::*; - //TODO: create a place for temporary images - let fmt = match format { - Jpeg => "jpeg", - Png => "png", - _ => todo!(), - }; - - location = format!("./test-config/images/tempcover{i}.{fmt}.tmp"); - break; + let tmp_dir = TempDir::new()?; + let fmt = match format { + Jpeg => "jpeg", + Png => "png", + _ => todo!(), }; - img.save_with_format(&location, format)?; - location.to_string() + let file_path = tmp_dir.path().join(format!("{}.{fmt}", self.uuid)); + + open(&file_path).unwrap(); + img.save_with_format(&file_path, format).unwrap(); + + file_path }, }; - open(uri)?; + dbg!(open(uri)?); Ok(()) } @@ -1137,4 +1131,4 @@ impl MusicLibrary { Ok(albums) } -} \ No newline at end of file +} diff --git a/src/music_storage/playlist.rs b/src/music_storage/playlist.rs index ca1832b..783a293 100644 --- a/src/music_storage/playlist.rs +++ b/src/music_storage/playlist.rs @@ -1,21 +1,10 @@ -use std::{fs::File, path::{Path, PathBuf}, io::{Read, Error}}; +use std::{fs::File, io::{Read, Error}}; -use bincode::config; use chrono::Duration; use uuid::Uuid; -// use walkdir::Error; +use super::library::{AlbumArt, Song, Tag}; -use crate::music_controller::controller::Controller; - -use super::{ - library::{AlbumArt, Song, Tag}, - music_collection::MusicCollection, db_reader::{ - itunes::reader::ITunesLibrary, - extern_library::ExternalLibrary - }, -}; - -use m3u8_rs::{MediaPlaylist, MediaPlaylistType, MediaSegment, Playlist as List2, MasterPlaylist}; +use m3u8_rs::{MediaPlaylist, MediaPlaylistType, MediaSegment, Playlist as List2}; #[derive(Debug, Clone)] pub enum SortOrder { diff --git a/src/music_storage/utils.rs b/src/music_storage/utils.rs index 9d832e1..1f68a8c 100644 --- a/src/music_storage/utils.rs +++ b/src/music_storage/utils.rs @@ -1,10 +1,7 @@ -use std::any::Any; use std::fs::{File, self}; use std::io::{BufReader, BufWriter}; -use std::os::windows::fs::MetadataExt; use std::path::{Path, PathBuf}; use std::error::Error; - use walkdir::WalkDir; use file_format::{FileFormat, Kind}; use snap; @@ -12,6 +9,9 @@ use deunicode::deunicode_with_tofu; use super::library::{AlbumArt, URI}; +#[cfg(target_family = "windows")] +use std::os::windows::fs::MetadataExt; + pub(super) fn normalize(input_string: &str) -> String { // Normalize the string to latin characters... this needs a lot of work let mut normalized = deunicode_with_tofu(input_string, " "); From 162982ef868cda38161c223d9b754fb82033be58 Mon Sep 17 00:00:00 2001 From: MrDulfin <mrdulfin@mrdulfin.com> Date: Sun, 24 Mar 2024 21:36:22 -0400 Subject: [PATCH 108/136] Updated album art viewer function to --- src/lib.rs | 2 - src/music_controller/queue.rs | 312 ++++++++++++++++++++-------------- src/music_storage/library.rs | 75 +++++--- 3 files changed, 235 insertions(+), 154 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 379a56c..387a488 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,3 @@ -#[allow(dead_code)] - pub mod music_storage { pub mod library; pub mod music_collection; diff --git a/src/music_controller/queue.rs b/src/music_controller/queue.rs index 8dfae31..6180e9d 100644 --- a/src/music_controller/queue.rs +++ b/src/music_controller/queue.rs @@ -1,6 +1,6 @@ use uuid::Uuid; use crate::{ - music_player::Player, + music_player::{Player, PlayerError}, music_storage::library::{Album, MusicLibrary, URI} }; use std::{ @@ -8,13 +8,25 @@ use std::{ sync::{Arc, RwLock} }; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum QueueError { + #[error("Index out of bounds! Index {0} is over len {1}")] + OutOfBounds(usize, usize), + #[error("The Queue is empty!")] + EmptyQueue + +} + #[derive(Debug, PartialEq, Clone, Copy)] pub enum QueueState { Played, - Current, + First, AddHere, NoState, } + #[derive(Debug, Clone, PartialEq)] #[non_exhaustive] pub enum QueueItemType<'a> { @@ -23,12 +35,20 @@ pub enum QueueItemType<'a> { Album{ album: Album<'a>, shuffled: bool, + order: Option<Vec<Uuid>>, // disc #, track # current: (i32, i32) }, + Playlist { + uuid: Uuid, + shuffled: bool, + order: Option<Vec<Uuid>>, + current: Uuid + }, None, Test } + impl QueueItemType<'_> { fn get_uri(&self, lib: Arc<RwLock<MusicLibrary>>) -> Option<URI> { use QueueItemType::*; @@ -42,7 +62,7 @@ impl QueueItemType<'_> { Option::None } }, - Album{album, shuffled, current: (disc, index)} => { + Album{album, shuffled, current: (disc, index), ..} => { if !shuffled { Some(album.track(*disc as usize, *index as usize).unwrap().location.clone()) }else { @@ -55,19 +75,23 @@ impl QueueItemType<'_> { } } +// TODO: move this to a different location to be used elsewhere #[derive(Debug, Clone, PartialEq)] #[non_exhaustive] -pub enum QueueSource { +pub enum PlayerLocation { + Test, Library, Playlist(Uuid), File, + Custom } + #[derive(Debug, Clone, PartialEq)] #[non_exhaustive] pub struct QueueItem<'a> { item: QueueItemType<'a>, state: QueueState, - source: QueueSource, + source: PlayerLocation, by_human: bool } impl QueueItem<'_> { @@ -75,7 +99,7 @@ impl QueueItem<'_> { QueueItem { item: QueueItemType::None, state: QueueState::NoState, - source: QueueSource::Library, + source: PlayerLocation::Library, by_human: false } } @@ -87,111 +111,83 @@ pub struct Queue<'a> { pub player: Player, pub name: String, pub items: Vec<QueueItem<'a>>, + pub played: Vec<QueueItem<'a>>, + pub loop_: bool } impl<'a> Queue<'a> { + fn has_addhere(&self) -> bool { + for item in &self.items { + if item.state == QueueState::AddHere { + return true + } + } + false + } + fn dbg_items(&self) { dbg!(self.items.iter().map(|item| item.item.clone() ).collect::<Vec<QueueItemType>>(), self.items.len()); } - pub fn new() -> Result<Self, Box<dyn Error>> { + + pub fn new() -> Result<Self, PlayerError> { Ok( Queue { player: Player::new()?, name: String::new(), - items: Vec::new() + items: Vec::new(), + played: Vec::new(), + loop_: false, } ) } - pub fn current_index(&mut self/* , max: usize */) -> Option<usize> { - let mut e = self.items.iter().filter(|song| song.state == QueueState::Played ).collect::<Vec<&QueueItem>>().len(); - // TODO: make the max number of past songs modular - while e > 50 { - self.items.remove(0); - e -=1; - } - if e == 0 { - None - }else { - Some(e - 1) - } - } - - fn contains_state(&self, state: QueueState) -> bool { - !self.items.iter().filter(|item| item.state == state ).collect::<Vec<_>>().is_empty() - } - - fn is_empty(&self) -> bool { - self.items.iter().filter(|item| item.state != QueueState::Played).collect::<Vec<_>>().is_empty() - } - pub fn set_items(&mut self, tracks: Vec<QueueItem<'a>>) { let mut tracks = tracks; self.items.clear(); self.items.append(&mut tracks); } - pub fn add_item(&mut self, item: QueueItemType<'a>, source: QueueSource, by_human: bool) -> Result<(), Box<dyn Error>> { - use QueueState::*; + pub fn add_item(&mut self, item: QueueItemType<'a>, source: PlayerLocation, by_human: bool) { let mut i: usize = 0; - let ind = self.current_index(); self.items = self.items.iter().enumerate().map(|(j, item_)| { let mut item_ = item_.to_owned(); // get the index of the current AddHere item and give it to i - if item_.state == AddHere { - i = j - ind.unwrap_or(0); - item_.state = NoState; - } else if item_.state == Current { - i = j - ind.unwrap_or(0); + if item_.state == QueueState::AddHere { + i = j; + item_.state = QueueState::NoState; } item_ }).collect::<Vec<QueueItem>>(); - let pos = ind.unwrap_or(0) + i + if !self.is_empty() || (self.is_empty() && ind == None) { 0 } else { 1 }; - // dbg!(&pos, &i, &ind); - self.items.insert( - pos, - QueueItem { - item: item.clone(), - state: if pos == self.items.len() && i == 0 { - Current - }else { - AddHere - }, - source, - by_human - } - ); - Ok(()) + self.items.insert(i + if self.items.is_empty() { 0 } else { 1 }, QueueItem { + item, + state: QueueState::AddHere, + source, + by_human + }); } - pub fn add_item_next(&mut self, item: QueueItemType<'a>, source: QueueSource) { + pub fn add_item_next(&mut self, item: QueueItemType<'a>, source: PlayerLocation) { use QueueState::*; - let ind_ = self.current_index(); - let ind = ind_.unwrap_or(0); - let empty = self.is_empty(); + let empty = self.items.is_empty(); self.items.insert( - ind + if !empty && ind_ == None { 1 } else { 2 }, + (if empty { 0 } else { 1 }), QueueItem { item, - state: if empty { - Current - }else if self.items.get(ind + 1).is_none() || (!self.contains_state(AddHere) && self.items.get(ind + 1).is_some()) { - AddHere - }else { - NoState - }, + state: if (self.items.get(1).is_none() || (!self.has_addhere() && self.items.get(1).is_some()) || empty) { AddHere } else { NoState }, source, by_human: true } ) } - pub fn remove_item(&mut self, index: usize) -> Result<(), Box<dyn Error>> { - use QueueState::*; - let remove_index: usize = (if let Some(current_index) = self.current_index() { dbg!(¤t_index); current_index } else { 0 } + index ); + pub fn add_multi(&mut self, items: Vec<QueueItemType>, source: PlayerLocation, by_human: bool) { + + } + + pub fn remove_item(&mut self, remove_index: usize) -> Result<(), QueueError> { // dbg!(/*&remove_index, self.current_index(), &index,*/ &self.items[remove_index]); @@ -199,39 +195,29 @@ impl<'a> Queue<'a> { // update the state of the next item to replace the item being removed if self.items.get(remove_index + 1).is_some() { self.items[remove_index + 1].state = self.items[remove_index].state; - }else if self.items[remove_index].state != Current { - self.items[remove_index - 1].state = self.items[remove_index].state; } - self.items[remove_index].state = NoState; + self.items[remove_index].state = QueueState::NoState; self.items.remove(remove_index); Ok(()) }else { - Err("No Songs to remove!".into()) + Err(QueueError::EmptyQueue) } } pub fn clear(&mut self) { - self.items.retain(|item| item.state == QueueState::Played ); + self.items.clear(); } pub fn clear_except(&mut self, index: usize) -> Result<(), Box<dyn Error>> { - let mut index = index; - let ind = match self.current_index() { - Some(e) => e, - None => return Err("nothing to clear!".into()) - }; - let empty = self.is_empty(); - - if !empty { - index += ind; - }else { - index -=1 - } + use QueueState::*; + let empty = self.items.is_empty(); if !empty && index < self.items.len() { let i = self.items[index].clone(); - self.items.retain(|item| item.state == QueueState::Played || *item == i ); - self.items[ind+1].state = QueueState::Current + self.items.retain(|item| *item == i ); + self.items[0].state = AddHere; + }else if empty { + return Err("Queue is empty!".into()); }else { return Err("index out of bounds!".into()); } @@ -239,49 +225,117 @@ impl<'a> Queue<'a> { } pub fn clear_played(&mut self) { - self.items.retain(|item| item.state != QueueState::Played ); + self.played.clear(); } pub fn clear_all(&mut self) { - self.items.clear() + self.items.clear(); + self.played.clear(); } - fn move_to(&mut self, index: usize) -> Result<(), Box<dyn Error>> { - let empty = self.is_empty(); - let nothing_error = Err("Nothing in the queue to move to!".into()); - let ind = self.current_index().unwrap_or(0); - let index = if !empty { index + ind } else { return nothing_error; }; - if !empty && index < self.items.len() -1 { + // TODO: uh, fix this? + fn move_to(&mut self, index: usize) -> Result<(), QueueError> { + use QueueState::*; + + let empty = self.items.is_empty(); + let nothing_error = Err(QueueError::EmptyQueue); + let index = if !empty { index } else { return nothing_error; }; + + if !empty && index < self.items.len() { let position = self.player.position(); if position.is_some_and(|dur| !dur.is_zero() ) { - self.items[ind].state = QueueState::Played; + self.played.push(self.items[0].clone()); } let to_item = self.items[index].clone(); - let ind = self.current_index().unwrap_or(0); loop { - if self.items[ind].item != to_item.item { + let empty = !self.items.is_empty(); + let item = self.items[0].item.to_owned(); + + if item != to_item.item && !empty { + if self.items[0].state == AddHere && self.items.get(1).is_some() { + self.items[1].state = AddHere; + } if let Err(e) = self.remove_item(0) { dbg!(&e); self.dbg_items(); return Err(e); } // dbg!(&to_item.item, &self.items[ind].item); + }else if empty { + return nothing_error; }else { break; } } }else { - return Err("index out of bounds!".into()); + return Err(QueueError::EmptyQueue.into()); } Ok(()) } - pub fn swap(&mut self, index1: usize, index2: usize) {} + pub fn swap(&mut self, a: usize, b: usize) { + self.items.swap(a, b) + } - pub fn move_item(&mut self, item: usize, to_index: usize) {} + pub fn move_item(&mut self, a: usize, b: usize) { + let item = self.items[a].to_owned(); + if a != b { + self.items.remove(a); + } + self.items.insert(b, item); + } - pub fn next() {} + #[allow(clippy::should_implement_trait)] + pub fn next(&mut self, lib: Arc<RwLock<MusicLibrary>>) -> Result<OutQueue, Box<dyn Error>> { + + + + if self.items.is_empty() { + if self.loop_ { + return Err(QueueError::EmptyQueue.into()); // TODO: add function to loop the queue + }else { + return Err(QueueError::EmptyQueue.into()); + } + } + // TODO: add an algorithm to detect if the song should be skipped + let item = self.items[0].clone(); + let uri: URI = match &self.items[1].item { + QueueItemType::Song(uuid) => { + // TODO: Refactor later for multiple URIs + match &lib.read().unwrap().query_uuid(uuid) { + Some(song) => song.0.location.clone(), + None => return Err("Uuid does not exist!".into()), + } + }, + QueueItemType::Album { album, current, ..} => { + let (disc, track) = (current.0 as usize, current.1 as usize); + match album.track(disc, track) { + Some(track) => track.location.clone(), + None => return Err(format!("Track in Album {} at disc {} track {} does not exist!", album.title(), disc, track).into()) + } + }, + QueueItemType::Playlist { current, .. } => { + // TODO: Refactor later for multiple URIs + match &lib.read().unwrap().query_uuid(current) { + Some(song) => song.0.location.clone(), + None => return Err("Uuid does not exist!".into()), + } + }, + _ => todo!() + }; + if !self.player.is_paused() { + self.player.enqueue_next(&uri)?; + self.player.play()? + } + if self.items[0].state == QueueState::AddHere || !self.has_addhere() { + self.items[1].state = QueueState::AddHere; + } + self.played.push(item); + self.items.remove(0); + + Ok(todo!()) + } pub fn prev() {} @@ -293,40 +347,42 @@ impl<'a> Queue<'a> { } Ok(()) } + pub fn check_played(&mut self) { + while self.played.len() > 50 { + self.played.remove(0); + } + } +} + +pub struct OutQueue { + +} + +pub enum OutQueueItem { + } #[test] fn item_add_test() { let mut q = Queue::new().unwrap(); - dbg!(1); - for _ in 0..1 { - q.items.push(QueueItem { item: QueueItemType::Song(Uuid::new_v4()), state: QueueState::Played, source: QueueSource::File, by_human: false }); - } - dbg!(2); - - // q.clear(); - dbg!(3); for _ in 0..5 { // dbg!("tick!"); - q.add_item(QueueItemType::Song(Uuid::new_v4()), QueueSource::Library, true).unwrap(); + q.add_item(QueueItemType::Song(Uuid::new_v4()), PlayerLocation::Library, true); // dbg!(&q.items, &q.items.len()); } - dbg!(4); - dbg!(&q.items, &q.items.len()); - // q.clear_played(); for _ in 0..1 { q.remove_item(0).inspect_err(|e| println!("{e:?}")); } - // for _ in 0..2 { - // q.items.push(QueueItem { item: QueueItemType::Test, state: QueueState::NoState, source: QueueSource::Library, by_human: false }); - // } - // dbg!(5); + for _ in 0..2 { + q.items.push(QueueItem { item: QueueItemType::Test, state: QueueState::NoState, source: PlayerLocation::Library, by_human: false }); + } + dbg!(5); - // q.add_item_next(QueueItemType::Test, QueueSource::File); - // dbg!(6); + q.add_item_next(QueueItemType::Test, PlayerLocation::Test); + dbg!(6); dbg!(&q.items, &q.items.len()); } @@ -335,25 +391,23 @@ fn item_add_test() { fn test_() { let mut q = Queue::new().unwrap(); for _ in 0..400 { - q.items.push(QueueItem { item: QueueItemType::Song(Uuid::new_v4()), state: QueueState::Played, source: QueueSource::File, by_human: false }); + q.items.push(QueueItem { item: QueueItemType::Song(Uuid::new_v4()), state: QueueState::NoState, source: PlayerLocation::File, by_human: false }); } for _ in 0..50000 { - q.add_item(QueueItemType::Song(Uuid::new_v4()), QueueSource::Library, true).unwrap(); + q.add_item(QueueItemType::Song(Uuid::new_v4()), PlayerLocation::Library, true); } - q.add_item_next(QueueItemType::Test, QueueSource::File); + // q.add_item_next(QueueItemType::Test, PlayerLocation::File); - dbg!(&q.items, &q.items.len()); + // dbg!(&q.items, &q.items.len()); } #[test] fn move_test() { let mut q = Queue::new().unwrap(); - for _ in 0..1 { - q.items.push(QueueItem { item: QueueItemType::Song(Uuid::new_v4()), state: QueueState::Played, source: QueueSource::File, by_human: false }); - } + for _ in 0..5 { - q.add_item(QueueItemType::Song(Uuid::new_v4()), QueueSource::Library, true).unwrap(); + q.add_item(QueueItemType::Song(Uuid::new_v4()), PlayerLocation::Library, true); } // q.add_item(QueueItemType::Test, QueueSource::Library, true).unwrap(); dbg!(&q.items, &q.items.len()); diff --git a/src/music_storage/library.rs b/src/music_storage/library.rs index 8c547c0..1a198f8 100644 --- a/src/music_storage/library.rs +++ b/src/music_storage/library.rs @@ -5,7 +5,9 @@ use crate::config::config::Config; // Various std things use std::collections::BTreeMap; use std::error::Error; +use std::io::Write; use std::ops::ControlFlow::{Break, Continue}; +use std::thread::sleep; // Files use file_format::{FileFormat, Kind}; @@ -14,7 +16,7 @@ use image::guess_format; use lofty::{AudioFile, ItemKey, ItemValue, ParseOptions, Probe, TagType, TaggedFileExt}; use rcue::parser::parse_from_file; use uuid::Uuid; -use std::fs; +use std::fs::{self, File}; use tempfile::TempDir; use std::path::{Path, PathBuf}; use walkdir::WalkDir; @@ -160,11 +162,6 @@ pub struct Song { pub tags: BTreeMap<Tag, String>, } -#[test] -fn get_art_test() { - let s = Song::from_file(Path::new("F:\\Music\\Mp3\\ななひら\\Colory Starry\\05 - Fly Away!.mp3")).unwrap(); - s.open_album_art(0).inspect_err(|e| println!("{e:?}")).unwrap(); -} impl Song { /// Get a tag's value @@ -431,7 +428,7 @@ impl Song { Ok(tracks) } - pub fn open_album_art(&self, index: usize) -> Result<(), Box<dyn Error>> { + pub fn open_album_art(&self, index: usize, temp_dir: &TempDir) -> Result<(), Box<dyn Error>> { use opener::open; use urlencoding::decode; @@ -451,9 +448,21 @@ impl Song { let blank_tag = &lofty::Tag::new(TagType::Id3v2); let tagged_file: lofty::TaggedFile; - let uri = urlencoding::decode(self.location.as_uri().strip_prefix("file://").unwrap())?.into_owned(); + #[cfg(windows)] + let uri = urlencoding::decode( + match self.location.as_uri().strip_prefix("file:///") { + Some(str) => str, + None => return Err("invalid path.. again?".into()) + })?.into_owned(); - let tag = match Probe::open(uri).unwrap().options(normal_options).read() { + #[cfg(unix)] + let uri = urlencoding::decode( + match self.location.as_uri().strip_prefix("file://") { + Some(str) => str, + None => return Err("invalid path.. again?".into()) + })?.into_owned(); + + let tag = match Probe::open(uri)?.options(normal_options).read() { Ok(file) => { tagged_file = file; @@ -471,26 +480,16 @@ impl Song { }; let data = tag.pictures()[index].data(); - let format = dbg!(guess_format(data)?); - let img = image::load_from_memory(data)?; - let tmp_dir = TempDir::new()?; - let fmt = match format { - Jpeg => "jpeg", - Png => "png", - _ => todo!(), - }; + let fmt = FileFormat::from_bytes(data); + let file_path = temp_dir.path().join(format!("{}_{index}.{}", self.uuid, fmt.extension())); - let file_path = tmp_dir.path().join(format!("{}.{fmt}", self.uuid)); - - open(&file_path).unwrap(); - img.save_with_format(&file_path, format).unwrap(); + File::create(&file_path)?.write_all(data)?; file_path }, }; - dbg!(open(uri)?); - + dbg!(open(dbg!(uri))?); Ok(()) } } @@ -558,6 +557,14 @@ impl URI { path_str.to_string() } + pub fn as_path(&self) -> Result<&PathBuf, Box<dyn Error>> { + if let Self::Local(path) = self { + Ok(path) + }else { + Err("This URI is not local!".into()) + } + } + pub fn exists(&self) -> Result<bool, std::io::Error> { match self { URI::Local(loc) => loc.try_exists(), @@ -1132,3 +1139,25 @@ impl MusicLibrary { Ok(albums) } } + +#[cfg(test)] +mod test { + use std::{path::Path, thread::sleep, time::{Duration, Instant}}; + + use tempfile::TempDir; + + use super::Song; + + + #[test] + fn get_art_test() { + let s = Song::from_file(Path::new(".\\test-config\\music\\Snail_s House - Hot Milk.mp3")).unwrap(); + let dir = &TempDir::new().unwrap(); + + let now = Instant::now(); + _ = s.open_album_art(0, dir).inspect_err(|e| println!("{e:?}")); + println!("{}ms", now.elapsed().as_millis() ); + + sleep(Duration::from_secs(1)); + } +} \ No newline at end of file From 259dbec3a0f0ad0720ae40f3557c0d6d786e721a Mon Sep 17 00:00:00 2001 From: MrDulfin <mrdulfin@mrdulfin.com> Date: Sun, 24 Mar 2024 21:36:22 -0400 Subject: [PATCH 109/136] Updated album art viewer function --- src/lib.rs | 2 - src/music_controller/queue.rs | 312 ++++++++++++++++++++-------------- src/music_storage/library.rs | 75 +++++--- 3 files changed, 235 insertions(+), 154 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 379a56c..387a488 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,3 @@ -#[allow(dead_code)] - pub mod music_storage { pub mod library; pub mod music_collection; diff --git a/src/music_controller/queue.rs b/src/music_controller/queue.rs index 8dfae31..6180e9d 100644 --- a/src/music_controller/queue.rs +++ b/src/music_controller/queue.rs @@ -1,6 +1,6 @@ use uuid::Uuid; use crate::{ - music_player::Player, + music_player::{Player, PlayerError}, music_storage::library::{Album, MusicLibrary, URI} }; use std::{ @@ -8,13 +8,25 @@ use std::{ sync::{Arc, RwLock} }; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum QueueError { + #[error("Index out of bounds! Index {0} is over len {1}")] + OutOfBounds(usize, usize), + #[error("The Queue is empty!")] + EmptyQueue + +} + #[derive(Debug, PartialEq, Clone, Copy)] pub enum QueueState { Played, - Current, + First, AddHere, NoState, } + #[derive(Debug, Clone, PartialEq)] #[non_exhaustive] pub enum QueueItemType<'a> { @@ -23,12 +35,20 @@ pub enum QueueItemType<'a> { Album{ album: Album<'a>, shuffled: bool, + order: Option<Vec<Uuid>>, // disc #, track # current: (i32, i32) }, + Playlist { + uuid: Uuid, + shuffled: bool, + order: Option<Vec<Uuid>>, + current: Uuid + }, None, Test } + impl QueueItemType<'_> { fn get_uri(&self, lib: Arc<RwLock<MusicLibrary>>) -> Option<URI> { use QueueItemType::*; @@ -42,7 +62,7 @@ impl QueueItemType<'_> { Option::None } }, - Album{album, shuffled, current: (disc, index)} => { + Album{album, shuffled, current: (disc, index), ..} => { if !shuffled { Some(album.track(*disc as usize, *index as usize).unwrap().location.clone()) }else { @@ -55,19 +75,23 @@ impl QueueItemType<'_> { } } +// TODO: move this to a different location to be used elsewhere #[derive(Debug, Clone, PartialEq)] #[non_exhaustive] -pub enum QueueSource { +pub enum PlayerLocation { + Test, Library, Playlist(Uuid), File, + Custom } + #[derive(Debug, Clone, PartialEq)] #[non_exhaustive] pub struct QueueItem<'a> { item: QueueItemType<'a>, state: QueueState, - source: QueueSource, + source: PlayerLocation, by_human: bool } impl QueueItem<'_> { @@ -75,7 +99,7 @@ impl QueueItem<'_> { QueueItem { item: QueueItemType::None, state: QueueState::NoState, - source: QueueSource::Library, + source: PlayerLocation::Library, by_human: false } } @@ -87,111 +111,83 @@ pub struct Queue<'a> { pub player: Player, pub name: String, pub items: Vec<QueueItem<'a>>, + pub played: Vec<QueueItem<'a>>, + pub loop_: bool } impl<'a> Queue<'a> { + fn has_addhere(&self) -> bool { + for item in &self.items { + if item.state == QueueState::AddHere { + return true + } + } + false + } + fn dbg_items(&self) { dbg!(self.items.iter().map(|item| item.item.clone() ).collect::<Vec<QueueItemType>>(), self.items.len()); } - pub fn new() -> Result<Self, Box<dyn Error>> { + + pub fn new() -> Result<Self, PlayerError> { Ok( Queue { player: Player::new()?, name: String::new(), - items: Vec::new() + items: Vec::new(), + played: Vec::new(), + loop_: false, } ) } - pub fn current_index(&mut self/* , max: usize */) -> Option<usize> { - let mut e = self.items.iter().filter(|song| song.state == QueueState::Played ).collect::<Vec<&QueueItem>>().len(); - // TODO: make the max number of past songs modular - while e > 50 { - self.items.remove(0); - e -=1; - } - if e == 0 { - None - }else { - Some(e - 1) - } - } - - fn contains_state(&self, state: QueueState) -> bool { - !self.items.iter().filter(|item| item.state == state ).collect::<Vec<_>>().is_empty() - } - - fn is_empty(&self) -> bool { - self.items.iter().filter(|item| item.state != QueueState::Played).collect::<Vec<_>>().is_empty() - } - pub fn set_items(&mut self, tracks: Vec<QueueItem<'a>>) { let mut tracks = tracks; self.items.clear(); self.items.append(&mut tracks); } - pub fn add_item(&mut self, item: QueueItemType<'a>, source: QueueSource, by_human: bool) -> Result<(), Box<dyn Error>> { - use QueueState::*; + pub fn add_item(&mut self, item: QueueItemType<'a>, source: PlayerLocation, by_human: bool) { let mut i: usize = 0; - let ind = self.current_index(); self.items = self.items.iter().enumerate().map(|(j, item_)| { let mut item_ = item_.to_owned(); // get the index of the current AddHere item and give it to i - if item_.state == AddHere { - i = j - ind.unwrap_or(0); - item_.state = NoState; - } else if item_.state == Current { - i = j - ind.unwrap_or(0); + if item_.state == QueueState::AddHere { + i = j; + item_.state = QueueState::NoState; } item_ }).collect::<Vec<QueueItem>>(); - let pos = ind.unwrap_or(0) + i + if !self.is_empty() || (self.is_empty() && ind == None) { 0 } else { 1 }; - // dbg!(&pos, &i, &ind); - self.items.insert( - pos, - QueueItem { - item: item.clone(), - state: if pos == self.items.len() && i == 0 { - Current - }else { - AddHere - }, - source, - by_human - } - ); - Ok(()) + self.items.insert(i + if self.items.is_empty() { 0 } else { 1 }, QueueItem { + item, + state: QueueState::AddHere, + source, + by_human + }); } - pub fn add_item_next(&mut self, item: QueueItemType<'a>, source: QueueSource) { + pub fn add_item_next(&mut self, item: QueueItemType<'a>, source: PlayerLocation) { use QueueState::*; - let ind_ = self.current_index(); - let ind = ind_.unwrap_or(0); - let empty = self.is_empty(); + let empty = self.items.is_empty(); self.items.insert( - ind + if !empty && ind_ == None { 1 } else { 2 }, + (if empty { 0 } else { 1 }), QueueItem { item, - state: if empty { - Current - }else if self.items.get(ind + 1).is_none() || (!self.contains_state(AddHere) && self.items.get(ind + 1).is_some()) { - AddHere - }else { - NoState - }, + state: if (self.items.get(1).is_none() || (!self.has_addhere() && self.items.get(1).is_some()) || empty) { AddHere } else { NoState }, source, by_human: true } ) } - pub fn remove_item(&mut self, index: usize) -> Result<(), Box<dyn Error>> { - use QueueState::*; - let remove_index: usize = (if let Some(current_index) = self.current_index() { dbg!(¤t_index); current_index } else { 0 } + index ); + pub fn add_multi(&mut self, items: Vec<QueueItemType>, source: PlayerLocation, by_human: bool) { + + } + + pub fn remove_item(&mut self, remove_index: usize) -> Result<(), QueueError> { // dbg!(/*&remove_index, self.current_index(), &index,*/ &self.items[remove_index]); @@ -199,39 +195,29 @@ impl<'a> Queue<'a> { // update the state of the next item to replace the item being removed if self.items.get(remove_index + 1).is_some() { self.items[remove_index + 1].state = self.items[remove_index].state; - }else if self.items[remove_index].state != Current { - self.items[remove_index - 1].state = self.items[remove_index].state; } - self.items[remove_index].state = NoState; + self.items[remove_index].state = QueueState::NoState; self.items.remove(remove_index); Ok(()) }else { - Err("No Songs to remove!".into()) + Err(QueueError::EmptyQueue) } } pub fn clear(&mut self) { - self.items.retain(|item| item.state == QueueState::Played ); + self.items.clear(); } pub fn clear_except(&mut self, index: usize) -> Result<(), Box<dyn Error>> { - let mut index = index; - let ind = match self.current_index() { - Some(e) => e, - None => return Err("nothing to clear!".into()) - }; - let empty = self.is_empty(); - - if !empty { - index += ind; - }else { - index -=1 - } + use QueueState::*; + let empty = self.items.is_empty(); if !empty && index < self.items.len() { let i = self.items[index].clone(); - self.items.retain(|item| item.state == QueueState::Played || *item == i ); - self.items[ind+1].state = QueueState::Current + self.items.retain(|item| *item == i ); + self.items[0].state = AddHere; + }else if empty { + return Err("Queue is empty!".into()); }else { return Err("index out of bounds!".into()); } @@ -239,49 +225,117 @@ impl<'a> Queue<'a> { } pub fn clear_played(&mut self) { - self.items.retain(|item| item.state != QueueState::Played ); + self.played.clear(); } pub fn clear_all(&mut self) { - self.items.clear() + self.items.clear(); + self.played.clear(); } - fn move_to(&mut self, index: usize) -> Result<(), Box<dyn Error>> { - let empty = self.is_empty(); - let nothing_error = Err("Nothing in the queue to move to!".into()); - let ind = self.current_index().unwrap_or(0); - let index = if !empty { index + ind } else { return nothing_error; }; - if !empty && index < self.items.len() -1 { + // TODO: uh, fix this? + fn move_to(&mut self, index: usize) -> Result<(), QueueError> { + use QueueState::*; + + let empty = self.items.is_empty(); + let nothing_error = Err(QueueError::EmptyQueue); + let index = if !empty { index } else { return nothing_error; }; + + if !empty && index < self.items.len() { let position = self.player.position(); if position.is_some_and(|dur| !dur.is_zero() ) { - self.items[ind].state = QueueState::Played; + self.played.push(self.items[0].clone()); } let to_item = self.items[index].clone(); - let ind = self.current_index().unwrap_or(0); loop { - if self.items[ind].item != to_item.item { + let empty = !self.items.is_empty(); + let item = self.items[0].item.to_owned(); + + if item != to_item.item && !empty { + if self.items[0].state == AddHere && self.items.get(1).is_some() { + self.items[1].state = AddHere; + } if let Err(e) = self.remove_item(0) { dbg!(&e); self.dbg_items(); return Err(e); } // dbg!(&to_item.item, &self.items[ind].item); + }else if empty { + return nothing_error; }else { break; } } }else { - return Err("index out of bounds!".into()); + return Err(QueueError::EmptyQueue.into()); } Ok(()) } - pub fn swap(&mut self, index1: usize, index2: usize) {} + pub fn swap(&mut self, a: usize, b: usize) { + self.items.swap(a, b) + } - pub fn move_item(&mut self, item: usize, to_index: usize) {} + pub fn move_item(&mut self, a: usize, b: usize) { + let item = self.items[a].to_owned(); + if a != b { + self.items.remove(a); + } + self.items.insert(b, item); + } - pub fn next() {} + #[allow(clippy::should_implement_trait)] + pub fn next(&mut self, lib: Arc<RwLock<MusicLibrary>>) -> Result<OutQueue, Box<dyn Error>> { + + + + if self.items.is_empty() { + if self.loop_ { + return Err(QueueError::EmptyQueue.into()); // TODO: add function to loop the queue + }else { + return Err(QueueError::EmptyQueue.into()); + } + } + // TODO: add an algorithm to detect if the song should be skipped + let item = self.items[0].clone(); + let uri: URI = match &self.items[1].item { + QueueItemType::Song(uuid) => { + // TODO: Refactor later for multiple URIs + match &lib.read().unwrap().query_uuid(uuid) { + Some(song) => song.0.location.clone(), + None => return Err("Uuid does not exist!".into()), + } + }, + QueueItemType::Album { album, current, ..} => { + let (disc, track) = (current.0 as usize, current.1 as usize); + match album.track(disc, track) { + Some(track) => track.location.clone(), + None => return Err(format!("Track in Album {} at disc {} track {} does not exist!", album.title(), disc, track).into()) + } + }, + QueueItemType::Playlist { current, .. } => { + // TODO: Refactor later for multiple URIs + match &lib.read().unwrap().query_uuid(current) { + Some(song) => song.0.location.clone(), + None => return Err("Uuid does not exist!".into()), + } + }, + _ => todo!() + }; + if !self.player.is_paused() { + self.player.enqueue_next(&uri)?; + self.player.play()? + } + if self.items[0].state == QueueState::AddHere || !self.has_addhere() { + self.items[1].state = QueueState::AddHere; + } + self.played.push(item); + self.items.remove(0); + + Ok(todo!()) + } pub fn prev() {} @@ -293,40 +347,42 @@ impl<'a> Queue<'a> { } Ok(()) } + pub fn check_played(&mut self) { + while self.played.len() > 50 { + self.played.remove(0); + } + } +} + +pub struct OutQueue { + +} + +pub enum OutQueueItem { + } #[test] fn item_add_test() { let mut q = Queue::new().unwrap(); - dbg!(1); - for _ in 0..1 { - q.items.push(QueueItem { item: QueueItemType::Song(Uuid::new_v4()), state: QueueState::Played, source: QueueSource::File, by_human: false }); - } - dbg!(2); - - // q.clear(); - dbg!(3); for _ in 0..5 { // dbg!("tick!"); - q.add_item(QueueItemType::Song(Uuid::new_v4()), QueueSource::Library, true).unwrap(); + q.add_item(QueueItemType::Song(Uuid::new_v4()), PlayerLocation::Library, true); // dbg!(&q.items, &q.items.len()); } - dbg!(4); - dbg!(&q.items, &q.items.len()); - // q.clear_played(); for _ in 0..1 { q.remove_item(0).inspect_err(|e| println!("{e:?}")); } - // for _ in 0..2 { - // q.items.push(QueueItem { item: QueueItemType::Test, state: QueueState::NoState, source: QueueSource::Library, by_human: false }); - // } - // dbg!(5); + for _ in 0..2 { + q.items.push(QueueItem { item: QueueItemType::Test, state: QueueState::NoState, source: PlayerLocation::Library, by_human: false }); + } + dbg!(5); - // q.add_item_next(QueueItemType::Test, QueueSource::File); - // dbg!(6); + q.add_item_next(QueueItemType::Test, PlayerLocation::Test); + dbg!(6); dbg!(&q.items, &q.items.len()); } @@ -335,25 +391,23 @@ fn item_add_test() { fn test_() { let mut q = Queue::new().unwrap(); for _ in 0..400 { - q.items.push(QueueItem { item: QueueItemType::Song(Uuid::new_v4()), state: QueueState::Played, source: QueueSource::File, by_human: false }); + q.items.push(QueueItem { item: QueueItemType::Song(Uuid::new_v4()), state: QueueState::NoState, source: PlayerLocation::File, by_human: false }); } for _ in 0..50000 { - q.add_item(QueueItemType::Song(Uuid::new_v4()), QueueSource::Library, true).unwrap(); + q.add_item(QueueItemType::Song(Uuid::new_v4()), PlayerLocation::Library, true); } - q.add_item_next(QueueItemType::Test, QueueSource::File); + // q.add_item_next(QueueItemType::Test, PlayerLocation::File); - dbg!(&q.items, &q.items.len()); + // dbg!(&q.items, &q.items.len()); } #[test] fn move_test() { let mut q = Queue::new().unwrap(); - for _ in 0..1 { - q.items.push(QueueItem { item: QueueItemType::Song(Uuid::new_v4()), state: QueueState::Played, source: QueueSource::File, by_human: false }); - } + for _ in 0..5 { - q.add_item(QueueItemType::Song(Uuid::new_v4()), QueueSource::Library, true).unwrap(); + q.add_item(QueueItemType::Song(Uuid::new_v4()), PlayerLocation::Library, true); } // q.add_item(QueueItemType::Test, QueueSource::Library, true).unwrap(); dbg!(&q.items, &q.items.len()); diff --git a/src/music_storage/library.rs b/src/music_storage/library.rs index 8c547c0..1a198f8 100644 --- a/src/music_storage/library.rs +++ b/src/music_storage/library.rs @@ -5,7 +5,9 @@ use crate::config::config::Config; // Various std things use std::collections::BTreeMap; use std::error::Error; +use std::io::Write; use std::ops::ControlFlow::{Break, Continue}; +use std::thread::sleep; // Files use file_format::{FileFormat, Kind}; @@ -14,7 +16,7 @@ use image::guess_format; use lofty::{AudioFile, ItemKey, ItemValue, ParseOptions, Probe, TagType, TaggedFileExt}; use rcue::parser::parse_from_file; use uuid::Uuid; -use std::fs; +use std::fs::{self, File}; use tempfile::TempDir; use std::path::{Path, PathBuf}; use walkdir::WalkDir; @@ -160,11 +162,6 @@ pub struct Song { pub tags: BTreeMap<Tag, String>, } -#[test] -fn get_art_test() { - let s = Song::from_file(Path::new("F:\\Music\\Mp3\\ななひら\\Colory Starry\\05 - Fly Away!.mp3")).unwrap(); - s.open_album_art(0).inspect_err(|e| println!("{e:?}")).unwrap(); -} impl Song { /// Get a tag's value @@ -431,7 +428,7 @@ impl Song { Ok(tracks) } - pub fn open_album_art(&self, index: usize) -> Result<(), Box<dyn Error>> { + pub fn open_album_art(&self, index: usize, temp_dir: &TempDir) -> Result<(), Box<dyn Error>> { use opener::open; use urlencoding::decode; @@ -451,9 +448,21 @@ impl Song { let blank_tag = &lofty::Tag::new(TagType::Id3v2); let tagged_file: lofty::TaggedFile; - let uri = urlencoding::decode(self.location.as_uri().strip_prefix("file://").unwrap())?.into_owned(); + #[cfg(windows)] + let uri = urlencoding::decode( + match self.location.as_uri().strip_prefix("file:///") { + Some(str) => str, + None => return Err("invalid path.. again?".into()) + })?.into_owned(); - let tag = match Probe::open(uri).unwrap().options(normal_options).read() { + #[cfg(unix)] + let uri = urlencoding::decode( + match self.location.as_uri().strip_prefix("file://") { + Some(str) => str, + None => return Err("invalid path.. again?".into()) + })?.into_owned(); + + let tag = match Probe::open(uri)?.options(normal_options).read() { Ok(file) => { tagged_file = file; @@ -471,26 +480,16 @@ impl Song { }; let data = tag.pictures()[index].data(); - let format = dbg!(guess_format(data)?); - let img = image::load_from_memory(data)?; - let tmp_dir = TempDir::new()?; - let fmt = match format { - Jpeg => "jpeg", - Png => "png", - _ => todo!(), - }; + let fmt = FileFormat::from_bytes(data); + let file_path = temp_dir.path().join(format!("{}_{index}.{}", self.uuid, fmt.extension())); - let file_path = tmp_dir.path().join(format!("{}.{fmt}", self.uuid)); - - open(&file_path).unwrap(); - img.save_with_format(&file_path, format).unwrap(); + File::create(&file_path)?.write_all(data)?; file_path }, }; - dbg!(open(uri)?); - + dbg!(open(dbg!(uri))?); Ok(()) } } @@ -558,6 +557,14 @@ impl URI { path_str.to_string() } + pub fn as_path(&self) -> Result<&PathBuf, Box<dyn Error>> { + if let Self::Local(path) = self { + Ok(path) + }else { + Err("This URI is not local!".into()) + } + } + pub fn exists(&self) -> Result<bool, std::io::Error> { match self { URI::Local(loc) => loc.try_exists(), @@ -1132,3 +1139,25 @@ impl MusicLibrary { Ok(albums) } } + +#[cfg(test)] +mod test { + use std::{path::Path, thread::sleep, time::{Duration, Instant}}; + + use tempfile::TempDir; + + use super::Song; + + + #[test] + fn get_art_test() { + let s = Song::from_file(Path::new(".\\test-config\\music\\Snail_s House - Hot Milk.mp3")).unwrap(); + let dir = &TempDir::new().unwrap(); + + let now = Instant::now(); + _ = s.open_album_art(0, dir).inspect_err(|e| println!("{e:?}")); + println!("{}ms", now.elapsed().as_millis() ); + + sleep(Duration::from_secs(1)); + } +} \ No newline at end of file From 1734a39db566cb349a9127dcfd38c02adbd775ea Mon Sep 17 00:00:00 2001 From: MrDulfin <mrdulfin@mrdulfin.com> Date: Sun, 24 Mar 2024 21:40:19 -0400 Subject: [PATCH 110/136] removed `image` crate --- Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index a3c737b..266e022 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,5 +37,4 @@ uuid = { version = "1.6.1", features = ["v4", "serde"]} serde_json = "1.0.111" deunicode = "1.4.2" opener = { version = "0.7.0", features = ["reveal"]} -image = "0.25.0" tempfile = "3.10.1" From 50f42e3a265c1e698537861d4ef1422bcd9bec10 Mon Sep 17 00:00:00 2001 From: MrDulfin <mrdulfin@mrdulfin.com> Date: Sun, 24 Mar 2024 21:41:43 -0400 Subject: [PATCH 111/136] removed `image` crate --- Cargo.toml | 1 - src/music_storage/library.rs | 2 -- 2 files changed, 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a3c737b..266e022 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,5 +37,4 @@ uuid = { version = "1.6.1", features = ["v4", "serde"]} serde_json = "1.0.111" deunicode = "1.4.2" opener = { version = "0.7.0", features = ["reveal"]} -image = "0.25.0" tempfile = "3.10.1" diff --git a/src/music_storage/library.rs b/src/music_storage/library.rs index 1a198f8..2095821 100644 --- a/src/music_storage/library.rs +++ b/src/music_storage/library.rs @@ -12,7 +12,6 @@ use std::thread::sleep; // Files use file_format::{FileFormat, Kind}; use glib::filename_to_uri; -use image::guess_format; use lofty::{AudioFile, ItemKey, ItemValue, ParseOptions, Probe, TagType, TaggedFileExt}; use rcue::parser::parse_from_file; use uuid::Uuid; @@ -20,7 +19,6 @@ use std::fs::{self, File}; use tempfile::TempDir; use std::path::{Path, PathBuf}; use walkdir::WalkDir; -use image::ImageFormat::*; // Time use chrono::{serde::ts_milliseconds_option, DateTime, Utc}; From 7da0b1a1dbff0af4e5ae1c6d02c1b1a82cc0ae8f Mon Sep 17 00:00:00 2001 From: MrDulfin <mrdulfin@mrdulfin.com> Date: Sat, 30 Mar 2024 00:55:56 -0400 Subject: [PATCH 112/136] updated dependencies and made small changes to the library --- Cargo.toml | 7 +++---- src/music_storage/library.rs | 13 ++++++++----- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 266e022..2d106a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,9 +32,8 @@ leb128 = "0.2.5" urlencoding = "2.1.3" m3u8-rs = "5.0.5" thiserror = "1.0.56" -font = "0.27.0" -uuid = { version = "1.6.1", features = ["v4", "serde"]} +uuid = { version = "1.6.1", features = ["v4", "serde"] } serde_json = "1.0.111" deunicode = "1.4.2" -opener = { version = "0.7.0", features = ["reveal"]} -tempfile = "3.10.1" +opener = { version = "0.7.0", features = ["reveal"] } +tempfile = "3.10.1" \ No newline at end of file diff --git a/src/music_storage/library.rs b/src/music_storage/library.rs index 2095821..1afc991 100644 --- a/src/music_storage/library.rs +++ b/src/music_storage/library.rs @@ -7,7 +7,7 @@ use std::collections::BTreeMap; use std::error::Error; use std::io::Write; use std::ops::ControlFlow::{Break, Continue}; -use std::thread::sleep; + // Files use file_format::{FileFormat, Kind}; @@ -426,6 +426,8 @@ impl Song { Ok(tracks) } + /// Takes the AlbumArt[index] and opens it in the native file viewer + pub fn open_album_art(&self, index: usize, temp_dir: &TempDir) -> Result<(), Box<dyn Error>> { use opener::open; use urlencoding::decode; @@ -446,14 +448,14 @@ impl Song { let blank_tag = &lofty::Tag::new(TagType::Id3v2); let tagged_file: lofty::TaggedFile; - #[cfg(windows)] + #[cfg(target_family = "windows")] let uri = urlencoding::decode( match self.location.as_uri().strip_prefix("file:///") { Some(str) => str, None => return Err("invalid path.. again?".into()) })?.into_owned(); - #[cfg(unix)] + #[cfg(target_family = "unix")] let uri = urlencoding::decode( match self.location.as_uri().strip_prefix("file://") { Some(str) => str, @@ -1149,13 +1151,14 @@ mod test { #[test] fn get_art_test() { - let s = Song::from_file(Path::new(".\\test-config\\music\\Snail_s House - Hot Milk.mp3")).unwrap(); + let s = Song::from_file(Path::new("")).unwrap(); let dir = &TempDir::new().unwrap(); let now = Instant::now(); _ = s.open_album_art(0, dir).inspect_err(|e| println!("{e:?}")); + _ = s.open_album_art(1, dir).inspect_err(|e| println!("{e:?}")); println!("{}ms", now.elapsed().as_millis() ); - sleep(Duration::from_secs(1)); + sleep(Duration::from_secs(20)); } } \ No newline at end of file From f02f5bca41c75dd022e25c11b3d60c477ad66c3f Mon Sep 17 00:00:00 2001 From: MrDulfin <mrdulfin@mrdulfin.com> Date: Wed, 3 Apr 2024 01:00:52 -0400 Subject: [PATCH 113/136] first draft of library Rework --- Cargo.toml | 3 +- src/music_controller/controller.rs | 2 +- src/music_controller/queue.rs | 11 +- src/music_storage/db_reader/foobar/reader.rs | 6 +- src/music_storage/db_reader/itunes/reader.rs | 17 ++- src/music_storage/library.rs | 127 ++++++++++++++----- src/music_storage/playlist.rs | 40 +++++- 7 files changed, 149 insertions(+), 57 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2d106a4..3c7d698 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,4 +36,5 @@ uuid = { version = "1.6.1", features = ["v4", "serde"] } serde_json = "1.0.111" deunicode = "1.4.2" opener = { version = "0.7.0", features = ["reveal"] } -tempfile = "3.10.1" \ No newline at end of file +tempfile = "3.10.1" +nestify = "0.3.3" diff --git a/src/music_controller/controller.rs b/src/music_controller/controller.rs index ebcc27f..ca14d12 100644 --- a/src/music_controller/controller.rs +++ b/src/music_controller/controller.rs @@ -314,7 +314,7 @@ mod tests { a.q_set_volume(i, 0.04); // a.new_queue(); let songs = a.lib_get_songs(); - a.q_enqueue(i, songs[2].location.clone()); + a.q_enqueue(i, songs[2].primary_uri().unwrap().0.clone()); // a.enqueue(1, songs[2].location.clone()); a.q_play(i).unwrap(); // a.play(1).unwrap(); diff --git a/src/music_controller/queue.rs b/src/music_controller/queue.rs index 6180e9d..78bbe7c 100644 --- a/src/music_controller/queue.rs +++ b/src/music_controller/queue.rs @@ -57,14 +57,14 @@ impl QueueItemType<'_> { match self { Song(uuid) => { if let Some((song, _)) = lib.query_uuid(uuid) { - Some(song.location.clone()) + Some(song.primary_uri().unwrap().0.clone()) // TODO: error handle these better }else { Option::None } }, Album{album, shuffled, current: (disc, index), ..} => { if !shuffled { - Some(album.track(*disc as usize, *index as usize).unwrap().location.clone()) + Some(album.track(*disc as usize, *index as usize).unwrap().primary_uri().unwrap().0.clone()) }else { todo!() } @@ -302,23 +302,22 @@ impl<'a> Queue<'a> { let item = self.items[0].clone(); let uri: URI = match &self.items[1].item { QueueItemType::Song(uuid) => { - // TODO: Refactor later for multiple URIs match &lib.read().unwrap().query_uuid(uuid) { - Some(song) => song.0.location.clone(), + Some(song) => song.0.primary_uri()?.0.clone(), None => return Err("Uuid does not exist!".into()), } }, QueueItemType::Album { album, current, ..} => { let (disc, track) = (current.0 as usize, current.1 as usize); match album.track(disc, track) { - Some(track) => track.location.clone(), + Some(track) => track.primary_uri()?.0.clone(), None => return Err(format!("Track in Album {} at disc {} track {} does not exist!", album.title(), disc, track).into()) } }, QueueItemType::Playlist { current, .. } => { // TODO: Refactor later for multiple URIs match &lib.read().unwrap().query_uuid(current) { - Some(song) => song.0.location.clone(), + Some(song) => song.0.primary_uri()?.0.clone(), None => return Err("Uuid does not exist!".into()), } }, diff --git a/src/music_storage/db_reader/foobar/reader.rs b/src/music_storage/db_reader/foobar/reader.rs index 815f9a7..4d07e71 100644 --- a/src/music_storage/db_reader/foobar/reader.rs +++ b/src/music_storage/db_reader/foobar/reader.rs @@ -176,14 +176,15 @@ pub struct FoobarPlaylistTrack { impl FoobarPlaylistTrack { fn find_song(&self) -> Song { let location = URI::Local(self.file_name.clone().into()); + let internal_tags = Vec::new(); Song { - location, + location: vec![location], uuid: Uuid::new_v4(), plays: 0, skips: 0, favorited: false, - // banned: None, + banned: None, rating: None, format: None, duration: self.duration, @@ -193,6 +194,7 @@ impl FoobarPlaylistTrack { date_modified: None, album_art: Vec::new(), tags: BTreeMap::new(), + internal_tags, } } } diff --git a/src/music_storage/db_reader/itunes/reader.rs b/src/music_storage/db_reader/itunes/reader.rs index 90a48f2..f6032db 100644 --- a/src/music_storage/db_reader/itunes/reader.rs +++ b/src/music_storage/db_reader/itunes/reader.rs @@ -13,7 +13,7 @@ use std::vec::Vec; use chrono::prelude::*; use crate::music_storage::db_reader::extern_library::ExternalLibrary; -use crate::music_storage::library::{AlbumArt, Service, Song, Tag, URI}; +use crate::music_storage::library::{AlbumArt, BannedType, Service, Song, Tag, URI}; use crate::music_storage::utils; use urlencoding::decode; @@ -166,17 +166,19 @@ impl ExternalLibrary for ITunesLibrary { }; let play_time_ = StdDur::from_secs(track.plays as u64 * dur.as_secs()); + let internal_tags = Vec::new(); // TODO: handle internal tags generation + let ny: Song = Song { - location, + location: vec![location], uuid: Uuid::new_v4(), plays: track.plays, skips: 0, favorited: track.favorited, - // banned: if track.banned { - // Some(BannedType::All) - // }else { - // None - // }, + banned: if track.banned { + Some(BannedType::All) + }else { + None + }, rating: track.rating, format: match FileFormat::from_file(PathBuf::from(&loc)) { Ok(e) => Some(e), @@ -192,6 +194,7 @@ impl ExternalLibrary for ITunesLibrary { Err(_) => Vec::new(), }, tags: tags_, + internal_tags, }; // dbg!(&ny.tags); bun.push(ny); diff --git a/src/music_storage/library.rs b/src/music_storage/library.rs index 1afc991..ab2c231 100644 --- a/src/music_storage/library.rs +++ b/src/music_storage/library.rs @@ -1,3 +1,4 @@ +use super::playlist::PlaylistFolder; // Crate things use super::utils::{find_images, normalize, read_file, write_file}; use crate::config::config::Config; @@ -117,35 +118,58 @@ impl ToString for Field { } } +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)] +#[non_exhaustive] +pub enum InternalTag { + DoNotTrack(DoNotTrack), + SongType(SongType), + SongLink(Uuid, SongType), + // Volume Adjustment from -100% to 100% + VolumeAdjustment(i8), +} + #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +#[non_exhaustive] pub enum BannedType { Shuffle, All, } -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)] +#[non_exhaustive] pub enum DoNotTrack { // TODO: add services to not track + LastFM, + LibreFM, + MusicBrainz, + Discord, } -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] -enum SongType { - // TODO: add song types +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)] +#[non_exhaustive] +pub enum SongType { + // TODO: add MORE?! song types Main, Instrumental, Remix, Custom(String) } +impl Default for SongType { + fn default() -> Self { + SongType::Main + } +} + /// Stores information about a single song #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] pub struct Song { - pub location: URI, + pub location: Vec<URI>, pub uuid: Uuid, pub plays: i32, pub skips: i32, pub favorited: bool, - // pub banned: Option<BannedType>, + pub banned: Option<BannedType>, pub rating: Option<u8>, pub format: Option<FileFormat>, pub duration: Duration, @@ -158,6 +182,7 @@ pub struct Song { pub date_modified: Option<DateTime<Utc>>, pub album_art: Vec<AlbumArt>, pub tags: BTreeMap<Tag, String>, + pub internal_tags: Vec<InternalTag> } @@ -180,7 +205,7 @@ impl Song { pub fn get_field(&self, target_field: &str) -> Option<Field> { let lower_target = target_field.to_lowercase(); match lower_target.as_str() { - "location" => Some(Field::Location(self.location.clone())), + "location" => Some(Field::Location(self.primary_uri().unwrap().0.clone())), //TODO: make this not unwrap() "plays" => Some(Field::Plays(self.plays)), "skips" => Some(Field::Skips(self.skips)), "favorited" => Some(Field::Favorited(self.favorited)), @@ -279,13 +304,15 @@ impl Song { // TODO: Fix error handling let binding = fs::canonicalize(target_file).unwrap(); + // TODO: Handle creation of internal tag: Song Type and Song Links + let internal_tags = { Vec::new() }; let new_song = Song { - location: URI::Local(binding), + location: vec![URI::Local(binding)], uuid: Uuid::new_v4(), plays: 0, skips: 0, favorited: false, - // banned: None, + banned: None, rating: None, format, duration, @@ -295,6 +322,7 @@ impl Song { date_modified: Some(chrono::offset::Utc::now()), tags, album_art, + internal_tags, }; Ok(new_song) } @@ -399,17 +427,17 @@ impl Song { let album_art = find_images(&audio_location.to_path_buf()).unwrap(); let new_song = Song { - location: URI::Cue { + location: vec![URI::Cue { location: audio_location.clone(), index: i, start, end, - }, + }], uuid: Uuid::new_v4(), plays: 0, skips: 0, favorited: false, - // banned: None, + banned: None, rating: None, format, duration, @@ -419,6 +447,7 @@ impl Song { date_modified: Some(chrono::offset::Utc::now()), tags, album_art, + internal_tags: Vec::new() }; tracks.push((new_song, audio_location.clone())); } @@ -448,16 +477,17 @@ impl Song { let blank_tag = &lofty::Tag::new(TagType::Id3v2); let tagged_file: lofty::TaggedFile; + // TODO: add support for other URI types... or don't #[cfg(target_family = "windows")] let uri = urlencoding::decode( - match self.location.as_uri().strip_prefix("file:///") { + match self.primary_uri()?.0.as_uri().strip_prefix("file:///") { Some(str) => str, None => return Err("invalid path.. again?".into()) })?.into_owned(); #[cfg(target_family = "unix")] let uri = urlencoding::decode( - match self.location.as_uri().strip_prefix("file://") { + match self.primary_uri()?.as_uri().strip_prefix("file://") { Some(str) => str, None => return Err("invalid path.. again?".into()) })?.into_owned(); @@ -492,6 +522,26 @@ impl Song { dbg!(open(dbg!(uri))?); Ok(()) } + + /// Returns a reference to the first valid URI in the song, and any invalid URIs that come before it, or errors if there are no valid URIs + #[allow(clippy::type_complexity)] + pub fn primary_uri(&self) -> Result<(&URI, Option<Vec<&URI>>), Box<dyn Error>> { + let mut invalid_uris = Vec::new(); + let mut valid_uri = None; + + for uri in &self.location { + if uri.exists()? { + valid_uri = Some(uri); + break; + }else { + invalid_uris.push(uri); + } + } + match valid_uri { + Some(uri) => Ok((uri, if !invalid_uris.is_empty() { Some(invalid_uris) } else { None } )), + None => Err("No valid URIs for this song".into()) + } + } } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] @@ -652,14 +702,8 @@ pub struct MusicLibrary { pub name: String, pub uuid: Uuid, pub library: Vec<Song>, -} - -#[test] -fn library_init() { - let config = Config::read_file(PathBuf::from("test_config/config_test.json")).unwrap(); - let target_uuid = config.libraries.libraries[0].uuid; - let a = MusicLibrary::init(Arc::new(RwLock::from(config)), target_uuid).unwrap(); - dbg!(a); + pub playlists: PlaylistFolder, + pub backup_songs: Vec<Song> // maybe move this to the config instead? } impl MusicLibrary { @@ -669,6 +713,8 @@ impl MusicLibrary { name, uuid, library: Vec::new(), + playlists: PlaylistFolder::new(), + backup_songs: Vec::new(), } } @@ -736,8 +782,11 @@ impl MusicLibrary { .par_iter() .enumerate() .try_for_each(|(i, track)| { - if path == &track.location { - return std::ops::ControlFlow::Break((track, i)); + for location in &track.location { + //TODO: check that this works + if path == location { + return std::ops::ControlFlow::Break((track, i)); + } } Continue(()) }); @@ -773,7 +822,7 @@ impl MusicLibrary { fn query_path(&self, path: PathBuf) -> Option<Vec<&Song>> { let result: Arc<Mutex<Vec<&Song>>> = Arc::new(Mutex::new(Vec::new())); self.library.par_iter().for_each(|track| { - if path == track.location.path() { + if path == track.primary_uri().unwrap().0.path() { //TODO: make this also not unwrap result.clone().lock().unwrap().push(track); } }); @@ -885,13 +934,14 @@ impl MusicLibrary { } pub fn add_song(&mut self, new_song: Song) -> Result<(), Box<dyn Error>> { - if self.query_uri(&new_song.location).is_some() { - return Err(format!("URI already in database: {:?}", new_song.location).into()); + let location = new_song.primary_uri()?.0; + if self.query_uri(location).is_some() { + return Err(format!("URI already in database: {:?}", location).into()); } - 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()) + match location { + URI::Local(_) if self.query_path(location.path()).is_some() => { + return Err(format!("Location exists for {:?}", location).into()) } _ => (), } @@ -914,17 +964,18 @@ impl MusicLibrary { } /// Scan the song by a location and update its tags + // TODO: change this to work with multiple uris pub fn update_uri( &mut self, target_uri: &URI, new_tags: Vec<Tag>, ) -> Result<(), Box<dyn std::error::Error>> { - let target_song = match self.query_uri(target_uri) { + let (target_song, _) = match self.query_uri(target_uri) { Some(song) => song, None => return Err("URI not in database!".to_string().into()), }; - println!("{:?}", target_song.0.location); + println!("{:?}", target_song.location); for tag in new_tags { println!("{:?}", tag); @@ -1142,10 +1193,12 @@ impl MusicLibrary { #[cfg(test)] mod test { - use std::{path::Path, thread::sleep, time::{Duration, Instant}}; + use std::{path::{Path, PathBuf}, sync::{Arc, RwLock}, thread::sleep, time::{Duration, Instant}}; use tempfile::TempDir; + use crate::{config::config::Config, music_storage::library::MusicLibrary}; + use super::Song; @@ -1161,4 +1214,12 @@ mod test { sleep(Duration::from_secs(20)); } + + #[test] + fn library_init() { + let config = Config::read_file(PathBuf::from("test_config/config_test.json")).unwrap(); + let target_uuid = config.libraries.libraries[0].uuid; + let a = MusicLibrary::init(Arc::new(RwLock::from(config)), target_uuid).unwrap(); + dbg!(a); + } } \ No newline at end of file diff --git a/src/music_storage/playlist.rs b/src/music_storage/playlist.rs index 783a293..eb4fef4 100644 --- a/src/music_storage/playlist.rs +++ b/src/music_storage/playlist.rs @@ -1,17 +1,43 @@ -use std::{fs::File, io::{Read, Error}}; +use std::{fs::File, io::{Error, Read}, time::Duration}; -use chrono::Duration; +// use chrono::Duration; +use serde::{Deserialize, Serialize}; use uuid::Uuid; use super::library::{AlbumArt, Song, Tag}; use m3u8_rs::{MediaPlaylist, MediaPlaylistType, MediaSegment, Playlist as List2}; +use nestify::nest; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Deserialize, Serialize)] pub enum SortOrder { Manual, Tag(Tag) } -#[derive(Debug, Clone)] + +nest! { + #[derive(Debug, Clone, Deserialize, Serialize)]* + pub struct PlaylistFolder { + name: String, + items: Vec< + pub enum PlaylistFolderItem { + Folder(PlaylistFolder), + List(Playlist) + } + > + } +} + +impl PlaylistFolder { + pub fn new() -> Self { + PlaylistFolder { + name: String::new(), + items: Vec::new(), + } + } +} + + +#[derive(Debug, Clone, Deserialize, Serialize)] pub struct Playlist { uuid: Uuid, title: String, @@ -28,7 +54,7 @@ impl Playlist { pub fn play_count(&self) -> i32 { self.play_count } - pub fn play_time(&self) -> chrono::Duration { + pub fn play_time(&self) -> Duration { self.play_time } pub fn set_tracks(&mut self, tracks: Vec<Uuid>) { @@ -71,7 +97,7 @@ impl Playlist { |track| { MediaSegment { - uri: track.location.to_string().into(), + uri: track.primary_uri().unwrap().0.to_string().into(), // TODO: error handle this better duration: track.duration.as_millis() as f32, title: Some(track.tags.get_key_value(&Tag::Title).unwrap().1.into()), ..Default::default() @@ -148,7 +174,7 @@ impl Default for Playlist { tracks: Vec::default(), sort_order: SortOrder::Manual, play_count: 0, - play_time: Duration::zero(), + play_time: Duration::from_secs(0), } } } From a75081d4fc4f10e08141a1abc59c85cc214a1546 Mon Sep 17 00:00:00 2001 From: MrDulfin <mrdulfin@mrdulfin.com> Date: Fri, 5 Apr 2024 02:09:15 -0400 Subject: [PATCH 114/136] added m3u8 to playlist function and fixed playlist to m3u8 function --- src/config/config.rs | 64 +++++++------- src/music_storage/library.rs | 1 + src/music_storage/playlist.rs | 151 ++++++++++++++++++++++++---------- 3 files changed, 142 insertions(+), 74 deletions(-) diff --git a/src/config/config.rs b/src/config/config.rs index 76c59cc..5a209ae 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -69,10 +69,10 @@ impl ConfigLibraries { } pub fn get_library(&self, uuid: &Uuid) -> Result<ConfigLibrary, ConfigError> { - dbg!(&uuid); + for library in &self.libraries { + // dbg!(&library.uuid, &uuid); if &library.uuid == uuid { - dbg!(&library.uuid); return Ok(library.to_owned()) } } @@ -148,6 +148,13 @@ impl Config { let config: Config = serde_json::from_str::<Config>(&bun)?; Ok(config) } + + pub fn push_library(&mut self, lib: ConfigLibrary) { + if self.libraries.libraries.is_empty() { + self.libraries.default_library = lib.uuid; + } + self.libraries.libraries.push(lib); + } } #[derive(Error, Debug)] @@ -165,45 +172,42 @@ pub enum ConfigError { } #[cfg(test)] -mod tests { +pub mod tests { use std::{path::PathBuf, sync::{Arc, RwLock}}; use crate::music_storage::library::MusicLibrary; use super::{Config, ConfigLibraries, ConfigLibrary}; - #[test] - fn config_test() { - let lib_a = ConfigLibrary::new(PathBuf::from("test-config/library1"), String::from("library1"), None); - let lib_b = ConfigLibrary::new(PathBuf::from("test-config/library2"), String::from("library2"), None); - let lib_c = ConfigLibrary::new(PathBuf::from("test-config/library3"), String::from("library3"), None); - let config = Config { + pub fn new_config_lib() -> (Config, MusicLibrary) { + let lib = ConfigLibrary::new(PathBuf::from("test-config/library"), String::from("library"), None); + let mut config = Config { path: PathBuf::from("test-config/config_test.json"), - libraries: ConfigLibraries { - libraries: vec![ - lib_a.clone(), - lib_b.clone(), - lib_c.clone(), - ], - ..Default::default() - }, ..Default::default() }; - config.write_file(); - let arc = Arc::new(RwLock::from(config)); - MusicLibrary::init(arc.clone(), lib_a.uuid).unwrap(); - MusicLibrary::init(arc.clone(), lib_b.uuid).unwrap(); - MusicLibrary::init(arc.clone(), lib_c.uuid).unwrap(); - } + config.push_library(lib); + config.write_file().unwrap(); - #[test] - fn test2() { - let config = Config::read_file(PathBuf::from("test-config/config_test.json")).unwrap(); - let uuid = config.libraries.get_default().unwrap().uuid; - let mut lib = MusicLibrary::init(Arc::new(RwLock::from(config.clone())), uuid).unwrap(); + let mut lib = MusicLibrary::init(Arc::new(RwLock::from(config.clone())), dbg!(config.libraries.default_library)).unwrap(); lib.scan_folder("test-config/music/").unwrap(); lib.save(config.clone()).unwrap(); - dbg!(&lib); - dbg!(&config); + + (config, lib) + } + + pub fn read_config_lib() -> (Config, MusicLibrary) { + let config = Config::read_file(PathBuf::from("test-config/config_test.json")).unwrap(); + + // dbg!(&config); + + let mut lib = MusicLibrary::init(Arc::new(RwLock::from(config.clone())), config.libraries.get_default().unwrap().uuid).unwrap(); + + + lib.scan_folder("test-config/music/").unwrap(); + + lib.save(config.clone()).unwrap(); + + + (config, lib) } #[test] diff --git a/src/music_storage/library.rs b/src/music_storage/library.rs index 1afc991..22452a9 100644 --- a/src/music_storage/library.rs +++ b/src/music_storage/library.rs @@ -686,6 +686,7 @@ impl MusicLibrary { false => { // If the library does not exist, re-create it let lib = MusicLibrary::new(String::new(), uuid); + write_file(&lib, path)?; lib } diff --git a/src/music_storage/playlist.rs b/src/music_storage/playlist.rs index 783a293..bd40b6a 100644 --- a/src/music_storage/playlist.rs +++ b/src/music_storage/playlist.rs @@ -1,8 +1,9 @@ -use std::{fs::File, io::{Read, Error}}; +use std::{fs::File, io::Read, path:: PathBuf, sync::{Arc, RwLock}}; +use std::error::Error; use chrono::Duration; use uuid::Uuid; -use super::library::{AlbumArt, Song, Tag}; +use super::library::{AlbumArt, MusicLibrary, Song, Tag, URI}; use m3u8_rs::{MediaPlaylist, MediaPlaylistType, MediaSegment, Playlist as List2}; @@ -34,16 +35,14 @@ impl Playlist { pub fn set_tracks(&mut self, tracks: Vec<Uuid>) { self.tracks = tracks; } - pub fn add_track(&mut self, track: Uuid) -> Result<(), Error> { + pub fn add_track(&mut self, track: Uuid) { self.tracks.push(track); - Ok(()) } - pub fn remove_track(&mut self, index: i32) -> Result<(), Error> { + pub fn remove_track(&mut self, index: i32) { let index = index as usize; if (self.tracks.len() - 1) >= index { self.tracks.remove(index); } - Ok(()) } // pub fn get_index(&self, song_name: &str) -> Option<usize> { // let mut index = 0; @@ -58,26 +57,41 @@ impl Playlist { // } // None // } - pub fn contains_value(&self, tag: &Tag, value: &str) -> bool { - &self.tracks.iter().for_each(|track| { + pub fn contains_value(&self, tag: &Tag, value: &String, lib: Arc<RwLock<MusicLibrary>>) -> bool { + let lib = lib.read().unwrap(); + let items = match lib.query_tracks(value, &vec![tag.to_owned()], &vec![tag.to_owned()]) { + Some(e) => e, + None => return false + }; + + for item in items { + for uuid in &self.tracks { + if uuid == &item.uuid { + return true; + } + } + } - }); false } - pub fn to_m3u8(&mut self, tracks: Vec<Song>) { - let seg = tracks - .iter() - .map({ - |track| { - MediaSegment { - uri: track.location.to_string().into(), - duration: track.duration.as_millis() as f32, - title: Some(track.tags.get_key_value(&Tag::Title).unwrap().1.into()), - ..Default::default() - } + pub fn to_m3u8(&mut self, lib: Arc<RwLock<MusicLibrary>>, location: &str) -> Result<(), Box<dyn Error>> { + let lib = lib.read().unwrap(); + let seg = self.tracks + .iter() + .filter_map( |uuid| { + if let Some((track, _)) = lib.query_uuid(uuid) { + if let URI::Local(_) = track.location { + Some(MediaSegment { + uri: track.location.to_string(), + duration: track.duration.as_millis() as f32, + title: track.tags.get_key_value(&Tag::Title).map(|tag| tag.1.into()), + ..Default::default() + }) + }else { None } + }else { None } } - }) + ) .collect::<Vec<MediaSegment>>(); let m3u8 = MediaPlaylist { @@ -90,19 +104,21 @@ impl Playlist { segments: seg.clone(), ..Default::default() }; - //TODO: change this to put in a real file path + let mut file = std::fs::OpenOptions::new() .read(true) .create(true) + .truncate(true) .write(true) - .open("F:\\Dango Music Player\\playlist.m3u8") - .unwrap(); - m3u8.write_to(&mut file).unwrap(); + .open(location)?; + m3u8.write_to(&mut file)?; + Ok(()) } - pub fn from_m3u8(path: &str) -> Result<Playlist, Error> { + + pub fn from_m3u8(path: &str, lib: Arc<RwLock<MusicLibrary>>) -> Result<Playlist, Box<dyn Error>> { let mut file = match File::open(path) { Ok(file) => file, - Err(e) => return Err(e), + Err(e) => return Err(e.into()), }; let mut bytes = Vec::new(); file.read_to_end(&mut bytes).unwrap(); @@ -110,18 +126,53 @@ impl Playlist { let parsed = m3u8_rs::parse_playlist(&bytes); let playlist = match parsed { - Result::Ok((i, playlist)) => playlist, + Result::Ok((_, playlist)) => playlist, Result::Err(e) => panic!("Parsing error: \n{}", e), }; match playlist { - List2::MasterPlaylist(_) => panic!(), - List2::MediaPlaylist(pl) => { - let values = pl.segments.iter().map(|seg| seg.uri.to_owned() ).collect::<Vec<String>>(); + List2::MasterPlaylist(_) => Err("This is a Master Playlist!\nPlase input a Media Playlist".into()), + List2::MediaPlaylist(playlist_) => { + let mut uuids = Vec::new(); + for seg in playlist_.segments { + let path_ = PathBuf::from(seg.uri.to_owned()); + let mut lib = lib.write().unwrap(); + + let uuid = if let Some((song, _)) = lib.query_uri(&URI::Local(path_.clone())) { + song.uuid + }else { + let song_ = Song::from_file(&path_)?; + let uuid = song_.uuid.to_owned(); + lib.add_song(song_)?; + uuid + }; + uuids.push(uuid); + } + let mut playlist = Playlist::new(); + + #[cfg(target_family = "windows")] + { + playlist.title = path.split("\\") + .last() + .unwrap_or_default() + .strip_suffix(".m3u8") + .unwrap_or_default() + .to_string(); + } + #[cfg(target_family = "unix")] + { + playlist.title = path.split("/") + .last() + .unwrap_or_default() + .strip_suffix(".m3u8") + .unwrap_or_default() + .to_string(); + } + + playlist.set_tracks(uuids); + Ok(playlist) } } - - todo!() } fn title(&self) -> &String { &self.title @@ -153,14 +204,26 @@ impl Default for Playlist { } } -// #[test] -// fn list_to_m3u8() { -// let lib = ITunesLibrary::from_file(Path::new( -// "F:\\Music\\Mp3\\Music Main\\iTunes Music Library.xml", -// )); -// let mut a = Playlist::new(); -// let c = lib.to_songs(); -// let mut b = c.iter().map(|song| song.to_owned()).collect::<Vec<Song>>(); -// a.tracks.append(&mut b); -// a.to_m3u8() -// } +#[cfg(test)] +mod test_super { + use super::*; + use crate::config::config::tests::read_config_lib; + + #[test] + fn list_to_m3u8() { + let (_, lib) = read_config_lib(); + let mut playlist = Playlist::new(); + let tracks = lib.library.iter().map(|track| track.uuid ).collect(); + playlist.set_tracks(tracks); + + _ = playlist.to_m3u8(Arc::new(RwLock::from(lib)), ".\\test-config\\playlists\\playlist.m3u8"); + } + + #[test] + fn m3u8_to_list() { + let (_, lib) = read_config_lib(); + let arc = Arc::new(RwLock::from(lib)); + let playlist = Playlist::from_m3u8(".\\test-config\\playlists\\playlist.m3u8", arc).unwrap(); + dbg!(playlist); + } +} From ab529f4c9b82e1a376d93fbccaf86ec5dad0a5d6 Mon Sep 17 00:00:00 2001 From: MrDulfin <mrdulfin@mrdulfin.com> Date: Fri, 5 Apr 2024 20:26:13 -0400 Subject: [PATCH 115/136] added `out_queue` function, rudamentary listenbrainz scrobbling and other small changes --- Cargo.toml | 3 +- src/config/config.rs | 15 +++- src/music_controller/connections.rs | 93 +++++++++++++++++++- src/music_controller/controller.rs | 47 +++++----- src/music_controller/queue.rs | 8 +- src/music_storage/playlist.rs | 132 +++++++++++++++++++++++----- 6 files changed, 244 insertions(+), 54 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2d106a4..49ac4e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,4 +36,5 @@ uuid = { version = "1.6.1", features = ["v4", "serde"] } serde_json = "1.0.111" deunicode = "1.4.2" opener = { version = "0.7.0", features = ["reveal"] } -tempfile = "3.10.1" \ No newline at end of file +tempfile = "3.10.1" +listenbrainz = "0.7.0" diff --git a/src/config/config.rs b/src/config/config.rs index 5a209ae..1c19b15 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -90,11 +90,18 @@ impl ConfigLibraries { } #[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct ConfigConnections { + pub listenbrainz_token: Option<String> +} + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +#[serde(default)] pub struct Config { pub path: PathBuf, pub backup_folder: Option<PathBuf>, pub libraries: ConfigLibraries, pub volume: f32, + pub connections: ConfigConnections, } impl Config { @@ -212,10 +219,10 @@ pub mod tests { #[test] fn test3() { - let config = Config::read_file(PathBuf::from("test-config/config_test.json")).unwrap(); - let uuid = config.libraries.get_default().unwrap().uuid; - let lib = MusicLibrary::init(Arc::new(RwLock::from(config.clone())), uuid).unwrap(); + let (config, lib) = read_config_lib(); - dbg!(lib); + _ = config.write_file(); + + dbg!(config); } } diff --git a/src/music_controller/connections.rs b/src/music_controller/connections.rs index ac4abd9..dec723c 100644 --- a/src/music_controller/connections.rs +++ b/src/music_controller/connections.rs @@ -1,6 +1,95 @@ -use super::controller::Controller; +use std::{ + sync::{Arc, RwLock}, + error::Error, +}; + +use listenbrainz::ListenBrainz; +use uuid::Uuid; + +use crate::{ + config::config::Config, music_controller::controller::{Controller, QueueCmd, QueueResponse}, music_storage::library::{MusicLibrary, Song, Tag} +}; + +use super::controller::DatabaseResponse; + impl Controller { - //more stuff goes here + pub fn listenbrainz_authenticate(&mut self) -> Result<ListenBrainz, Box<dyn Error>> { + let config = &self.config.read().unwrap(); + let mut client = ListenBrainz::new(); + + let lbz_token = match &config.connections.listenbrainz_token { + Some(token) => token, + None => todo!("No ListenBrainz token in config") + }; + + if !client.is_authenticated() { + client.authenticate(lbz_token)?; + } + + Ok(client) + } + pub fn lbz_scrobble(&self, client: ListenBrainz, uuid: Uuid) -> Result<(), Box<dyn Error>> { + let config = &self.config.read().unwrap(); + + &self.db_mail.send(super::controller::DatabaseCmd::QueryUuid(uuid)); + let res = &self.db_mail.recv()?; + let song = match res { + DatabaseResponse::Song(song) => song, + _ => todo!() + }; + let unknown = &"unknown".to_string(); + let artist = song.get_tag(&Tag::Artist).unwrap_or(unknown); + let track = song.get_tag(&Tag::Title).unwrap_or(unknown); + let release = song.get_tag(&Tag::Album).map(|rel| rel.as_str()); + + client.listen(artist, track, release)?; + Ok(()) + } + + pub fn lbz_now_playing(&self, client: ListenBrainz, uuid: Uuid) -> Result<(), Box<dyn Error>> { + let config = &self.config.read().unwrap(); + + &self.db_mail.send(super::controller::DatabaseCmd::QueryUuid(uuid)); + let res = &self.db_mail.recv()?; + let song = match res { + DatabaseResponse::Song(song) => song, + _ => todo!() + }; + let unknown = &"unknown".to_string(); + let artist = song.get_tag(&Tag::Artist).unwrap_or(unknown); + let track = song.get_tag(&Tag::Title).unwrap_or(unknown); + let release = song.get_tag(&Tag::Album).map(|rel| rel.as_str()); + + client.listen(artist, track, release)?; + Ok(()) + } +} + +#[cfg(test)] +mod test_super { + use std::{thread::sleep, time::Duration}; + + use super::*; + use crate::config::config::tests::read_config_lib; + + #[test] + fn listenbrainz() { + let mut c = Controller::start(".\\test-config\\config_test.json").unwrap(); + + let client = c.listenbrainz_authenticate().unwrap(); + + c.q_new().unwrap(); + c.queue_mail[0].send(QueueCmd::SetVolume(0.04)).unwrap(); + + let songs = c.lib_get_songs(); + + c.q_enqueue(0, songs[1].location.to_owned()).unwrap(); + c.q_play(0).unwrap(); + + + sleep(Duration::from_secs(100)); + c.lbz_scrobble(client, songs[1].uuid).unwrap(); + } } diff --git a/src/music_controller/controller.rs b/src/music_controller/controller.rs index ebcc27f..d13a62f 100644 --- a/src/music_controller/controller.rs +++ b/src/music_controller/controller.rs @@ -6,12 +6,14 @@ use std::path::PathBuf; use std::sync::{Arc, RwLock}; use crossbeam_channel::{Sender, Receiver}; use crossbeam_channel; +use listenbrainz::ListenBrainz; use std::thread::spawn; use std::error::Error; use crossbeam_channel::unbounded; use uuid::Uuid; +use crate::music_controller::queue::{QueueItem, QueueItemType}; use crate::music_storage::library::{Tag, URI}; use crate::{ music_storage::library::{MusicLibrary, Song}, @@ -21,27 +23,27 @@ use crate::{ pub struct Controller { // queues: Vec<Queue>, - config: Arc<RwLock<Config>>, + pub config: Arc<RwLock<Config>>, // library: MusicLibrary, - controller_mail: MailMan<ControllerCmd, ControllerResponse>, - db_mail: MailMan<DatabaseCmd, DatabaseResponse>, - queue_mail: Vec<MailMan<QueueCmd, QueueResponse>>, + pub(super) controller_mail: MailMan<ControllerCmd, ControllerResponse>, + pub(super) db_mail: MailMan<DatabaseCmd, DatabaseResponse>, + pub(super) queue_mail: Vec<MailMan<QueueCmd, QueueResponse>>, } #[derive(Debug)] -pub enum ControllerCmd { +pub(super) enum ControllerCmd { Default, Test } #[derive(Debug)] -enum ControllerResponse { +pub(super) enum ControllerResponse { Empty, QueueMailMan(MailMan<QueueCmd, QueueResponse>), } #[derive(Debug)] -pub enum DatabaseCmd { +pub(super) enum DatabaseCmd { Default, Test, SaveLibrary, @@ -49,11 +51,10 @@ pub enum DatabaseCmd { QueryUuid(Uuid), QueryUuids(Vec<Uuid>), ReadFolder(String), - } #[derive(Debug)] -enum DatabaseResponse { +pub(super) enum DatabaseResponse { Empty, Song(Song), Songs(Vec<Song>), @@ -61,7 +62,7 @@ enum DatabaseResponse { } #[derive(Debug)] -enum QueueCmd { +pub(super) enum QueueCmd { Default, Test, Play, @@ -73,25 +74,26 @@ enum QueueCmd { } #[derive(Debug)] -enum QueueResponse { +pub(super) enum QueueResponse { Default, Test, Index(i32), + Uuid(Uuid), } #[derive(Debug)] -struct MailMan<T, U> { +pub(super) struct MailMan<T: Send, U: Send> { pub tx: Sender<T>, rx: Receiver<U> } -impl<T> MailMan<T, T> { +impl<T: Send> MailMan<T, T> { pub fn new() -> Self { let (tx, rx) = unbounded::<T>(); MailMan { tx, rx } } } -impl<T, U> MailMan<T, U> { +impl<T: Send, U: Send> MailMan<T, U> { pub fn double() -> (MailMan<T, U>, MailMan<U, T>) { let (tx, rx) = unbounded::<T>(); let (tx1, rx1) = unbounded::<U>(); @@ -108,14 +110,16 @@ impl<T, U> MailMan<T, U> { } pub fn recv(&self) -> Result<U, Box<dyn Error>> { - let u = self.rx.recv().unwrap(); + let u = self.rx.recv()?; Ok(u) } } #[allow(unused_variables)] impl Controller { - pub fn start(config_path: String) -> Result<Self, Box<dyn Error>> { + pub fn start<P>(config_path: P) -> Result<Self, Box<dyn Error>> + where std::path::PathBuf: std::convert::From<P> + { let config_path = PathBuf::from(config_path); let config = Config::read_file(config_path)?; let uuid = config.libraries.get_default()?.uuid; @@ -185,7 +189,6 @@ impl Controller { }); - Ok( Controller { // queues: Vec::new(), @@ -197,7 +200,7 @@ impl Controller { ) } - fn lib_get_songs(&self) -> Vec<Song> { + pub fn lib_get_songs(&self) -> Vec<Song> { self.db_mail.send(DatabaseCmd::GetSongs); match self.db_mail.recv().unwrap() { DatabaseResponse::Songs(songs) => songs, @@ -205,7 +208,7 @@ impl Controller { } } - fn lib_scan_folder(&self, folder: String) -> Result<(), Box<dyn Error>> { + pub fn lib_scan_folder(&self, folder: String) -> Result<(), Box<dyn Error>> { let mail = &self.db_mail; mail.send(DatabaseCmd::ReadFolder(folder))?; dbg!(mail.recv()?); @@ -259,14 +262,14 @@ impl Controller { Ok(self.queue_mail.len() - 1) } - fn q_play(&self, index: usize) -> Result<(), Box<dyn Error>> { + pub fn q_play(&self, index: usize) -> Result<(), Box<dyn Error>> { let mail = &self.queue_mail[index]; mail.send(QueueCmd::Play)?; dbg!(mail.recv()?); Ok(()) } - fn q_pause(&self, index: usize) -> Result<(), Box<dyn Error>> { + pub fn q_pause(&self, index: usize) -> Result<(), Box<dyn Error>> { let mail = &self.queue_mail[index]; mail.send(QueueCmd::Pause)?; dbg!(mail.recv()?); @@ -286,7 +289,7 @@ impl Controller { // Ok(()) // } - fn q_enqueue(&self, index: usize, uri: URI) -> Result<(), Box<dyn Error>> { + pub fn q_enqueue(&self, index: usize, uri: URI) -> Result<(), Box<dyn Error>> { let mail = &self.queue_mail[index]; mail.send(QueueCmd::Enqueue(uri))?; // dbg!(mail.recv()?); diff --git a/src/music_controller/queue.rs b/src/music_controller/queue.rs index 6180e9d..483010f 100644 --- a/src/music_controller/queue.rs +++ b/src/music_controller/queue.rs @@ -89,10 +89,10 @@ pub enum PlayerLocation { #[derive(Debug, Clone, PartialEq)] #[non_exhaustive] pub struct QueueItem<'a> { - item: QueueItemType<'a>, - state: QueueState, - source: PlayerLocation, - by_human: bool + pub(super) item: QueueItemType<'a>, + pub(super) state: QueueState, + pub(super) source: PlayerLocation, + pub(super) by_human: bool } impl QueueItem<'_> { fn new() -> Self { diff --git a/src/music_storage/playlist.rs b/src/music_storage/playlist.rs index bd40b6a..63876fb 100644 --- a/src/music_storage/playlist.rs +++ b/src/music_storage/playlist.rs @@ -1,18 +1,21 @@ use std::{fs::File, io::Read, path:: PathBuf, sync::{Arc, RwLock}}; use std::error::Error; -use chrono::Duration; +use std::time::Duration; +use serde::{Deserialize, Serialize}; use uuid::Uuid; use super::library::{AlbumArt, MusicLibrary, Song, Tag, URI}; use m3u8_rs::{MediaPlaylist, MediaPlaylistType, MediaSegment, Playlist as List2}; -#[derive(Debug, Clone)] +use rayon::prelude::*; + +#[derive(Debug, Clone, Serialize, Deserialize)] pub enum SortOrder { Manual, - Tag(Tag) + Tag(Vec<Tag>) } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct Playlist { uuid: Uuid, title: String, @@ -29,9 +32,24 @@ impl Playlist { pub fn play_count(&self) -> i32 { self.play_count } - pub fn play_time(&self) -> chrono::Duration { + pub fn play_time(&self) -> Duration { self.play_time } + + fn title(&self) -> &String { + &self.title + } + + fn cover(&self) -> Option<&AlbumArt> { + match &self.cover { + Some(e) => Some(e), + None => None, + } + } + + fn tracks(&self) -> Vec<Uuid> { + self.tracks.to_owned() + } pub fn set_tracks(&mut self, tracks: Vec<Uuid>) { self.tracks = tracks; } @@ -75,6 +93,15 @@ impl Playlist { false } + pub fn to_file(&self, path: &str) -> Result<(), Box<dyn Error>> { + super::utils::write_file(self, PathBuf::from(path))?; + Ok(()) + } + + pub fn from_file(path: &str) -> Result<Playlist, Box<dyn Error>> { + super::utils::read_file(PathBuf::from(path)) + } + pub fn to_m3u8(&mut self, lib: Arc<RwLock<MusicLibrary>>, location: &str) -> Result<(), Box<dyn Error>> { let lib = lib.read().unwrap(); let seg = self.tracks @@ -174,22 +201,72 @@ impl Playlist { } } } - fn title(&self) -> &String { - &self.title - } - fn cover(&self) -> Option<&AlbumArt> { - match &self.cover { - Some(e) => Some(e), - None => None, + + + pub fn out_tracks(&self, lib: Arc<RwLock<MusicLibrary>>) -> (Vec<Song>, Vec<&Uuid>) { + let lib = lib.read().unwrap(); + let mut songs = vec![]; + let mut invalid_uuids = vec![]; + + for uuid in &self.tracks { + if let Some((track, _)) = lib.query_uuid(uuid) { + songs.push(track.to_owned()); + }else { + invalid_uuids.push(uuid); + } } - } - fn tracks(&self) -> Vec<Uuid> { - self.tracks.to_owned() + + if let SortOrder::Tag(sort_by) = &self.sort_order { + println!("sorting by: {:?}", sort_by); + + songs.par_sort_by(|a, b| { + for (i, sort_option) in sort_by.iter().enumerate() { + dbg!(&i); + let tag_a = match sort_option { + Tag::Field(field_selection) => match a.get_field(field_selection.as_str()) { + Some(field_value) => field_value.to_string(), + None => continue, + }, + _ => match a.get_tag(sort_option) { + Some(tag_value) => tag_value.to_owned(), + None => continue, + }, + }; + + let tag_b = match sort_option { + Tag::Field(field_selection) => match b.get_field(field_selection) { + Some(field_value) => field_value.to_string(), + None => continue, + }, + _ => match b.get_tag(sort_option) { + Some(tag_value) => tag_value.to_owned(), + None => continue, + }, + }; + dbg!(&i); + + if let (Ok(num_a), Ok(num_b)) = (tag_a.parse::<i32>(), tag_b.parse::<i32>()) { + // If parsing succeeds, compare as numbers + return dbg!(num_a.cmp(&num_b)); + } else { + // If parsing fails, compare as strings + return dbg!(tag_a.cmp(&tag_b)); + } + } + + // If all tags are equal, sort by Track number + let path_a = PathBuf::from(a.get_field("location").unwrap().to_string()); + let path_b = PathBuf::from(b.get_field("location").unwrap().to_string()); + + path_a.file_name().cmp(&path_b.file_name()) + }) + } + + (songs, invalid_uuids) } } - impl Default for Playlist { fn default() -> Self { Playlist { @@ -199,7 +276,7 @@ impl Default for Playlist { tracks: Vec::default(), sort_order: SortOrder::Manual, play_count: 0, - play_time: Duration::zero(), + play_time: Duration::from_millis(0), } } } @@ -207,7 +284,7 @@ impl Default for Playlist { #[cfg(test)] mod test_super { use super::*; - use crate::config::config::tests::read_config_lib; + use crate::{config::config::tests::read_config_lib, music_storage::playlist}; #[test] fn list_to_m3u8() { @@ -219,11 +296,24 @@ mod test_super { _ = playlist.to_m3u8(Arc::new(RwLock::from(lib)), ".\\test-config\\playlists\\playlist.m3u8"); } - #[test] - fn m3u8_to_list() { + + fn m3u8_to_list() -> Playlist { let (_, lib) = read_config_lib(); let arc = Arc::new(RwLock::from(lib)); let playlist = Playlist::from_m3u8(".\\test-config\\playlists\\playlist.m3u8", arc).unwrap(); - dbg!(playlist); + + playlist.to_file(".\\test-config\\playlists\\playlist"); + dbg!(playlist) + } + + #[test] + fn out_queue_sort() { + let (_, lib) = read_config_lib(); + let mut list = m3u8_to_list(); + list.sort_order = SortOrder::Tag(vec![Tag::Album]); + + let songs = &list.out_tracks(Arc::new(RwLock::from(lib))); + + dbg!(songs); } } From 94e6c2521901baac363a2776f8055c3e3bc562d4 Mon Sep 17 00:00:00 2001 From: MrDulfin <mrdulfin@mrdulfin.com> Date: Tue, 7 May 2024 18:28:03 -0400 Subject: [PATCH 116/136] made a few minor changes --- Cargo.toml | 1 + src/music_controller/connections.rs | 8 ++++++++ src/music_controller/queue.rs | 2 +- src/music_storage/utils.rs | 6 +++--- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 49ac4e7..e3f236f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,3 +38,4 @@ deunicode = "1.4.2" opener = { version = "0.7.0", features = ["reveal"] } tempfile = "3.10.1" listenbrainz = "0.7.0" +discord-rpc-client = "0.4.0" diff --git a/src/music_controller/connections.rs b/src/music_controller/connections.rs index dec723c..bf5e688 100644 --- a/src/music_controller/connections.rs +++ b/src/music_controller/connections.rs @@ -3,6 +3,7 @@ use std::{ error::Error, }; +use discord_rpc_client::Client; use listenbrainz::ListenBrainz; use uuid::Uuid; @@ -65,6 +66,13 @@ impl Controller { client.listen(artist, track, release)?; Ok(()) } + + pub fn discord_song_change(client: &mut Client,song: Song) { + client.set_activity(|a| { + a.state(format!("Listening to {}", song.get_tag(&Tag::Title).unwrap())) + .into() + }); + } } #[cfg(test)] diff --git a/src/music_controller/queue.rs b/src/music_controller/queue.rs index 483010f..0559696 100644 --- a/src/music_controller/queue.rs +++ b/src/music_controller/queue.rs @@ -66,7 +66,7 @@ impl QueueItemType<'_> { if !shuffled { Some(album.track(*disc as usize, *index as usize).unwrap().location.clone()) }else { - todo!() + todo!() //what to do for non shuffled album } }, ExternalSong(uri) => { Some(uri.clone()) }, diff --git a/src/music_storage/utils.rs b/src/music_storage/utils.rs index 1f68a8c..2f04d4d 100644 --- a/src/music_storage/utils.rs +++ b/src/music_storage/utils.rs @@ -25,12 +25,12 @@ pub(super) fn normalize(input_string: &str) -> String { /// Write any data structure which implements [serde::Serialize] /// out to a [bincode] encoded file compressed using [snap] -pub(super) fn write_file<T: serde::Serialize>( +pub(super) fn write_file<T: serde::Serialize, U: std::convert::AsRef<Path>+std::convert::AsRef<std::ffi::OsStr>+Clone>( library: T, - path: PathBuf, + path: U, ) -> Result<(), Box<dyn Error>> { // Create a temporary name for writing out - let mut writer_name = path.clone(); + let mut writer_name = PathBuf::from(&path); writer_name.set_extension("tmp"); // Create a new BufWriter on the file and a snap frame encoder From 56040bfd28fcff01cfdb8a1f4d0a076d5af22da6 Mon Sep 17 00:00:00 2001 From: MrDulfin <mrdulfin@mrdulfin.com> Date: Sun, 19 May 2024 18:48:29 -0400 Subject: [PATCH 117/136] Updated Controller and added Player Trait --- src/lib.rs | 7 +- src/music_controller/connections.rs | 158 +++++----- src/music_controller/controller.rs | 281 +++--------------- src/music_controller/queue.rs | 224 ++------------ .../gstreamer.rs} | 44 +-- src/music_player/kira.rs | 0 src/music_player/player.rs | 57 ++++ 7 files changed, 226 insertions(+), 545 deletions(-) rename src/{music_player.rs => music_player/gstreamer.rs} (94%) create mode 100644 src/music_player/kira.rs create mode 100644 src/music_player/player.rs diff --git a/src/lib.rs b/src/lib.rs index 387a488..9f6bb0a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,13 +8,16 @@ pub mod music_storage { pub mod db_reader; } -pub mod music_controller{ +pub mod music_controller { pub mod controller; pub mod connections; pub mod queue; } -pub mod music_player; +pub mod music_player { + pub mod gstreamer; + pub mod player; +} #[allow(clippy::module_inception)] pub mod config { pub mod config; diff --git a/src/music_controller/connections.rs b/src/music_controller/connections.rs index bf5e688..b2ceb85 100644 --- a/src/music_controller/connections.rs +++ b/src/music_controller/connections.rs @@ -1,103 +1,103 @@ -use std::{ - sync::{Arc, RwLock}, - error::Error, -}; +// use std::{ +// sync::{Arc, RwLock}, +// error::Error, +// }; -use discord_rpc_client::Client; -use listenbrainz::ListenBrainz; -use uuid::Uuid; +// use discord_rpc_client::Client; +// use listenbrainz::ListenBrainz; +// use uuid::Uuid; -use crate::{ - config::config::Config, music_controller::controller::{Controller, QueueCmd, QueueResponse}, music_storage::library::{MusicLibrary, Song, Tag} -}; +// use crate::{ +// config::config::Config, music_controller::controller::{Controller, QueueCmd, QueueResponse}, music_storage::library::{MusicLibrary, Song, Tag} +// }; -use super::controller::DatabaseResponse; +// use super::controller::DatabaseResponse; -impl Controller { - pub fn listenbrainz_authenticate(&mut self) -> Result<ListenBrainz, Box<dyn Error>> { - let config = &self.config.read().unwrap(); - let mut client = ListenBrainz::new(); +// impl Controller { +// pub fn listenbrainz_authenticate(&mut self) -> Result<ListenBrainz, Box<dyn Error>> { +// let config = &self.config.read().unwrap(); +// let mut client = ListenBrainz::new(); - let lbz_token = match &config.connections.listenbrainz_token { - Some(token) => token, - None => todo!("No ListenBrainz token in config") - }; +// let lbz_token = match &config.connections.listenbrainz_token { +// Some(token) => token, +// None => todo!("No ListenBrainz token in config") +// }; - if !client.is_authenticated() { - client.authenticate(lbz_token)?; - } +// if !client.is_authenticated() { +// client.authenticate(lbz_token)?; +// } - Ok(client) - } - pub fn lbz_scrobble(&self, client: ListenBrainz, uuid: Uuid) -> Result<(), Box<dyn Error>> { - let config = &self.config.read().unwrap(); +// Ok(client) +// } +// pub fn lbz_scrobble(&self, client: ListenBrainz, uuid: Uuid) -> Result<(), Box<dyn Error>> { +// let config = &self.config.read().unwrap(); - &self.db_mail.send(super::controller::DatabaseCmd::QueryUuid(uuid)); - let res = &self.db_mail.recv()?; - let song = match res { - DatabaseResponse::Song(song) => song, - _ => todo!() - }; - let unknown = &"unknown".to_string(); - let artist = song.get_tag(&Tag::Artist).unwrap_or(unknown); - let track = song.get_tag(&Tag::Title).unwrap_or(unknown); - let release = song.get_tag(&Tag::Album).map(|rel| rel.as_str()); +// &self.db_mail.send(super::controller::DatabaseCmd::QueryUuid(uuid)); +// let res = &self.db_mail.recv()?; +// let song = match res { +// DatabaseResponse::Song(song) => song, +// _ => todo!() +// }; +// let unknown = &"unknown".to_string(); +// let artist = song.get_tag(&Tag::Artist).unwrap_or(unknown); +// let track = song.get_tag(&Tag::Title).unwrap_or(unknown); +// let release = song.get_tag(&Tag::Album).map(|rel| rel.as_str()); - client.listen(artist, track, release)?; - Ok(()) - } +// client.listen(artist, track, release)?; +// Ok(()) +// } - pub fn lbz_now_playing(&self, client: ListenBrainz, uuid: Uuid) -> Result<(), Box<dyn Error>> { - let config = &self.config.read().unwrap(); +// pub fn lbz_now_playing(&self, client: ListenBrainz, uuid: Uuid) -> Result<(), Box<dyn Error>> { +// let config = &self.config.read().unwrap(); - &self.db_mail.send(super::controller::DatabaseCmd::QueryUuid(uuid)); - let res = &self.db_mail.recv()?; - let song = match res { - DatabaseResponse::Song(song) => song, - _ => todo!() - }; - let unknown = &"unknown".to_string(); - let artist = song.get_tag(&Tag::Artist).unwrap_or(unknown); - let track = song.get_tag(&Tag::Title).unwrap_or(unknown); - let release = song.get_tag(&Tag::Album).map(|rel| rel.as_str()); +// &self.db_mail.send(super::controller::DatabaseCmd::QueryUuid(uuid)); +// let res = &self.db_mail.recv()?; +// let song = match res { +// DatabaseResponse::Song(song) => song, +// _ => todo!() +// }; +// let unknown = &"unknown".to_string(); +// let artist = song.get_tag(&Tag::Artist).unwrap_or(unknown); +// let track = song.get_tag(&Tag::Title).unwrap_or(unknown); +// let release = song.get_tag(&Tag::Album).map(|rel| rel.as_str()); - client.listen(artist, track, release)?; - Ok(()) - } +// client.listen(artist, track, release)?; +// Ok(()) +// } - pub fn discord_song_change(client: &mut Client,song: Song) { - client.set_activity(|a| { - a.state(format!("Listening to {}", song.get_tag(&Tag::Title).unwrap())) - .into() - }); - } -} +// pub fn discord_song_change(client: &mut Client,song: Song) { +// client.set_activity(|a| { +// a.state(format!("Listening to {}", song.get_tag(&Tag::Title).unwrap())) +// .into() +// }); +// } +// } -#[cfg(test)] -mod test_super { - use std::{thread::sleep, time::Duration}; +// #[cfg(test)] +// mod test_super { +// use std::{thread::sleep, time::Duration}; - use super::*; - use crate::config::config::tests::read_config_lib; +// use super::*; +// use crate::config::config::tests::read_config_lib; - #[test] - fn listenbrainz() { - let mut c = Controller::start(".\\test-config\\config_test.json").unwrap(); +// #[test] +// fn listenbrainz() { +// let mut c = Controller::start(".\\test-config\\config_test.json").unwrap(); - let client = c.listenbrainz_authenticate().unwrap(); +// let client = c.listenbrainz_authenticate().unwrap(); - c.q_new().unwrap(); - c.queue_mail[0].send(QueueCmd::SetVolume(0.04)).unwrap(); +// c.q_new().unwrap(); +// c.queue_mail[0].send(QueueCmd::SetVolume(0.04)).unwrap(); - let songs = c.lib_get_songs(); +// let songs = c.lib_get_songs(); - c.q_enqueue(0, songs[1].location.to_owned()).unwrap(); - c.q_play(0).unwrap(); +// c.q_enqueue(0, songs[1].location.to_owned()).unwrap(); +// c.q_play(0).unwrap(); - sleep(Duration::from_secs(100)); - c.lbz_scrobble(client, songs[1].uuid).unwrap(); - } -} +// sleep(Duration::from_secs(100)); +// c.lbz_scrobble(client, songs[1].uuid).unwrap(); +// } +// } diff --git a/src/music_controller/controller.rs b/src/music_controller/controller.rs index d13a62f..8efb630 100644 --- a/src/music_controller/controller.rs +++ b/src/music_controller/controller.rs @@ -13,7 +13,8 @@ use std::error::Error; use crossbeam_channel::unbounded; use uuid::Uuid; -use crate::music_controller::queue::{QueueItem, QueueItemType}; +use crate::music_controller::queue::QueueItem; +use crate::music_player::gstreamer::GStreamer; use crate::music_storage::library::{Tag, URI}; use crate::{ music_storage::library::{MusicLibrary, Song}, @@ -22,63 +23,10 @@ use crate::{ }; pub struct Controller { - // queues: Vec<Queue>, + pub queue: Queue, pub config: Arc<RwLock<Config>>, - // library: MusicLibrary, - pub(super) controller_mail: MailMan<ControllerCmd, ControllerResponse>, - pub(super) db_mail: MailMan<DatabaseCmd, DatabaseResponse>, - pub(super) queue_mail: Vec<MailMan<QueueCmd, QueueResponse>>, -} -#[derive(Debug)] -pub(super) enum ControllerCmd { - Default, - Test -} - -#[derive(Debug)] -pub(super) enum ControllerResponse { - Empty, - QueueMailMan(MailMan<QueueCmd, QueueResponse>), - -} - -#[derive(Debug)] -pub(super) enum DatabaseCmd { - Default, - Test, - SaveLibrary, - GetSongs, - QueryUuid(Uuid), - QueryUuids(Vec<Uuid>), - ReadFolder(String), -} - -#[derive(Debug)] -pub(super) enum DatabaseResponse { - Empty, - Song(Song), - Songs(Vec<Song>), - Library(MusicLibrary), -} - -#[derive(Debug)] -pub(super) enum QueueCmd { - Default, - Test, - Play, - Pause, - // SetSongs(Vec<QueueItem<QueueState>>), - // SetLocation(URI), - Enqueue(URI), - SetVolume(f64), -} - -#[derive(Debug)] -pub(super) enum QueueResponse { - Default, - Test, - Index(i32), - Uuid(Uuid), + pub library: MusicLibrary, + player_mail: MailMan<PlayerCmd, PlayerRes> } #[derive(Debug)] @@ -115,227 +63,72 @@ impl<T: Send, U: Send> MailMan<T, U> { } } +enum PlayerCmd { + Test(URI) +} + +enum PlayerRes { + Test +} + #[allow(unused_variables)] impl Controller { pub fn start<P>(config_path: P) -> Result<Self, Box<dyn Error>> where std::path::PathBuf: std::convert::From<P> { let config_path = PathBuf::from(config_path); + let config = Config::read_file(config_path)?; let uuid = config.libraries.get_default()?.uuid; let config_ = Arc::new(RwLock::from(config)); - let mut lib = MusicLibrary::init(config_.clone(), uuid)?; + let library = MusicLibrary::init(config_.clone(), uuid)?; - let config = config_.clone(); - let (out_thread_controller, in_thread) = MailMan::double(); - let monitor_thread = spawn(move || { - use ControllerCmd::*; - loop { - let command = in_thread.recv().unwrap(); + let (player_mail, in_thread) = MailMan::<PlayerCmd, PlayerRes>::double(); - match command { - Default => (), - Test => { - in_thread.send(ControllerResponse::Empty).unwrap(); - }, - } - } - }); + spawn(move || { + let mut player = GStreamer::new().unwrap(); - let config = config_.clone(); - let (out_thread_db, in_thread) = MailMan::double(); - let db_monitor = spawn(move || { - use DatabaseCmd::*; - loop { - let command = in_thread.recv().unwrap(); - - match command { - Default => {}, - Test => { - in_thread.send(DatabaseResponse::Empty).unwrap(); - }, - GetSongs => { - let songs = lib.query_tracks(&String::from(""), &(vec![Tag::Title]), &(vec![Tag::Title])).unwrap().iter().cloned().cloned().collect(); - in_thread.send(DatabaseResponse::Songs(songs)).unwrap(); - }, - SaveLibrary => { - //TODO: make this send lib ref to the function to save instead - lib.save(config.read().unwrap().to_owned()).unwrap(); - }, - QueryUuid(uuid) => { - match lib.query_uuid(&uuid) { - Some(song) => in_thread.send(DatabaseResponse::Song(song.0.clone())).unwrap(), - None => in_thread.send(DatabaseResponse::Empty).unwrap(), - } - }, - QueryUuids(uuids) => { - let mut vec = Vec::new(); - for uuid in uuids { - match lib.query_uuid(&uuid) { - Some(song) => vec.push(song.0.clone()), - None => unimplemented!() - } - } - in_thread.send(DatabaseResponse::Songs(vec)).unwrap(); - }, - ReadFolder(folder) => { - lib.scan_folder(&folder).unwrap(); - in_thread.send(DatabaseResponse::Empty).unwrap(); + while true { + match in_thread.recv().unwrap() { + PlayerCmd::Test(uri) => { + &player.set_volume(0.04); + _ = &player.enqueue_next(&uri).unwrap(); + _ = &player.play(); + in_thread.send(PlayerRes::Test).unwrap(); } - } } + }); Ok( Controller { - // queues: Vec::new(), + queue: Queue::new(), config: config_.clone(), - controller_mail: out_thread_controller, - db_mail: out_thread_db, - queue_mail: Vec::new(), + library, + player_mail } ) } - pub fn lib_get_songs(&self) -> Vec<Song> { - self.db_mail.send(DatabaseCmd::GetSongs); - match self.db_mail.recv().unwrap() { - DatabaseResponse::Songs(songs) => songs, - _ => Vec::new() - } + pub fn q_add(&self, item: Uuid, source:super::queue::PlayerLocation , by_human: bool) { + self.queue.add_item(item, source, by_human) } - pub fn lib_scan_folder(&self, folder: String) -> Result<(), Box<dyn Error>> { - let mail = &self.db_mail; - mail.send(DatabaseCmd::ReadFolder(folder))?; - dbg!(mail.recv()?); - Ok(()) - } - - pub fn lib_save(&self) -> Result<(), Box<dyn Error>> { - self.db_mail.send(DatabaseCmd::SaveLibrary); - Ok(()) - } - - pub fn q_new(&mut self) -> Result<usize, Box<dyn Error>> { - let (out_thread_queue, in_thread) = MailMan::<QueueCmd, QueueResponse>::double(); - let queues_monitor = spawn(move || { - use QueueCmd::*; - let mut queue = Queue::new().unwrap(); - loop { - let command = in_thread.recv().unwrap(); - match command { - Default => {}, - Test => { in_thread.send(QueueResponse::Test).unwrap() }, - Play => { - match queue.player.play() { - Ok(_) => in_thread.send(QueueResponse::Default).unwrap(), - Err(_) => todo!() - }; - - }, - Pause => { - match queue.player.pause() { - Ok(_) => in_thread.send(QueueResponse::Default).unwrap(), - Err(_) => todo!() - } - }, - // SetSongs(songs) => { - // queue.set_tracks(songs); - // in_thread.send(QueueResponse::Default).unwrap(); - // }, - Enqueue(uri) => { - queue.player.enqueue_next(&uri).unwrap(); - - // in_thread.send(QueueResponse::Default).unwrap(); - }, - SetVolume(vol) => { - queue.player.set_volume(vol); - } - } - } - }); - self.queue_mail.push(out_thread_queue); - Ok(self.queue_mail.len() - 1) - } - - pub fn q_play(&self, index: usize) -> Result<(), Box<dyn Error>> { - let mail = &self.queue_mail[index]; - mail.send(QueueCmd::Play)?; - dbg!(mail.recv()?); - Ok(()) - } - - pub fn q_pause(&self, index: usize) -> Result<(), Box<dyn Error>> { - let mail = &self.queue_mail[index]; - mail.send(QueueCmd::Pause)?; - dbg!(mail.recv()?); - Ok(()) - } - - pub fn q_set_volume(&self, index: usize, volume: f64) -> Result<(), Box<dyn Error>> { - let mail = &self.queue_mail[index]; - mail.send(QueueCmd::SetVolume(volume))?; - Ok(()) - } - - // fn q_set_songs(&self, index: usize, songs: Vec<QueueItem<QueueState>>) -> Result<(), Box<dyn Error>> { - // let mail = &self.queue_mail[index]; - // mail.send(QueueCmd::SetSongs(songs))?; - // dbg!(mail.recv()?); - // Ok(()) - // } - - pub fn q_enqueue(&self, index: usize, uri: URI) -> Result<(), Box<dyn Error>> { - let mail = &self.queue_mail[index]; - mail.send(QueueCmd::Enqueue(uri))?; - // dbg!(mail.recv()?); - Ok(()) - } - - } #[cfg(test)] -mod tests { +mod test_super { use std::{thread::sleep, time::Duration}; - use super::Controller; - - #[test] - fn play_test() { - let mut a = match Controller::start("test-config/config_test.json".to_string()) { - Ok(c) => c, - Err(e) => panic!("{e}") - }; - sleep(Duration::from_millis(500)); - - let i = a.q_new().unwrap(); - a.q_set_volume(i, 0.04); - // a.new_queue(); - let songs = a.lib_get_songs(); - a.q_enqueue(i, songs[2].location.clone()); - // a.enqueue(1, songs[2].location.clone()); - a.q_play(i).unwrap(); - // a.play(1).unwrap(); - - sleep(Duration::from_secs(10)); - a.q_pause(i); - sleep(Duration::from_secs(10)); - a.q_play(i); - sleep(Duration::from_secs(1000)); - } + use super::*; #[test] fn test_() { - let a = match Controller::start("test-config/config_test.json".to_string()) { - Ok(c) => c, - Err(e) => panic!("{e}") - }; - a.lib_scan_folder("F:/Music/Mp3".to_string()); - a.lib_save(); + let c = Controller::start("F:\\Dangoware\\Dango Music Player\\dmp-core\\test-config\\config_test.json").unwrap(); + + sleep(Duration::from_secs(60)); } -} +} \ No newline at end of file diff --git a/src/music_controller/queue.rs b/src/music_controller/queue.rs index 0559696..39736e6 100644 --- a/src/music_controller/queue.rs +++ b/src/music_controller/queue.rs @@ -1,8 +1,5 @@ use uuid::Uuid; -use crate::{ - music_player::{Player, PlayerError}, - music_storage::library::{Album, MusicLibrary, URI} -}; +use crate::music_storage::library::{MusicLibrary, Song, URI}; use std::{ error::Error, sync::{Arc, RwLock} @@ -27,54 +24,6 @@ pub enum QueueState { NoState, } -#[derive(Debug, Clone, PartialEq)] -#[non_exhaustive] -pub enum QueueItemType<'a> { - Song(Uuid), - ExternalSong(URI), - Album{ - album: Album<'a>, - shuffled: bool, - order: Option<Vec<Uuid>>, - // disc #, track # - current: (i32, i32) - }, - Playlist { - uuid: Uuid, - shuffled: bool, - order: Option<Vec<Uuid>>, - current: Uuid - }, - None, - Test -} - -impl QueueItemType<'_> { - fn get_uri(&self, lib: Arc<RwLock<MusicLibrary>>) -> Option<URI> { - use QueueItemType::*; - - let lib = lib.read().unwrap(); - match self { - Song(uuid) => { - if let Some((song, _)) = lib.query_uuid(uuid) { - Some(song.location.clone()) - }else { - Option::None - } - }, - Album{album, shuffled, current: (disc, index), ..} => { - if !shuffled { - Some(album.track(*disc as usize, *index as usize).unwrap().location.clone()) - }else { - todo!() //what to do for non shuffled album - } - }, - ExternalSong(uri) => { Some(uri.clone()) }, - _ => { Option::None } - } - } -} - // TODO: move this to a different location to be used elsewhere #[derive(Debug, Clone, PartialEq)] #[non_exhaustive] @@ -88,16 +37,16 @@ pub enum PlayerLocation { #[derive(Debug, Clone, PartialEq)] #[non_exhaustive] -pub struct QueueItem<'a> { - pub(super) item: QueueItemType<'a>, +pub struct QueueItem { + pub(super) item: Song, pub(super) state: QueueState, pub(super) source: PlayerLocation, pub(super) by_human: bool } -impl QueueItem<'_> { - fn new() -> Self { +impl From<Song> for QueueItem { + fn from(song: Song) -> Self { QueueItem { - item: QueueItemType::None, + item: song, state: QueueState::NoState, source: PlayerLocation::Library, by_human: false @@ -107,15 +56,14 @@ impl QueueItem<'_> { #[derive(Debug)] -pub struct Queue<'a> { - pub player: Player, - pub name: String, - pub items: Vec<QueueItem<'a>>, - pub played: Vec<QueueItem<'a>>, - pub loop_: bool +pub struct Queue { + pub items: Vec<QueueItem>, + pub played: Vec<QueueItem>, + pub loop_: bool, + pub shuffle: bool } -impl<'a> Queue<'a> { +impl Queue { fn has_addhere(&self) -> bool { for item in &self.items { if item.state == QueueState::AddHere { @@ -126,28 +74,26 @@ impl<'a> Queue<'a> { } fn dbg_items(&self) { - dbg!(self.items.iter().map(|item| item.item.clone() ).collect::<Vec<QueueItemType>>(), self.items.len()); + dbg!(self.items.iter().map(|item| item.item.clone() ).collect::<Vec<Song>>(), self.items.len()); } - pub fn new() -> Result<Self, PlayerError> { - Ok( - Queue { - player: Player::new()?, - name: String::new(), - items: Vec::new(), - played: Vec::new(), - loop_: false, - } - ) + pub fn new() -> Self { + //TODO: Make the queue take settings from config/state if applicable + Queue { + items: Vec::new(), + played: Vec::new(), + loop_: false, + shuffle: false, + } } - pub fn set_items(&mut self, tracks: Vec<QueueItem<'a>>) { + pub fn set_items(&mut self, tracks: Vec<QueueItem>) { let mut tracks = tracks; self.items.clear(); self.items.append(&mut tracks); } - pub fn add_item(&mut self, item: QueueItemType<'a>, source: PlayerLocation, by_human: bool) { + pub fn add_item(&mut self, item: Song, source: PlayerLocation, by_human: bool) { let mut i: usize = 0; self.items = self.items.iter().enumerate().map(|(j, item_)| { @@ -168,7 +114,7 @@ impl<'a> Queue<'a> { }); } - pub fn add_item_next(&mut self, item: QueueItemType<'a>, source: PlayerLocation) { + pub fn add_item_next(&mut self, item: Song, source: PlayerLocation) { use QueueState::*; let empty = self.items.is_empty(); @@ -176,14 +122,14 @@ impl<'a> Queue<'a> { (if empty { 0 } else { 1 }), QueueItem { item, - state: if (self.items.get(1).is_none() || (!self.has_addhere() && self.items.get(1).is_some()) || empty) { AddHere } else { NoState }, + state: if (self.items.get(1).is_none() || !self.has_addhere() && self.items.get(1).is_some()) || empty { AddHere } else { NoState }, source, by_human: true } ) } - pub fn add_multi(&mut self, items: Vec<QueueItemType>, source: PlayerLocation, by_human: bool) { + pub fn add_multi(&mut self, items: Vec<Song>, source: PlayerLocation, by_human: bool) { } @@ -239,14 +185,10 @@ impl<'a> Queue<'a> { use QueueState::*; let empty = self.items.is_empty(); - let nothing_error = Err(QueueError::EmptyQueue); - let index = if !empty { index } else { return nothing_error; }; + + let index = if !empty { index } else { return Err(QueueError::EmptyQueue); }; if !empty && index < self.items.len() { - let position = self.player.position(); - if position.is_some_and(|dur| !dur.is_zero() ) { - self.played.push(self.items[0].clone()); - } let to_item = self.items[index].clone(); @@ -263,13 +205,13 @@ impl<'a> Queue<'a> { } // dbg!(&to_item.item, &self.items[ind].item); }else if empty { - return nothing_error; + return Err(QueueError::EmptyQueue); }else { break; } } }else { - return Err(QueueError::EmptyQueue.into()); + return Err(QueueError::EmptyQueue); } Ok(()) } @@ -287,9 +229,7 @@ impl<'a> Queue<'a> { } #[allow(clippy::should_implement_trait)] - pub fn next(&mut self, lib: Arc<RwLock<MusicLibrary>>) -> Result<OutQueue, Box<dyn Error>> { - - + pub fn next(&mut self) -> Result<&QueueItem, Box<dyn Error>> { if self.items.is_empty() { if self.loop_ { @@ -300,119 +240,21 @@ impl<'a> Queue<'a> { } // TODO: add an algorithm to detect if the song should be skipped let item = self.items[0].clone(); - let uri: URI = match &self.items[1].item { - QueueItemType::Song(uuid) => { - // TODO: Refactor later for multiple URIs - match &lib.read().unwrap().query_uuid(uuid) { - Some(song) => song.0.location.clone(), - None => return Err("Uuid does not exist!".into()), - } - }, - QueueItemType::Album { album, current, ..} => { - let (disc, track) = (current.0 as usize, current.1 as usize); - match album.track(disc, track) { - Some(track) => track.location.clone(), - None => return Err(format!("Track in Album {} at disc {} track {} does not exist!", album.title(), disc, track).into()) - } - }, - QueueItemType::Playlist { current, .. } => { - // TODO: Refactor later for multiple URIs - match &lib.read().unwrap().query_uuid(current) { - Some(song) => song.0.location.clone(), - None => return Err("Uuid does not exist!".into()), - } - }, - _ => todo!() - }; - if !self.player.is_paused() { - self.player.enqueue_next(&uri)?; - self.player.play()? - } + if self.items[0].state == QueueState::AddHere || !self.has_addhere() { self.items[1].state = QueueState::AddHere; } self.played.push(item); self.items.remove(0); - Ok(todo!()) + Ok(&self.items[1]) } pub fn prev() {} - pub fn enqueue_item(&mut self, item: QueueItem, lib: Arc<RwLock<MusicLibrary>>) -> Result<(), Box<dyn Error>> { - if let Some(uri) = item.item.get_uri(lib) { - self.player.enqueue_next(&uri)?; - }else { - return Err("this item does not exist!".into()); - } - Ok(()) - } pub fn check_played(&mut self) { while self.played.len() > 50 { self.played.remove(0); } } } - -pub struct OutQueue { - -} - -pub enum OutQueueItem { - -} - - -#[test] -fn item_add_test() { - let mut q = Queue::new().unwrap(); - - for _ in 0..5 { - // dbg!("tick!"); - q.add_item(QueueItemType::Song(Uuid::new_v4()), PlayerLocation::Library, true); - // dbg!(&q.items, &q.items.len()); - } - - for _ in 0..1 { - q.remove_item(0).inspect_err(|e| println!("{e:?}")); - } - for _ in 0..2 { - q.items.push(QueueItem { item: QueueItemType::Test, state: QueueState::NoState, source: PlayerLocation::Library, by_human: false }); - } - dbg!(5); - - q.add_item_next(QueueItemType::Test, PlayerLocation::Test); - dbg!(6); - - dbg!(&q.items, &q.items.len()); -} - -#[test] -fn test_() { - let mut q = Queue::new().unwrap(); - for _ in 0..400 { - q.items.push(QueueItem { item: QueueItemType::Song(Uuid::new_v4()), state: QueueState::NoState, source: PlayerLocation::File, by_human: false }); - } - for _ in 0..50000 { - q.add_item(QueueItemType::Song(Uuid::new_v4()), PlayerLocation::Library, true); - } - // q.add_item_next(QueueItemType::Test, PlayerLocation::File); - - // dbg!(&q.items, &q.items.len()); - -} - -#[test] -fn move_test() { - let mut q = Queue::new().unwrap(); - - for _ in 0..5 { - q.add_item(QueueItemType::Song(Uuid::new_v4()), PlayerLocation::Library, true); - } - // q.add_item(QueueItemType::Test, QueueSource::Library, true).unwrap(); - dbg!(&q.items, &q.items.len()); - - q.move_to(3).inspect_err(|e| {dbg!(e);}); - dbg!(&q.items, &q.items.len()); - // q.dbg_items(); -} diff --git a/src/music_player.rs b/src/music_player/gstreamer.rs similarity index 94% rename from src/music_player.rs rename to src/music_player/gstreamer.rs index e310403..91abfde 100644 --- a/src/music_player.rs +++ b/src/music_player/gstreamer.rs @@ -15,8 +15,10 @@ use gstreamer::prelude::*; use chrono::Duration; use thiserror::Error; +use super::player::{Player, PlayerError}; + #[derive(Debug)] -pub enum PlayerCmd { +pub enum GstCmd { Play, Pause, Eos, @@ -24,7 +26,7 @@ pub enum PlayerCmd { } #[derive(Debug, PartialEq, Eq)] -pub enum PlayerState { +pub enum GstState { Playing, Paused, Ready, @@ -33,7 +35,7 @@ pub enum PlayerState { VoidPending, } -impl From<gst::State> for PlayerState { +impl From<gst::State> for GstState { fn from(value: gst::State) -> Self { match value { gst::State::VoidPending => Self::VoidPending, @@ -45,7 +47,7 @@ impl From<gst::State> for PlayerState { } } -impl TryInto<gst::State> for PlayerState { +impl TryInto<gst::State> for GstState { fn try_into(self) -> Result<gst::State, Box<dyn Error>> { match self { Self::VoidPending => Ok(gst::State::VoidPending), @@ -60,24 +62,6 @@ impl TryInto<gst::State> for PlayerState { type Error = Box<dyn Error>; } -#[derive(Error, Debug)] -pub enum PlayerError { - #[error("player initialization failed")] - Init(#[from] glib::Error), - #[error("element factory failed to create playbin3")] - Factory(#[from] glib::BoolError), - #[error("could not change playback state")] - StateChange(#[from] gst::StateChangeError), - #[error("the file or source is not found")] - NotFound, - #[error("failed to build gstreamer item")] - Build, - #[error("poison error")] - Poison, - #[error("general player error")] - General, -} - #[derive(Debug, PartialEq, Eq)] enum PlaybackStats { Idle, @@ -91,10 +75,10 @@ enum PlaybackStats { /// An instance of a music player with a GStreamer backend #[derive(Debug)] -pub struct Player { +pub struct GStreamer { source: Option<URI>, //pub message_tx: Sender<PlayerCmd>, - pub message_rx: crossbeam::channel::Receiver<PlayerCmd>, + pub message_rx: crossbeam::channel::Receiver<GstCmd>, playback_tx: crossbeam::channel::Sender<PlaybackStats>, playbin: Arc<RwLock<Element>>, @@ -105,7 +89,7 @@ pub struct Player { position: Arc<RwLock<Option<Duration>>>, } -impl Player { +impl GStreamer { pub fn new() -> Result<Self, PlayerError> { // Initialize GStreamer, maybe figure out how to nicely fail here gst::init()?; @@ -162,14 +146,14 @@ impl Player { // Check if the current playback position is close to the end let finish_point = end - Duration::milliseconds(250); if pos_temp.unwrap() >= end { - let _ = playback_tx.try_send(PlayerCmd::Eos); + let _ = playback_tx.try_send(GstCmd::Eos); playbin_arc .write() .unwrap() .set_state(gst::State::Ready) .expect("Unable to set the pipeline state"); } else if pos_temp.unwrap() >= finish_point { - let _ = playback_tx.try_send(PlayerCmd::AboutToFinish); + let _ = playback_tx.try_send(GstCmd::AboutToFinish); } // This has to be done AFTER the current time in the file @@ -468,7 +452,7 @@ impl Player { } /// Get the current state of the playback - pub fn state(&mut self) -> PlayerState { + pub fn state(&mut self) -> GstState { self.playbin().unwrap().current_state().into() /* match *self.buffer.read().unwrap() { @@ -498,7 +482,9 @@ impl Player { } } -impl Drop for Player { +// impl Player for GStreamer {} + +impl Drop for GStreamer { /// Cleans up the `GStreamer` pipeline and the monitoring /// thread when [Player] is dropped. fn drop(&mut self) { diff --git a/src/music_player/kira.rs b/src/music_player/kira.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/music_player/player.rs b/src/music_player/player.rs new file mode 100644 index 0000000..a30b2a6 --- /dev/null +++ b/src/music_player/player.rs @@ -0,0 +1,57 @@ +use chrono::Duration; +use thiserror::Error; +use gstreamer as gst; + +use crate::music_storage::library::URI; + + +#[derive(Error, Debug)] +pub enum PlayerError { + #[error("player initialization failed")] + Init(#[from] glib::Error), + #[error("element factory failed to create playbin3")] + Factory(#[from] glib::BoolError), + #[error("could not change playback state")] + StateChange(#[from] gst::StateChangeError), + #[error("the file or source is not found")] + NotFound, + #[error("failed to build gstreamer item")] + Build, + #[error("poison error")] + Poison, + #[error("general player error")] + General, +} + +pub trait Player { + fn source(&self) -> &Option<URI>; + + fn enqueue_next(&mut self, next_track: &URI) -> Result<(), PlayerError>; + + fn set_volume(&mut self, volume: f64); + + fn volume(&mut self) -> f64; + + fn ready(&mut self) -> Result<(), PlayerError>; + + fn play(&mut self) -> Result<(), PlayerError>; + + fn resume(&mut self) -> Result<(), PlayerError>; + + fn pause(&mut self) -> Result<(), PlayerError>; + + fn stop(&mut self) -> Result<(), PlayerError>; + + fn is_paused(&mut self) -> bool; + + fn position(&mut self) -> Option<Duration>; + + fn duration(&mut self) -> Option<Duration>; + + fn raw_duration(&self) -> Option<Duration>; + + fn seek_by(&mut self, seek_amount: Duration) -> Result<(), PlayerError>; + + fn seek_to(&mut self, target_pos: Duration) -> Result<(), PlayerError>; + +} \ No newline at end of file From 9457c5c9965e44c256ea91f9182d99608dacf883 Mon Sep 17 00:00:00 2001 From: MrDulfin <mrdulfin@mrdulfin.com> Date: Sun, 19 May 2024 22:29:30 -0400 Subject: [PATCH 118/136] fixed a function and removed Album Art function --- src/music_controller/controller.rs | 3 +- src/music_storage/library.rs | 126 +++++++---------------------- 2 files changed, 33 insertions(+), 96 deletions(-) diff --git a/src/music_controller/controller.rs b/src/music_controller/controller.rs index 525fd9f..2ab8811 100644 --- a/src/music_controller/controller.rs +++ b/src/music_controller/controller.rs @@ -107,7 +107,8 @@ impl Controller { }) } - pub fn q_add(&self, item: Uuid, source: super::queue::PlayerLocation, by_human: bool) { + pub fn q_add(&mut self, item: &Uuid, source: super::queue::PlayerLocation, by_human: bool) { + let item = self.library.query_uuid(item).unwrap().0.to_owned(); self.queue.add_item(item, source, by_human) } } diff --git a/src/music_storage/library.rs b/src/music_storage/library.rs index a64101f..4818324 100644 --- a/src/music_storage/library.rs +++ b/src/music_storage/library.rs @@ -9,16 +9,15 @@ use std::error::Error; use std::io::Write; use std::ops::ControlFlow::{Break, Continue}; - // Files use file_format::{FileFormat, Kind}; use glib::filename_to_uri; use lofty::{AudioFile, ItemKey, ItemValue, ParseOptions, Probe, TagType, TaggedFileExt}; use rcue::parser::parse_from_file; -use uuid::Uuid; use std::fs::{self, File}; -use tempfile::TempDir; use std::path::{Path, PathBuf}; +use tempfile::TempDir; +use uuid::Uuid; use walkdir::WalkDir; // Time @@ -152,7 +151,7 @@ pub enum SongType { Main, Instrumental, Remix, - Custom(String) + Custom(String), } impl Default for SongType { @@ -182,10 +181,9 @@ pub struct Song { pub date_modified: Option<DateTime<Utc>>, pub album_art: Vec<AlbumArt>, pub tags: BTreeMap<Tag, String>, - pub internal_tags: Vec<InternalTag> + pub internal_tags: Vec<InternalTag>, } - impl Song { /// Get a tag's value /// @@ -342,7 +340,6 @@ impl Song { for file in cue_data.files.iter() { let audio_location = &parent_dir.join(file.file.clone()); - if !audio_location.exists() { continue; } @@ -447,80 +444,12 @@ impl Song { date_modified: Some(chrono::offset::Utc::now()), tags, album_art, - internal_tags: Vec::new() + internal_tags: Vec::new(), }; tracks.push((new_song, audio_location.clone())); } } - Ok(tracks) - } - - /// Takes the AlbumArt[index] and opens it in the native file viewer - - pub fn open_album_art(&self, index: usize, temp_dir: &TempDir) -> Result<(), Box<dyn Error>> { - use opener::open; - use urlencoding::decode; - - if index >= self.album_art.len() { - return Err("Index out of bounds".into()); - } - - let uri = match &self.album_art[index] { - AlbumArt::External(uri) => { - PathBuf::from(decode(match uri.as_uri().strip_prefix("file:///") { - Some(e) => e, - None => return Err("Invalid path?".into()) - })?.to_owned().to_string()) - }, - AlbumArt::Embedded(_) => { - let normal_options = ParseOptions::new().parsing_mode(lofty::ParsingMode::Relaxed); - let blank_tag = &lofty::Tag::new(TagType::Id3v2); - let tagged_file: lofty::TaggedFile; - - // TODO: add support for other URI types... or don't - #[cfg(target_family = "windows")] - let uri = urlencoding::decode( - match self.primary_uri()?.0.as_uri().strip_prefix("file:///") { - Some(str) => str, - None => return Err("invalid path.. again?".into()) - })?.into_owned(); - - #[cfg(target_family = "unix")] - let uri = urlencoding::decode( - match self.primary_uri()?.as_uri().strip_prefix("file://") { - Some(str) => str, - None => return Err("invalid path.. again?".into()) - })?.into_owned(); - - let tag = match Probe::open(uri)?.options(normal_options).read() { - Ok(file) => { - tagged_file = file; - - match tagged_file.primary_tag() { - Some(primary_tag) => primary_tag, - - None => match tagged_file.first_tag() { - Some(first_tag) => first_tag, - None => blank_tag, - }, - } - } - - Err(_) => blank_tag, - }; - - let data = tag.pictures()[index].data(); - - let fmt = FileFormat::from_bytes(data); - let file_path = temp_dir.path().join(format!("{}_{index}.{}", self.uuid, fmt.extension())); - - File::create(&file_path)?.write_all(data)?; - - file_path - }, - }; - dbg!(open(dbg!(uri))?); - Ok(()) + Ok(tracks) } /// Returns a reference to the first valid URI in the song, and any invalid URIs that come before it, or errors if there are no valid URIs @@ -533,13 +462,20 @@ impl Song { if uri.exists()? { valid_uri = Some(uri); break; - }else { + } else { invalid_uris.push(uri); } } match valid_uri { - Some(uri) => Ok((uri, if !invalid_uris.is_empty() { Some(invalid_uris) } else { None } )), - None => Err("No valid URIs for this song".into()) + Some(uri) => Ok(( + uri, + if !invalid_uris.is_empty() { + Some(invalid_uris) + } else { + None + }, + )), + None => Err("No valid URIs for this song".into()), } } } @@ -610,7 +546,7 @@ impl URI { pub fn as_path(&self) -> Result<&PathBuf, Box<dyn Error>> { if let Self::Local(path) = self { Ok(path) - }else { + } else { Err("This URI is not local!".into()) } } @@ -618,7 +554,7 @@ impl URI { pub fn exists(&self) -> Result<bool, std::io::Error> { match self { URI::Local(loc) => loc.try_exists(), - URI::Cue {location, ..} => location.try_exists(), + URI::Cue { location, .. } => location.try_exists(), URI::Remote(_, _loc) => todo!(), } } @@ -703,7 +639,7 @@ pub struct MusicLibrary { pub uuid: Uuid, pub library: Vec<Song>, pub playlists: PlaylistFolder, - pub backup_songs: Vec<Song> // maybe move this to the config instead? + pub backup_songs: Vec<Song>, // maybe move this to the config instead? } impl MusicLibrary { @@ -823,7 +759,8 @@ impl MusicLibrary { fn query_path(&self, path: PathBuf) -> Option<Vec<&Song>> { let result: Arc<Mutex<Vec<&Song>>> = Arc::new(Mutex::new(Vec::new())); self.library.par_iter().for_each(|track| { - if path == track.primary_uri().unwrap().0.path() { //TODO: make this also not unwrap + if path == track.primary_uri().unwrap().0.path() { + //TODO: make this also not unwrap result.clone().lock().unwrap().push(track); } }); @@ -835,10 +772,7 @@ impl MusicLibrary { } /// Finds all the audio files within a specified folder - pub fn scan_folder( - &mut self, - target_path: &str, - ) -> Result<i32, Box<dyn std::error::Error>> { + pub fn scan_folder(&mut self, target_path: &str) -> Result<i32, Box<dyn std::error::Error>> { let mut total = 0; let mut errors = 0; for target_file in WalkDir::new(target_path) @@ -901,7 +835,6 @@ impl MusicLibrary { } pub fn add_file(&mut self, target_file: &Path) -> Result<(), Box<dyn Error>> { - let new_song = Song::from_file(target_file)?; match self.add_song(new_song) { Ok(_) => (), @@ -917,14 +850,13 @@ impl MusicLibrary { let tracks = Song::from_cue(cuesheet)?; let mut tracks_added = tracks.len() as i32; - for (new_song, location) in tracks { // Try to remove the original audio file from the db if it exists if self.remove_uri(&URI::Local(location.clone())).is_ok() { tracks_added -= 1 } match self.add_song(new_song) { - Ok(_) => {}, + Ok(_) => {} Err(_error) => { //println!("{}", _error); continue; @@ -1194,7 +1126,12 @@ impl MusicLibrary { #[cfg(test)] mod test { - use std::{path::{Path, PathBuf}, sync::{Arc, RwLock}, thread::sleep, time::{Duration, Instant}}; + use std::{ + path::{Path, PathBuf}, + sync::{Arc, RwLock}, + thread::sleep, + time::{Duration, Instant}, + }; use tempfile::TempDir; @@ -1202,7 +1139,6 @@ mod test { use super::Song; - #[test] fn get_art_test() { let s = Song::from_file(Path::new("")).unwrap(); @@ -1211,7 +1147,7 @@ mod test { let now = Instant::now(); _ = s.open_album_art(0, dir).inspect_err(|e| println!("{e:?}")); _ = s.open_album_art(1, dir).inspect_err(|e| println!("{e:?}")); - println!("{}ms", now.elapsed().as_millis() ); + println!("{}ms", now.elapsed().as_millis()); sleep(Duration::from_secs(20)); } @@ -1223,4 +1159,4 @@ mod test { let a = MusicLibrary::init(Arc::new(RwLock::from(config)), target_uuid).unwrap(); dbg!(a); } -} \ No newline at end of file +} From c8c00a765bf1645545a08982443ace4c1ee4c142 Mon Sep 17 00:00:00 2001 From: MrDulfin <mrdulfin@mrdulfin.com> Date: Sun, 19 May 2024 22:42:47 -0400 Subject: [PATCH 119/136] removed a broken test function --- src/music_storage/library.rs | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/music_storage/library.rs b/src/music_storage/library.rs index 4818324..6fc3b36 100644 --- a/src/music_storage/library.rs +++ b/src/music_storage/library.rs @@ -1139,19 +1139,6 @@ mod test { use super::Song; - #[test] - fn get_art_test() { - let s = Song::from_file(Path::new("")).unwrap(); - let dir = &TempDir::new().unwrap(); - - let now = Instant::now(); - _ = s.open_album_art(0, dir).inspect_err(|e| println!("{e:?}")); - _ = s.open_album_art(1, dir).inspect_err(|e| println!("{e:?}")); - println!("{}ms", now.elapsed().as_millis()); - - sleep(Duration::from_secs(20)); - } - #[test] fn library_init() { let config = Config::read_file(PathBuf::from("test_config/config_test.json")).unwrap(); From f79bf8d477caf940927b512fe849d4acedbb0f02 Mon Sep 17 00:00:00 2001 From: MrDulfin <mrdulfin@mrdulfin.com> Date: Mon, 20 May 2024 00:00:06 -0400 Subject: [PATCH 120/136] cleaned some warnings and updated player trait --- src/music_controller/controller.rs | 38 +++++++----------------------- src/music_player/player.rs | 7 +++--- src/music_storage/library.rs | 21 ++++------------- 3 files changed, 16 insertions(+), 50 deletions(-) diff --git a/src/music_controller/controller.rs b/src/music_controller/controller.rs index 2ab8811..f84545c 100644 --- a/src/music_controller/controller.rs +++ b/src/music_controller/controller.rs @@ -4,29 +4,24 @@ use crossbeam_channel; use crossbeam_channel::{Receiver, Sender}; -use listenbrainz::ListenBrainz; use std::path::PathBuf; use std::sync::{Arc, RwLock}; -use std::thread::spawn; use crossbeam_channel::unbounded; use std::error::Error; use uuid::Uuid; -use crate::music_controller::queue::QueueItem; -use crate::music_player::gstreamer::GStreamer; -use crate::music_storage::library::{Tag, URI}; +use crate::music_player::player::Player; +use crate::music_storage::library::URI; use crate::{ - config::config::Config, - music_controller::queue::Queue, - music_storage::library::{MusicLibrary, Song}, + config::config::Config, music_controller::queue::Queue, music_storage::library::MusicLibrary, }; -pub struct Controller { +pub struct Controller<P: Player> { pub queue: Queue, pub config: Arc<RwLock<Config>>, pub library: MusicLibrary, - player_mail: MailMan<PlayerCmd, PlayerRes>, + pub player: P, } #[derive(Debug)] @@ -69,10 +64,10 @@ enum PlayerRes { } #[allow(unused_variables)] -impl Controller { - pub fn start<P>(config_path: P) -> Result<Self, Box<dyn Error>> +impl<P> Controller<P> { + pub fn start<T>(config_path: T) -> Result<Self, Box<dyn Error>> where - std::path::PathBuf: std::convert::From<P>, + std::path::PathBuf: std::convert::From<T>, { let config_path = PathBuf::from(config_path); @@ -84,26 +79,11 @@ impl Controller { let (player_mail, in_thread) = MailMan::<PlayerCmd, PlayerRes>::double(); - spawn(move || { - let mut player = GStreamer::new().unwrap(); - - while true { - match in_thread.recv().unwrap() { - PlayerCmd::Test(uri) => { - &player.set_volume(0.04); - _ = &player.enqueue_next(&uri).unwrap(); - _ = &player.play(); - in_thread.send(PlayerRes::Test).unwrap(); - } - } - } - }); - Ok(Controller { queue: Queue::new(), config: config_.clone(), library, - player_mail, + player: P::new(), }) } diff --git a/src/music_player/player.rs b/src/music_player/player.rs index a30b2a6..ddf79c4 100644 --- a/src/music_player/player.rs +++ b/src/music_player/player.rs @@ -1,10 +1,9 @@ use chrono::Duration; -use thiserror::Error; use gstreamer as gst; +use thiserror::Error; use crate::music_storage::library::URI; - #[derive(Error, Debug)] pub enum PlayerError { #[error("player initialization failed")] @@ -24,6 +23,7 @@ pub enum PlayerError { } pub trait Player { + fn new() -> Self; fn source(&self) -> &Option<URI>; fn enqueue_next(&mut self, next_track: &URI) -> Result<(), PlayerError>; @@ -53,5 +53,4 @@ pub trait Player { fn seek_by(&mut self, seek_amount: Duration) -> Result<(), PlayerError>; fn seek_to(&mut self, target_pos: Duration) -> Result<(), PlayerError>; - -} \ No newline at end of file +} diff --git a/src/music_storage/library.rs b/src/music_storage/library.rs index 6fc3b36..344dd3a 100644 --- a/src/music_storage/library.rs +++ b/src/music_storage/library.rs @@ -6,7 +6,6 @@ use crate::config::config::Config; // Various std things use std::collections::BTreeMap; use std::error::Error; -use std::io::Write; use std::ops::ControlFlow::{Break, Continue}; // Files @@ -14,9 +13,8 @@ use file_format::{FileFormat, Kind}; use glib::filename_to_uri; use lofty::{AudioFile, ItemKey, ItemValue, ParseOptions, Probe, TagType, TaggedFileExt}; use rcue::parser::parse_from_file; -use std::fs::{self, File}; +use std::fs; use std::path::{Path, PathBuf}; -use tempfile::TempDir; use uuid::Uuid; use walkdir::WalkDir; @@ -144,22 +142,17 @@ pub enum DoNotTrack { Discord, } -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)] #[non_exhaustive] pub enum SongType { // TODO: add MORE?! song types + #[default] Main, Instrumental, Remix, Custom(String), } -impl Default for SongType { - fn default() -> Self { - SongType::Main - } -} - /// Stores information about a single song #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] pub struct Song { @@ -1127,18 +1120,12 @@ impl MusicLibrary { #[cfg(test)] mod test { use std::{ - path::{Path, PathBuf}, + path::PathBuf, sync::{Arc, RwLock}, - thread::sleep, - time::{Duration, Instant}, }; - use tempfile::TempDir; - use crate::{config::config::Config, music_storage::library::MusicLibrary}; - use super::Song; - #[test] fn library_init() { let config = Config::read_file(PathBuf::from("test_config/config_test.json")).unwrap(); From 0fa97c27d00b147d63c3e5561ba7713e3bfcbce2 Mon Sep 17 00:00:00 2001 From: MrDulfin <mrdulfin@mrdulfin.com> Date: Mon, 20 May 2024 00:23:45 -0400 Subject: [PATCH 121/136] Cleaned up many warnings --- src/config/config.rs | 63 ++++++++++++++++++++---------- src/music_controller/controller.rs | 45 +++------------------ src/music_controller/queue.rs | 21 ++-------- src/music_storage/library.rs | 2 +- src/music_storage/playlist.rs | 17 +++----- 5 files changed, 58 insertions(+), 90 deletions(-) diff --git a/src/config/config.rs b/src/config/config.rs index 1c19b15..9b8ede8 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -1,7 +1,7 @@ use std::{ + fs::{self, File, OpenOptions}, + io::{Error, Read, Write}, path::PathBuf, - fs::{File, OpenOptions, self}, - io::{Error, Write, Read}, }; use serde::{Deserialize, Serialize}; @@ -41,7 +41,7 @@ impl ConfigLibrary { pub fn open(&self) -> Result<File, Error> { match File::open(self.path.as_path()) { Ok(ok) => Ok(ok), - Err(e) => Err(e) + Err(e) => Err(e), } } } @@ -62,18 +62,17 @@ impl ConfigLibraries { pub fn get_default(&self) -> Result<&ConfigLibrary, ConfigError> { for library in &self.libraries { if library.uuid == self.default_library { - return Ok(library) + return Ok(library); } } Err(ConfigError::NoDefaultLibrary) } pub fn get_library(&self, uuid: &Uuid) -> Result<ConfigLibrary, ConfigError> { - for library in &self.libraries { // dbg!(&library.uuid, &uuid); if &library.uuid == uuid { - return Ok(library.to_owned()) + return Ok(library.to_owned()); } } Err(ConfigError::NoConfigLibrary(*uuid)) @@ -82,7 +81,7 @@ impl ConfigLibraries { pub fn uuid_exists(&self, uuid: &Uuid) -> bool { for library in &self.libraries { if &library.uuid == uuid { - return true + return true; } } false @@ -91,7 +90,7 @@ impl ConfigLibraries { #[derive(Debug, Default, Serialize, Deserialize, Clone)] pub struct ConfigConnections { - pub listenbrainz_token: Option<String> + pub listenbrainz_token: Option<String>, } #[derive(Debug, Default, Serialize, Deserialize, Clone)] @@ -122,7 +121,12 @@ impl Config { pub fn write_file(&self) -> Result<(), Error> { let mut writer = self.path.clone(); writer.set_extension("tmp"); - let mut file = OpenOptions::new().create(true).truncate(true).read(true).write(true).open(&writer)?; + let mut file = OpenOptions::new() + .create(true) + .truncate(true) + .read(true) + .write(true) + .open(&writer)?; let config = to_string_pretty(self)?; // dbg!(&config); @@ -136,15 +140,20 @@ impl Config { Some(path) => { let mut writer = path.clone(); writer.set_extension("tmp"); - let mut file = OpenOptions::new().create(true).truncate(true).read(true).write(true).open(&writer)?; + let mut file = OpenOptions::new() + .create(true) + .truncate(true) + .read(true) + .write(true) + .open(&writer)?; let config = to_string_pretty(self)?; // dbg!(&config); file.write_all(config.as_bytes())?; fs::rename(writer, self.path.as_path())?; Ok(()) - }, - None => Err(ConfigError::NoBackupLibrary.into()) + } + None => Err(ConfigError::NoBackupLibrary.into()), } } @@ -175,17 +184,23 @@ pub enum ConfigError { BadPlaylist, #[error("No backup Config folder present")] NoBackupLibrary, - } #[cfg(test)] pub mod tests { - use std::{path::PathBuf, sync::{Arc, RwLock}}; + use super::{Config, ConfigLibrary}; use crate::music_storage::library::MusicLibrary; - use super::{Config, ConfigLibraries, ConfigLibrary}; + use std::{ + path::PathBuf, + sync::{Arc, RwLock}, + }; pub fn new_config_lib() -> (Config, MusicLibrary) { - let lib = ConfigLibrary::new(PathBuf::from("test-config/library"), String::from("library"), None); + let lib = ConfigLibrary::new( + PathBuf::from("test-config/library"), + String::from("library"), + None, + ); let mut config = Config { path: PathBuf::from("test-config/config_test.json"), ..Default::default() @@ -194,7 +209,11 @@ pub mod tests { config.push_library(lib); config.write_file().unwrap(); - let mut lib = MusicLibrary::init(Arc::new(RwLock::from(config.clone())), dbg!(config.libraries.default_library)).unwrap(); + let mut lib = MusicLibrary::init( + Arc::new(RwLock::from(config.clone())), + dbg!(config.libraries.default_library), + ) + .unwrap(); lib.scan_folder("test-config/music/").unwrap(); lib.save(config.clone()).unwrap(); @@ -206,20 +225,22 @@ pub mod tests { // dbg!(&config); - let mut lib = MusicLibrary::init(Arc::new(RwLock::from(config.clone())), config.libraries.get_default().unwrap().uuid).unwrap(); - + let mut lib = MusicLibrary::init( + Arc::new(RwLock::from(config.clone())), + config.libraries.get_default().unwrap().uuid, + ) + .unwrap(); lib.scan_folder("test-config/music/").unwrap(); lib.save(config.clone()).unwrap(); - (config, lib) } #[test] fn test3() { - let (config, lib) = read_config_lib(); + let (config, _) = read_config_lib(); _ = config.write_file(); diff --git a/src/music_controller/controller.rs b/src/music_controller/controller.rs index f84545c..5e0ea55 100644 --- a/src/music_controller/controller.rs +++ b/src/music_controller/controller.rs @@ -12,7 +12,6 @@ use std::error::Error; use uuid::Uuid; use crate::music_player::player::Player; -use crate::music_storage::library::URI; use crate::{ config::config::Config, music_controller::queue::Queue, music_storage::library::MusicLibrary, }; @@ -21,7 +20,7 @@ pub struct Controller<P: Player> { pub queue: Queue, pub config: Arc<RwLock<Config>>, pub library: MusicLibrary, - pub player: P, + pub player: Box<P>, } #[derive(Debug)] @@ -55,19 +54,12 @@ impl<T: Send, U: Send> MailMan<T, U> { } } -enum PlayerCmd { - Test(URI), -} - -enum PlayerRes { - Test, -} - #[allow(unused_variables)] -impl<P> Controller<P> { +impl<P: Player> Controller<P> { pub fn start<T>(config_path: T) -> Result<Self, Box<dyn Error>> where std::path::PathBuf: std::convert::From<T>, + P: Player, { let config_path = PathBuf::from(config_path); @@ -77,13 +69,11 @@ impl<P> Controller<P> { let config_ = Arc::new(RwLock::from(config)); let library = MusicLibrary::init(config_.clone(), uuid)?; - let (player_mail, in_thread) = MailMan::<PlayerCmd, PlayerRes>::double(); - Ok(Controller { - queue: Queue::new(), + queue: Queue::default(), config: config_.clone(), library, - player: P::new(), + player: Box::new(P::new()), }) } @@ -94,27 +84,4 @@ impl<P> Controller<P> { } #[cfg(test)] -mod test_super { - use std::{thread::sleep, time::Duration}; - - use super::Controller; - - #[test] - fn play_test() { - let mut a = match Controller::start("test-config/config_test.json".to_string()) { - Ok(c) => c, - Err(e) => panic!("{e}"), - }; - sleep(Duration::from_millis(500)); - } - - #[test] - fn test_() { - let c = Controller::start( - "F:\\Dangoware\\Dango Music Player\\dmp-core\\test-config\\config_test.json", - ) - .unwrap(); - - sleep(Duration::from_secs(60)); - } -} +mod test_super {} diff --git a/src/music_controller/queue.rs b/src/music_controller/queue.rs index 380d157..6c533c3 100644 --- a/src/music_controller/queue.rs +++ b/src/music_controller/queue.rs @@ -1,8 +1,5 @@ -use crate::music_storage::library::{MusicLibrary, Song, URI}; -use std::{ - error::Error, - sync::{Arc, RwLock}, -}; +use crate::music_storage::library::Song; +use std::error::Error; use uuid::Uuid; use thiserror::Error; @@ -53,7 +50,7 @@ impl From<Song> for QueueItem { } } -#[derive(Debug)] +#[derive(Debug, Default)] pub struct Queue { pub items: Vec<QueueItem>, pub played: Vec<QueueItem>, @@ -81,16 +78,6 @@ impl Queue { ); } - pub fn new() -> Self { - //TODO: Make the queue take settings from config/state if applicable - Queue { - items: Vec::new(), - played: Vec::new(), - loop_: false, - shuffle: false, - } - } - pub fn set_items(&mut self, tracks: Vec<QueueItem>) { let mut tracks = tracks; self.items.clear(); @@ -131,7 +118,7 @@ impl Queue { let empty = self.items.is_empty(); self.items.insert( - (if empty { 0 } else { 1 }), + if empty { 0 } else { 1 }, QueueItem { item, state: if (self.items.get(1).is_none() diff --git a/src/music_storage/library.rs b/src/music_storage/library.rs index 344dd3a..e3e660e 100644 --- a/src/music_storage/library.rs +++ b/src/music_storage/library.rs @@ -642,7 +642,7 @@ impl MusicLibrary { name, uuid, library: Vec::new(), - playlists: PlaylistFolder::new(), + playlists: PlaylistFolder::default(), backup_songs: Vec::new(), } } diff --git a/src/music_storage/playlist.rs b/src/music_storage/playlist.rs index e7ae61b..788e59b 100644 --- a/src/music_storage/playlist.rs +++ b/src/music_storage/playlist.rs @@ -1,3 +1,4 @@ +use std::default; use std::error::Error; use std::{ fs::File, @@ -26,6 +27,7 @@ pub enum SortOrder { nest! { #[derive(Debug, Clone, Deserialize, Serialize)]* + #[derive(Default)] pub struct PlaylistFolder { name: String, items: Vec< @@ -37,15 +39,6 @@ nest! { } } -impl PlaylistFolder { - pub fn new() -> Self { - PlaylistFolder { - name: String::new(), - items: Vec::new(), - } - } -} - #[derive(Debug, Clone, Deserialize, Serialize)] pub struct Playlist { uuid: Uuid, @@ -233,7 +226,7 @@ impl Playlist { #[cfg(target_family = "windows")] { playlist.title = path - .split("\\") + .split('\\') .last() .unwrap_or_default() .strip_suffix(".m3u8") @@ -339,7 +332,7 @@ impl Default for Playlist { #[cfg(test)] mod test_super { use super::*; - use crate::{config::config::tests::read_config_lib, music_storage::playlist}; + use crate::config::config::tests::read_config_lib; #[test] fn list_to_m3u8() { @@ -360,7 +353,7 @@ mod test_super { let playlist = Playlist::from_m3u8(".\\test-config\\playlists\\playlist.m3u8", arc).unwrap(); - playlist.to_file(".\\test-config\\playlists\\playlist"); + _ = playlist.to_file(".\\test-config\\playlists\\playlist"); dbg!(playlist) } From 9fac9ef777b3317ef2bd4b72bbe59c8b751de4a7 Mon Sep 17 00:00:00 2001 From: MrDulfin <mrdulfin@mrdulfin.com> Date: Mon, 20 May 2024 00:39:51 -0400 Subject: [PATCH 122/136] updated Playlist functions --- src/music_storage/playlist.rs | 53 ++++++++++++----------------------- 1 file changed, 18 insertions(+), 35 deletions(-) diff --git a/src/music_storage/playlist.rs b/src/music_storage/playlist.rs index 788e59b..1f04860 100644 --- a/src/music_storage/playlist.rs +++ b/src/music_storage/playlist.rs @@ -1,4 +1,3 @@ -use std::default; use std::error::Error; use std::{ fs::File, @@ -60,66 +59,50 @@ impl Playlist { self.play_time } - fn title(&self) -> &String { + pub fn title(&self) -> &String { &self.title } - fn cover(&self) -> Option<&AlbumArt> { + pub fn cover(&self) -> Option<&AlbumArt> { match &self.cover { Some(e) => Some(e), None => None, } } - fn tracks(&self) -> Vec<Uuid> { + pub fn tracks(&self) -> Vec<Uuid> { self.tracks.to_owned() } + pub fn set_tracks(&mut self, tracks: Vec<Uuid>) { self.tracks = tracks; } + pub fn add_track(&mut self, track: Uuid) { self.tracks.push(track); } + pub fn remove_track(&mut self, index: i32) { let index = index as usize; if (self.tracks.len() - 1) >= index { self.tracks.remove(index); } } - // pub fn get_index(&self, song_name: &str) -> Option<usize> { - // let mut index = 0; - // if self.contains_value(&Tag::Title, song_name) { - // for track in &self.tracks { - // index += 1; - // if song_name == track.tags.get_key_value(&Tag::Title).unwrap().1 { - // dbg!("Index gotted! ", index); - // return Some(index); - // } - // } - // } - // None - // } - pub fn contains_value( - &self, - tag: &Tag, - value: &String, - lib: Arc<RwLock<MusicLibrary>>, - ) -> bool { - let lib = lib.read().unwrap(); - let items = match lib.query_tracks(value, &vec![tag.to_owned()], &vec![tag.to_owned()]) { - Some(e) => e, - None => return false, - }; - - for item in items { - for uuid in &self.tracks { - if uuid == &item.uuid { - return true; + pub fn get_index(&self, uuid: Uuid) -> Option<usize> { + let mut i = 0; + if self.contains(uuid) { + for track in &self.tracks { + i += 1; + if &uuid == track { + dbg!("Index gotted! ", i); + return Some(i); } } } - - false + None + } + pub fn contains(&self, uuid: Uuid) -> bool { + self.get_index(uuid).is_some() } pub fn to_file(&self, path: &str) -> Result<(), Box<dyn Error>> { From dd7f447d460bb6c15ce498857dc0b1a299b6d8ab Mon Sep 17 00:00:00 2001 From: G2-Games <ke0bhogsg@gmail.com> Date: Mon, 20 May 2024 02:32:37 -0500 Subject: [PATCH 123/136] Implemented for the GStreamer backend --- src/music_controller/controller.rs | 2 +- src/music_player/gstreamer.rs | 478 +++++++++++++++-------------- src/music_player/player.rs | 39 ++- 3 files changed, 279 insertions(+), 240 deletions(-) diff --git a/src/music_controller/controller.rs b/src/music_controller/controller.rs index 5e0ea55..df4fa20 100644 --- a/src/music_controller/controller.rs +++ b/src/music_controller/controller.rs @@ -73,7 +73,7 @@ impl<P: Player> Controller<P> { queue: Queue::default(), config: config_.clone(), library, - player: Box::new(P::new()), + player: Box::new(P::new()?), }) } diff --git a/src/music_player/gstreamer.rs b/src/music_player/gstreamer.rs index 91abfde..ff5d25c 100644 --- a/src/music_player/gstreamer.rs +++ b/src/music_player/gstreamer.rs @@ -1,7 +1,7 @@ // Crate things //use crate::music_controller::config::Config; use crate::music_storage::library::URI; -use crossbeam_channel::unbounded; +use crossbeam_channel::{unbounded, Receiver, Sender}; use std::error::Error; use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}; @@ -13,29 +13,10 @@ use gstreamer::prelude::*; // Extra things use chrono::Duration; -use thiserror::Error; -use super::player::{Player, PlayerError}; +use super::player::{Player, PlayerCommand, PlayerError, PlayerState}; -#[derive(Debug)] -pub enum GstCmd { - Play, - Pause, - Eos, - AboutToFinish, -} - -#[derive(Debug, PartialEq, Eq)] -pub enum GstState { - Playing, - Paused, - Ready, - Buffering(u8), - Null, - VoidPending, -} - -impl From<gst::State> for GstState { +impl From<gst::State> for PlayerState { fn from(value: gst::State) -> Self { match value { gst::State::VoidPending => Self::VoidPending, @@ -47,7 +28,7 @@ impl From<gst::State> for GstState { } } -impl TryInto<gst::State> for GstState { +impl TryInto<gst::State> for PlayerState { fn try_into(self) -> Result<gst::State, Box<dyn Error>> { match self { Self::VoidPending => Ok(gst::State::VoidPending), @@ -63,7 +44,7 @@ impl TryInto<gst::State> for GstState { } #[derive(Debug, PartialEq, Eq)] -enum PlaybackStats { +enum PlaybackInfo { Idle, Switching, Playing{ @@ -77,10 +58,10 @@ enum PlaybackStats { #[derive(Debug)] pub struct GStreamer { source: Option<URI>, - //pub message_tx: Sender<PlayerCmd>, - pub message_rx: crossbeam::channel::Receiver<GstCmd>, - playback_tx: crossbeam::channel::Sender<PlaybackStats>, + message_rx: crossbeam::channel::Receiver<PlayerCommand>, + playback_tx: crossbeam::channel::Sender<PlaybackInfo>, + playbin: Arc<RwLock<Element>>, volume: f64, start: Option<Duration>, @@ -89,16 +70,163 @@ pub struct GStreamer { position: Arc<RwLock<Option<Duration>>>, } +impl From<gst::StateChangeError> for PlayerError { + fn from(value: gst::StateChangeError) -> Self { + PlayerError::StateChange(value.to_string()) + } +} + +impl From<glib::BoolError> for PlayerError { + fn from(value: glib::BoolError) -> Self { + PlayerError::General(value.to_string()) + } +} + impl GStreamer { - pub fn new() -> Result<Self, PlayerError> { + /// Set the playback URI + fn set_source(&mut self, source: &URI) -> Result<(), PlayerError> { + if !source.exists().is_ok_and(|x| x) { + // If the source doesn't exist, gstreamer will crash! + return Err(PlayerError::NotFound) + } + + // Make sure the playback tracker knows the stuff is stopped + self.playback_tx.send(PlaybackInfo::Switching).unwrap(); + + let uri = self.playbin.read().unwrap().property_value("current-uri"); + self.source = Some(source.clone()); + match source { + URI::Cue { start, end, .. } => { + self.playbin + .write() + .unwrap() + .set_property("uri", source.as_uri()); + + // Set the start and end positions of the CUE file + self.start = Some(Duration::from_std(*start).unwrap()); + self.end = Some(Duration::from_std(*end).unwrap()); + + // Send the updated position to the tracker + self.playback_tx.send(PlaybackInfo::Playing{ + start: self.start.unwrap(), + end: self.end.unwrap() + }).unwrap(); + + // Wait for it to be ready, and then move to the proper position + self.play().unwrap(); + let now = std::time::Instant::now(); + while now.elapsed() < std::time::Duration::from_millis(20) { + if self.seek_to(Duration::from_std(*start).unwrap()).is_ok() { + return Ok(()); + } + std::thread::sleep(std::time::Duration::from_millis(1)); + } + //panic!("Couldn't seek to beginning of cue track in reasonable time (>20ms)"); + return Err(PlayerError::StateChange("Could not seek to beginning of CUE track".into())) + } + _ => { + self.playbin + .write() + .unwrap() + .set_property("uri", source.as_uri()); + + self.play().unwrap(); + + while uri.get::<&str>().unwrap_or("") + == self.property("current-uri").get::<&str>().unwrap_or("") + || self.position().is_none() + { + std::thread::sleep(std::time::Duration::from_millis(10)); + } + + self.start = Some(Duration::seconds(0)); + self.end = self.raw_duration(); + + // Send the updated position to the tracker + self.playback_tx.send(PlaybackInfo::Playing{ + start: self.start.unwrap(), + end: self.end.unwrap() + }).unwrap(); + } + } + + Ok(()) + } + + /// Gets a mutable reference to the playbin element + fn playbin_mut( + &mut self, + ) -> Result<RwLockWriteGuard<gst::Element>, std::sync::PoisonError<RwLockWriteGuard<'_, Element>>> + { + let element = match self.playbin.write() { + Ok(element) => element, + Err(err) => return Err(err), + }; + Ok(element) + } + + /// Gets a read-only reference to the playbin element + fn playbin( + &self, + ) -> Result<RwLockReadGuard<gst::Element>, std::sync::PoisonError<RwLockReadGuard<'_, Element>>> + { + let element = match self.playbin.read() { + Ok(element) => element, + Err(err) => return Err(err), + }; + Ok(element) + } + + /// Set volume of the internal playbin player, can be + /// used to bypass the main volume control for seeking + fn set_gstreamer_volume(&mut self, volume: f64) { + self.playbin_mut().unwrap().set_property("volume", volume) + } + + fn set_state(&mut self, state: gst::State) -> Result<(), gst::StateChangeError> { + self.playbin_mut().unwrap().set_state(state)?; + + Ok(()) + } + + fn raw_duration(&self) -> Option<Duration> { + self.playbin() + .unwrap() + .query_duration::<ClockTime>() + .map(|pos| Duration::nanoseconds(pos.nseconds() as i64)) + } + + /// Get the current state of the playback + fn state(&mut self) -> PlayerState { + self.playbin().unwrap().current_state().into() + /* + match *self.buffer.read().unwrap() { + None => self.playbin().unwrap().current_state().into(), + Some(value) => PlayerState::Buffering(value), + } + */ + } + + fn property(&self, property: &str) -> glib::Value { + self.playbin().unwrap().property_value(property) + } +} + +impl Player for GStreamer { + fn new() -> Result<Self, PlayerError> { // Initialize GStreamer, maybe figure out how to nicely fail here - gst::init()?; + if let Err(err) = gst::init() { + return Err(PlayerError::Init(err.to_string())) + }; let ctx = glib::MainContext::default(); let _guard = ctx.acquire(); let mainloop = glib::MainLoop::new(Some(&ctx), false); let playbin_arc = Arc::new(RwLock::new( - gst::ElementFactory::make("playbin3").build()?, + match gst::ElementFactory::make("playbin3").build() { + Ok(playbin) => playbin, + Err(error) => return Err(PlayerError::Init(error.to_string())), + } )); let playbin = playbin_arc.clone(); @@ -124,53 +252,11 @@ impl GStreamer { // Set up the thread to monitor the position let (playback_tx, playback_rx) = unbounded(); - let (stat_tx, stat_rx) = unbounded::<PlaybackStats>(); + let (status_tx, status_rx) = unbounded::<PlaybackInfo>(); let position_update = Arc::clone(&position); - let _playback_monitor = std::thread::spawn(move || { //TODO: Figure out how to return errors nicely in threads - let mut stats = PlaybackStats::Idle; - let mut pos_temp; - loop { - // Check for new messages or updates about how to proceed - if let Ok(res) = stat_rx.recv_timeout(std::time::Duration::from_millis(100)) { - stats = res - } - pos_temp = playbin_arc - .read() - .unwrap() - .query_position::<ClockTime>() - .map(|pos| Duration::nanoseconds(pos.nseconds() as i64)); - - match stats { - PlaybackStats::Playing{start, end} if pos_temp.is_some() => { - // Check if the current playback position is close to the end - let finish_point = end - Duration::milliseconds(250); - if pos_temp.unwrap() >= end { - let _ = playback_tx.try_send(GstCmd::Eos); - playbin_arc - .write() - .unwrap() - .set_state(gst::State::Ready) - .expect("Unable to set the pipeline state"); - } else if pos_temp.unwrap() >= finish_point { - let _ = playback_tx.try_send(GstCmd::AboutToFinish); - } - - // This has to be done AFTER the current time in the file - // is calculated, or everything else is wrong - pos_temp = Some(pos_temp.unwrap() - start) - }, - PlaybackStats::Finished => { - *position_update.write().unwrap() = None; - break - }, - PlaybackStats::Idle | PlaybackStats::Switching => {}, - _ => () - } - - *position_update.write().unwrap() = pos_temp; - } - }); + let _playback_monitor = + std::thread::spawn(|| playback_monitor(playbin_arc, status_rx, playback_tx, position_update)); // Set up the thread to monitor bus messages let playbin_bus_ctrl = Arc::clone(&playbin); @@ -231,7 +317,7 @@ impl GStreamer { source, playbin, message_rx: playback_rx, - playback_tx: stat_tx, + playback_tx: status_tx, volume: 1.0, start: None, end: None, @@ -240,163 +326,65 @@ impl GStreamer { }) } - pub fn source(&self) -> &Option<URI> { + fn source(&self) -> &Option<URI> { &self.source } - pub fn enqueue_next(&mut self, next_track: &URI) -> Result<(), PlayerError> { + /// Insert a new track to be played. This method should be called at the + /// beginning to start playback of something, and once the [PlayerCommand] + /// indicates the track is about to finish to enqueue gaplessly. + fn enqueue_next(&mut self, next_track: &URI) -> Result<(), PlayerError> { self.set_source(next_track) } - /// Set the playback URI - fn set_source(&mut self, source: &URI) -> Result<(), PlayerError> { - if !source.exists().is_ok_and(|x| x) { - // If the source doesn't exist, gstreamer will crash! - return Err(PlayerError::NotFound) - } - - // Make sure the playback tracker knows the stuff is stopped - self.playback_tx.send(PlaybackStats::Switching).unwrap(); - - let uri = self.playbin.read().unwrap().property_value("current-uri"); - self.source = Some(source.clone()); - match source { - URI::Cue { start, end, .. } => { - self.playbin - .write() - .unwrap() - .set_property("uri", source.as_uri()); - - // Set the start and end positions of the CUE file - self.start = Some(Duration::from_std(*start).unwrap()); - self.end = Some(Duration::from_std(*end).unwrap()); - - // Send the updated position to the tracker - self.playback_tx.send(PlaybackStats::Playing{ - start: self.start.unwrap(), - end: self.end.unwrap() - }).unwrap(); - - // Wait for it to be ready, and then move to the proper position - self.play().unwrap(); - let now = std::time::Instant::now(); - while now.elapsed() < std::time::Duration::from_millis(20) { - if self.seek_to(Duration::from_std(*start).unwrap()).is_ok() { - return Ok(()); - } - std::thread::sleep(std::time::Duration::from_millis(1)); - } - panic!("Couldn't seek to beginning of cue track in reasonable time (>20ms)"); - } - _ => { - self.playbin - .write() - .unwrap() - .set_property("uri", source.as_uri()); - - self.play().unwrap(); - - while uri.get::<&str>().unwrap_or("") - == self.property("current-uri").get::<&str>().unwrap_or("") - || self.position().is_none() - { - std::thread::sleep(std::time::Duration::from_millis(10)); - } - - self.start = Some(Duration::seconds(0)); - self.end = self.raw_duration(); - - // Send the updated position to the tracker - self.playback_tx.send(PlaybackStats::Playing{ - start: self.start.unwrap(), - end: self.end.unwrap() - }).unwrap(); - } - } - - Ok(()) - } - - /// Gets a mutable reference to the playbin element - fn playbin_mut( - &mut self, - ) -> Result<RwLockWriteGuard<gst::Element>, std::sync::PoisonError<RwLockWriteGuard<'_, Element>>> - { - let element = match self.playbin.write() { - Ok(element) => element, - Err(err) => return Err(err), - }; - Ok(element) - } - - /// Gets a read-only reference to the playbin element - fn playbin( - &self, - ) -> Result<RwLockReadGuard<gst::Element>, std::sync::PoisonError<RwLockReadGuard<'_, Element>>> - { - let element = match self.playbin.read() { - Ok(element) => element, - Err(err) => return Err(err), - }; - Ok(element) - } - /// Set the playback volume, accepts a float from 0 to 1 - pub fn set_volume(&mut self, volume: f64) { + fn set_volume(&mut self, volume: f64) { self.volume = volume.clamp(0.0, 1.0); self.set_gstreamer_volume(self.volume); } - /// Set volume of the internal playbin player, can be - /// used to bypass the main volume control for seeking - fn set_gstreamer_volume(&mut self, volume: f64) { - self.playbin_mut().unwrap().set_property("volume", volume) - } - /// Returns the current volume level, a float from 0 to 1 - pub fn volume(&mut self) -> f64 { + fn volume(&mut self) -> f64 { self.volume } - fn set_state(&mut self, state: gst::State) -> Result<(), gst::StateChangeError> { - self.playbin_mut().unwrap().set_state(state)?; - + fn ready(&mut self) -> Result<(), PlayerError> { + self.set_state(gst::State::Ready)?; Ok(()) } - pub fn ready(&mut self) -> Result<(), gst::StateChangeError> { - self.set_state(gst::State::Ready) - } - /// If the player is paused or stopped, starts playback - pub fn play(&mut self) -> Result<(), gst::StateChangeError> { - self.set_state(gst::State::Playing) + fn play(&mut self) -> Result<(), PlayerError> { + self.set_state(gst::State::Playing)?; + Ok(()) } /// Pause, if playing - pub fn pause(&mut self) -> Result<(), gst::StateChangeError> { - //*self.paused.write().unwrap() = true; - self.set_state(gst::State::Paused) + fn pause(&mut self) -> Result<(), PlayerError> { + //self.paused = true; + self.set_state(gst::State::Paused)?; + Ok(()) } /// Resume from being paused - pub fn resume(&mut self) -> Result<(), gst::StateChangeError> { - //*self.paused.write().unwrap() = false; - self.set_state(gst::State::Playing) + fn resume(&mut self) -> Result<(), PlayerError> { + //self.paused = false; + self.set_state(gst::State::Playing)?; + Ok(()) } /// Check if playback is paused - pub fn is_paused(&mut self) -> bool { + fn is_paused(&mut self) -> bool { self.playbin().unwrap().current_state() == gst::State::Paused } /// Get the current playback position of the player - pub fn position(&mut self) -> Option<Duration> { + fn position(&mut self) -> Option<Duration> { *self.position.read().unwrap() } /// Get the duration of the currently playing track - pub fn duration(&mut self) -> Option<Duration> { + fn duration(&mut self) -> Option<Duration> { if self.end.is_some() && self.start.is_some() { Some(self.end.unwrap() - self.start.unwrap()) } else { @@ -404,18 +392,11 @@ impl GStreamer { } } - pub fn raw_duration(&self) -> Option<Duration> { - self.playbin() - .unwrap() - .query_duration::<ClockTime>() - .map(|pos| Duration::nanoseconds(pos.nseconds() as i64)) - } - /// Seek relative to the current position - pub fn seek_by(&mut self, seek_amount: Duration) -> Result<(), Box<dyn Error>> { + fn seek_by(&mut self, seek_amount: Duration) -> Result<(), PlayerError> { let time_pos = match *self.position.read().unwrap() { Some(pos) => pos, - None => return Err("No position".into()), + None => return Err(PlayerError::Seek("No position".into())), }; let seek_pos = time_pos + seek_amount; @@ -424,15 +405,15 @@ impl GStreamer { } /// Seek absolutely - pub fn seek_to(&mut self, target_pos: Duration) -> Result<(), Box<dyn Error>> { + fn seek_to(&mut self, target_pos: Duration) -> Result<(), PlayerError> { let start = if self.start.is_none() { - return Err("Failed to seek: No START time".into()); + return Err(PlayerError::Seek("No START time".into())); } else { self.start.unwrap() }; let end = if self.end.is_none() { - return Err("Failed to seek: No END time".into()); + return Err(PlayerError::Seek("No END time".into())); } else { self.end.unwrap() }; @@ -451,28 +432,13 @@ impl GStreamer { Ok(()) } - /// Get the current state of the playback - pub fn state(&mut self) -> GstState { - self.playbin().unwrap().current_state().into() - /* - match *self.buffer.read().unwrap() { - None => self.playbin().unwrap().current_state().into(), - Some(value) => PlayerState::Buffering(value), - } - */ - } - - pub fn property(&self, property: &str) -> glib::Value { - self.playbin().unwrap().property_value(property) - } - /// Stop the playback entirely - pub fn stop(&mut self) -> Result<(), gst::StateChangeError> { + fn stop(&mut self) -> Result<(), PlayerError> { self.pause()?; self.ready()?; // Send the updated position to the tracker - self.playback_tx.send(PlaybackStats::Idle).unwrap(); + self.playback_tx.send(PlaybackInfo::Idle).unwrap(); // Set all positions to none *self.position.write().unwrap() = None; @@ -480,9 +446,12 @@ impl GStreamer { self.end = None; Ok(()) } -} -// impl Player for GStreamer {} + /// Return a reference to the player message channel + fn message_channel(&self) -> &crossbeam::channel::Receiver<PlayerCommand> { + &self.message_rx + } +} impl Drop for GStreamer { /// Cleans up the `GStreamer` pipeline and the monitoring @@ -492,6 +461,57 @@ impl Drop for GStreamer { .unwrap() .set_state(gst::State::Null) .expect("Unable to set the pipeline to the `Null` state"); - let _ = self.playback_tx.send(PlaybackStats::Finished); + let _ = self.playback_tx.send(PlaybackInfo::Finished); + } +} + +fn playback_monitor( + playbin: Arc<RwLock<Element>>, + status_rx: Receiver<PlaybackInfo>, + playback_tx: Sender<PlayerCommand>, + position: Arc<RwLock<Option<Duration>>>, +) { + let mut stats = PlaybackInfo::Idle; + let mut pos_temp; + loop { + // Check for new messages to decide how to proceed + if let Ok(result) = status_rx.recv_timeout(std::time::Duration::from_millis(100)) { + stats = result + } + + pos_temp = playbin + .read() + .unwrap() + .query_position::<ClockTime>() + .map(|pos| Duration::nanoseconds(pos.nseconds() as i64)); + + match stats { + PlaybackInfo::Playing{start, end} if pos_temp.is_some() => { + // Check if the current playback position is close to the end + let finish_point = end - Duration::milliseconds(250); + if pos_temp.unwrap() >= end { + let _ = playback_tx.try_send(PlayerCommand::EndOfStream); + playbin + .write() + .unwrap() + .set_state(gst::State::Ready) + .expect("Unable to set the pipeline state"); + } else if pos_temp.unwrap() >= finish_point { + let _ = playback_tx.try_send(PlayerCommand::AboutToFinish); + } + + // This has to be done AFTER the current time in the file + // is calculated, or everything else is wrong + pos_temp = Some(pos_temp.unwrap() - start) + }, + PlaybackInfo::Finished => { + *position.write().unwrap() = None; + break + }, + PlaybackInfo::Idle | PlaybackInfo::Switching => {}, + _ => () + } + + *position.write().unwrap() = pos_temp; } } diff --git a/src/music_player/player.rs b/src/music_player/player.rs index ddf79c4..c2e24d9 100644 --- a/src/music_player/player.rs +++ b/src/music_player/player.rs @@ -1,17 +1,16 @@ use chrono::Duration; -use gstreamer as gst; use thiserror::Error; use crate::music_storage::library::URI; #[derive(Error, Debug)] pub enum PlayerError { - #[error("player initialization failed")] - Init(#[from] glib::Error), - #[error("element factory failed to create playbin3")] - Factory(#[from] glib::BoolError), + #[error("player initialization failed: {0}")] + Init(String), #[error("could not change playback state")] - StateChange(#[from] gst::StateChangeError), + StateChange(String), + #[error("seeking failed: {0}")] + Seek(String), #[error("the file or source is not found")] NotFound, #[error("failed to build gstreamer item")] @@ -19,11 +18,31 @@ pub enum PlayerError { #[error("poison error")] Poison, #[error("general player error")] - General, + General(String), +} + +#[derive(Debug, PartialEq, Eq)] +pub enum PlayerState { + Playing, + Paused, + Ready, + Buffering(u8), + Null, + VoidPending, +} + +#[derive(Debug)] +pub enum PlayerCommand { + Play, + Pause, + EndOfStream, + AboutToFinish, } pub trait Player { - fn new() -> Self; + /// Create a new player + fn new() -> Result<Self, PlayerError> where Self: Sized; + fn source(&self) -> &Option<URI>; fn enqueue_next(&mut self, next_track: &URI) -> Result<(), PlayerError>; @@ -48,9 +67,9 @@ pub trait Player { fn duration(&mut self) -> Option<Duration>; - fn raw_duration(&self) -> Option<Duration>; - fn seek_by(&mut self, seek_amount: Duration) -> Result<(), PlayerError>; fn seek_to(&mut self, target_pos: Duration) -> Result<(), PlayerError>; + + fn message_channel(&self) -> &crossbeam::channel::Receiver<PlayerCommand>; } From 48c7e29ecaf436a31d8cd719fed88b5a43da21c5 Mon Sep 17 00:00:00 2001 From: G2-Games <ke0bhogsg@gmail.com> Date: Mon, 20 May 2024 04:03:38 -0500 Subject: [PATCH 124/136] Fixed buffering for GStreamer --- src/music_player/gstreamer.rs | 57 ++++++++++++++++++++--------------- src/music_player/player.rs | 2 +- src/music_storage/library.rs | 8 ++--- src/music_storage/utils.rs | 1 - 4 files changed, 37 insertions(+), 31 deletions(-) diff --git a/src/music_player/gstreamer.rs b/src/music_player/gstreamer.rs index ff5d25c..db35c03 100644 --- a/src/music_player/gstreamer.rs +++ b/src/music_player/gstreamer.rs @@ -51,7 +51,10 @@ enum PlaybackInfo { start: Duration, end: Duration, }, - Finished // When this is sent, the thread will die! + + /// When this is sent, the thread will die! Use it when the [Player] is + /// done playing + Finished } /// An instance of a music player with a GStreamer backend @@ -66,7 +69,7 @@ pub struct GStreamer { volume: f64, start: Option<Duration>, end: Option<Duration>, - paused: bool, + paused: Arc<RwLock<bool>>, position: Arc<RwLock<Option<Duration>>>, } @@ -260,6 +263,8 @@ impl Player for GStreamer { // Set up the thread to monitor bus messages let playbin_bus_ctrl = Arc::clone(&playbin); + let paused = Arc::new(RwLock::new(false)); + let bus_paused = Arc::clone(&paused); let bus_watch = playbin .read() .unwrap() @@ -267,23 +272,18 @@ impl Player for GStreamer { .expect("Failed to get GStreamer message bus") .add_watch(move |_bus, msg| { match msg.view() { - gst::MessageView::Eos(_) => {} + gst::MessageView::Eos(_) => println!("End of stream"), gst::MessageView::StreamStart(_) => println!("Stream start"), - gst::MessageView::Error(_) => { - playbin_bus_ctrl - .write() - .unwrap() - .set_state(gst::State::Ready) - .unwrap(); - - playbin_bus_ctrl - .write() - .unwrap() - .set_state(gst::State::Playing) - .unwrap(); + gst::MessageView::Error(err) => { + println!("Error recieved: {}", err); + return glib::ControlFlow::Break } - /* TODO: Fix buffering!! gst::MessageView::Buffering(buffering) => { + if *bus_paused.read().unwrap() == true { + return glib::ControlFlow::Continue + } + + // If the player is not paused, pause it let percent = buffering.percent(); if percent < 100 { playbin_bus_ctrl @@ -291,7 +291,8 @@ impl Player for GStreamer { .unwrap() .set_state(gst::State::Paused) .unwrap(); - } else if !(buffering) { + } else if percent >= 100 { + println!("Finished buffering"); playbin_bus_ctrl .write() .unwrap() @@ -299,7 +300,6 @@ impl Player for GStreamer { .unwrap(); } } - */ _ => (), } glib::ControlFlow::Continue @@ -321,7 +321,7 @@ impl Player for GStreamer { volume: 1.0, start: None, end: None, - paused: false, + paused, position, }) } @@ -355,20 +355,21 @@ impl Player for GStreamer { /// If the player is paused or stopped, starts playback fn play(&mut self) -> Result<(), PlayerError> { + *self.paused.write().unwrap() = false; self.set_state(gst::State::Playing)?; Ok(()) } /// Pause, if playing fn pause(&mut self) -> Result<(), PlayerError> { - //self.paused = true; + *self.paused.write().unwrap() = true; self.set_state(gst::State::Paused)?; Ok(()) } /// Resume from being paused fn resume(&mut self) -> Result<(), PlayerError> { - //self.paused = false; + *self.paused.write().unwrap() = false; self.set_state(gst::State::Playing)?; Ok(()) } @@ -473,9 +474,10 @@ fn playback_monitor( ) { let mut stats = PlaybackInfo::Idle; let mut pos_temp; + let mut sent_atf = false; loop { // Check for new messages to decide how to proceed - if let Ok(result) = status_rx.recv_timeout(std::time::Duration::from_millis(100)) { + if let Ok(result) = status_rx.recv_timeout(std::time::Duration::from_millis(10)) { stats = result } @@ -489,15 +491,18 @@ fn playback_monitor( PlaybackInfo::Playing{start, end} if pos_temp.is_some() => { // Check if the current playback position is close to the end let finish_point = end - Duration::milliseconds(250); - if pos_temp.unwrap() >= end { + if pos_temp.unwrap().num_microseconds() >= end.num_microseconds() { let _ = playback_tx.try_send(PlayerCommand::EndOfStream); playbin .write() .unwrap() .set_state(gst::State::Ready) .expect("Unable to set the pipeline state"); - } else if pos_temp.unwrap() >= finish_point { + sent_atf = false + } else if pos_temp.unwrap().num_microseconds() >= finish_point.num_microseconds() + && !sent_atf { let _ = playback_tx.try_send(PlayerCommand::AboutToFinish); + sent_atf = true; } // This has to be done AFTER the current time in the file @@ -508,7 +513,9 @@ fn playback_monitor( *position.write().unwrap() = None; break }, - PlaybackInfo::Idle | PlaybackInfo::Switching => {}, + PlaybackInfo::Idle | PlaybackInfo::Switching => { + sent_atf = false + }, _ => () } diff --git a/src/music_player/player.rs b/src/music_player/player.rs index c2e24d9..3a9e76e 100644 --- a/src/music_player/player.rs +++ b/src/music_player/player.rs @@ -31,7 +31,7 @@ pub enum PlayerState { VoidPending, } -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] pub enum PlayerCommand { Play, Pause, diff --git a/src/music_storage/library.rs b/src/music_storage/library.rs index e3e660e..1dd63b5 100644 --- a/src/music_storage/library.rs +++ b/src/music_storage/library.rs @@ -218,8 +218,8 @@ impl Song { self.tags.remove(target_key); } - /// Creates a `Song` from a song file - pub fn from_file(target_file: &Path) -> Result<Self, Box<dyn Error>> { + /// Creates a `Song` from a music file + pub fn from_file<P: ?Sized + AsRef<Path>>(target_file: &P) -> Result<Self, Box<dyn Error>> { let normal_options = ParseOptions::new().parsing_mode(lofty::ParsingMode::Relaxed); let blank_tag = &lofty::Tag::new(TagType::Id3v2); @@ -283,7 +283,7 @@ impl Song { } // Find images around the music file that can be used - let mut found_images = find_images(target_file).unwrap(); + let mut found_images = find_images(target_file.as_ref()).unwrap(); album_art.append(&mut found_images); // Get the format as a string @@ -548,7 +548,7 @@ impl URI { match self { URI::Local(loc) => loc.try_exists(), URI::Cue { location, .. } => location.try_exists(), - URI::Remote(_, _loc) => todo!(), + URI::Remote(_, _loc) => Ok(true), // TODO: Investigate a way to do this? } } } diff --git a/src/music_storage/utils.rs b/src/music_storage/utils.rs index c9347c4..e29a6e0 100644 --- a/src/music_storage/utils.rs +++ b/src/music_storage/utils.rs @@ -84,7 +84,6 @@ pub fn find_images(song_path: &Path) -> Result<Vec<AlbumArt>, Box<dyn Error>> { .filter(|e| e.depth() < 3) // Don't recurse very deep { - println!("{:?}", target_file); let path = target_file.path(); if !path.is_file() || !path.exists() { continue; From 5b94c7950ff9670bfc0b78acdc65c0b311d324f4 Mon Sep 17 00:00:00 2001 From: MrDulfin <mrdulfin@mrdulfin.com> Date: Mon, 20 May 2024 23:51:44 -0400 Subject: [PATCH 125/136] Fixed `move_to()` function and added more queue functions. also created ControllerError --- src/config/config.rs | 1 - src/music_controller/controller.rs | 16 ++- src/music_controller/queue.rs | 206 ++++++++++++++++++++++++----- 3 files changed, 187 insertions(+), 36 deletions(-) diff --git a/src/config/config.rs b/src/config/config.rs index 9b8ede8..18ab894 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -54,7 +54,6 @@ pub struct ConfigLibraries { } impl ConfigLibraries { - //TODO: Add new function for test tube pub fn set_default(mut self, uuid: &Uuid) { self.default_library = *uuid; } diff --git a/src/music_controller/controller.rs b/src/music_controller/controller.rs index df4fa20..e3f1e68 100644 --- a/src/music_controller/controller.rs +++ b/src/music_controller/controller.rs @@ -6,16 +6,20 @@ use crossbeam_channel; use crossbeam_channel::{Receiver, Sender}; use std::path::PathBuf; use std::sync::{Arc, RwLock}; +use thiserror::Error; use crossbeam_channel::unbounded; use std::error::Error; use uuid::Uuid; -use crate::music_player::player::Player; +use crate::config::config::ConfigError; +use crate::music_player::player::{Player, PlayerError}; use crate::{ config::config::Config, music_controller::queue::Queue, music_storage::library::MusicLibrary, }; +use super::queue::QueueError; + pub struct Controller<P: Player> { pub queue: Queue, pub config: Arc<RwLock<Config>>, @@ -23,6 +27,16 @@ pub struct Controller<P: Player> { pub player: Box<P>, } +#[derive(Error, Debug)] +pub enum ControllerError { + #[error("{0:?}")] + QueueError(#[from] QueueError), + #[error("{0:?}")] + PlayerError(#[from] PlayerError), + #[error("{0:?}")] + ConfigError(#[from] ConfigError), +} + #[derive(Debug)] pub(super) struct MailMan<T: Send, U: Send> { pub tx: Sender<T>, diff --git a/src/music_controller/queue.rs b/src/music_controller/queue.rs index 6c533c3..85675ff 100644 --- a/src/music_controller/queue.rs +++ b/src/music_controller/queue.rs @@ -1,4 +1,5 @@ use crate::music_storage::library::Song; +use chrono::format::Item; use std::error::Error; use uuid::Uuid; @@ -6,10 +7,12 @@ use thiserror::Error; #[derive(Error, Debug)] pub enum QueueError { - #[error("Index out of bounds! Index {0} is over len {1}")] - OutOfBounds(usize, usize), + #[error("Index out of bounds! Index {index} is over len {len}")] + OutOfBounds { index: usize, len: usize }, #[error("The Queue is empty!")] EmptyQueue, + #[error("There are no past played songs!")] + EmptyPlayed, } #[derive(Debug, PartialEq, Clone, Copy)] @@ -21,7 +24,7 @@ pub enum QueueState { } // TODO: move this to a different location to be used elsewhere -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq)] #[non_exhaustive] pub enum PlayerLocation { Test, @@ -39,6 +42,7 @@ pub struct QueueItem { pub(super) source: PlayerLocation, pub(super) by_human: bool, } + impl From<Song> for QueueItem { fn from(song: Song) -> Self { QueueItem { @@ -58,6 +62,7 @@ pub struct Queue { pub shuffle: bool, } +// TODO: HAndle the First QueueState impl Queue { fn has_addhere(&self) -> bool { for item in &self.items { @@ -72,8 +77,8 @@ impl Queue { dbg!( self.items .iter() - .map(|item| item.item.clone()) - .collect::<Vec<Song>>(), + .map(|item| (item.item.uuid, item.state)) + .collect::<Vec<(Uuid, QueueState)>>(), self.items.len() ); } @@ -84,6 +89,7 @@ impl Queue { self.items.append(&mut tracks); } + /// Inserts an item after the AddHere item pub fn add_item(&mut self, item: Song, source: PlayerLocation, by_human: bool) { let mut i: usize = 0; @@ -113,6 +119,7 @@ impl Queue { ); } + /// Inserts an item after the currently playing item pub fn add_item_next(&mut self, item: Song, source: PlayerLocation) { use QueueState::*; let empty = self.items.is_empty(); @@ -135,9 +142,70 @@ impl Queue { ) } - pub fn add_multi(&mut self, items: Vec<Song>, source: PlayerLocation, by_human: bool) {} + pub fn add_multi(&mut self, items: Vec<Song>, source: PlayerLocation, by_human: bool) { + let mut i: usize = 0; - pub fn remove_item(&mut self, remove_index: usize) -> Result<(), QueueError> { + self.items = self + .items + .iter() + .enumerate() + .map(|(j, item_)| { + let mut item_ = item_.to_owned(); + // get the index of the current AddHere item and give it to i + if item_.state == QueueState::AddHere { + i = j; + item_.state = QueueState::NoState; + } + item_ + }) + .collect::<Vec<QueueItem>>(); + + let empty = self.items.is_empty(); + + let len = items.len(); + for item in items { + self.items.insert( + i + if empty { 0 } else { 1 }, + QueueItem { + item, + state: QueueState::NoState, + source, + by_human, + }, + ); + } + self.items[i + len - if empty { 1 } else { 0 }].state = QueueState::AddHere; + } + + /// Add multiple Songs after the currently playing Song + pub fn add_multi_next(&mut self, items: Vec<Song>, source: PlayerLocation, by_human: bool) { + use QueueState::*; + let empty = self.items.is_empty(); + + let add_here = (self.items.get(1).is_none() + || !self.has_addhere() && self.items.get(1).is_some()) + || empty; + + let len = items.len(); + + for item in items { + self.items.insert( + if empty { 0 } else { 1 }, + QueueItem { + item, + state: NoState, + source, + by_human: true, + }, + ) + } + + if add_here { + self.items[len - if empty { 1 } else { 0 }].state = QueueState::AddHere; + } + } + + pub fn remove_item(&mut self, remove_index: usize) -> Result<QueueItem, QueueError> { // dbg!(/*&remove_index, self.current_index(), &index,*/ &self.items[remove_index]); if remove_index < self.items.len() { @@ -145,19 +213,36 @@ impl Queue { if self.items.get(remove_index + 1).is_some() { self.items[remove_index + 1].state = self.items[remove_index].state; } - self.items[remove_index].state = QueueState::NoState; - self.items.remove(remove_index); - Ok(()) + Ok(self.items.remove(remove_index)) } else { Err(QueueError::EmptyQueue) } } + pub fn insert<T>(&mut self, index: usize, new_item: T, addhere: bool) + where + QueueItem: std::convert::From<T>, + { + if addhere { + let mut new_item = QueueItem::from(new_item); + for item in &mut self.items { + if item.state == QueueState::AddHere { + item.state = QueueState::NoState + } + } + new_item.state = QueueState::AddHere; + self.items.insert(index, new_item); + } else { + let new_item = QueueItem::from(new_item); + self.items.insert(index, new_item); + } + } + pub fn clear(&mut self) { self.items.clear(); } - pub fn clear_except(&mut self, index: usize) -> Result<(), Box<dyn Error>> { + pub fn clear_except(&mut self, index: usize) -> Result<(), QueueError> { use QueueState::*; let empty = self.items.is_empty(); @@ -166,9 +251,12 @@ impl Queue { self.items.retain(|item| *item == i); self.items[0].state = AddHere; } else if empty { - return Err("Queue is empty!".into()); + return Err(QueueError::EmptyQueue); } else { - return Err("index out of bounds!".into()); + return Err(QueueError::OutOfBounds { + index, + len: self.items.len(), + }); } Ok(()) } @@ -182,8 +270,7 @@ impl Queue { self.played.clear(); } - // TODO: uh, fix this? - fn move_to(&mut self, index: usize) -> Result<(), QueueError> { + pub fn move_to(&mut self, index: usize) -> Result<(), QueueError> { use QueueState::*; let empty = self.items.is_empty(); @@ -194,22 +281,20 @@ impl Queue { return Err(QueueError::EmptyQueue); }; - if !empty && index < self.items.len() { + if !empty && dbg!(index < self.items.len()) { let to_item = self.items[index].clone(); loop { - let empty = !self.items.is_empty(); + let empty = self.items.is_empty(); let item = self.items[0].item.to_owned(); if item != to_item.item && !empty { if self.items[0].state == AddHere && self.items.get(1).is_some() { self.items[1].state = AddHere; } - if let Err(e) = self.remove_item(0) { - dbg!(&e); - self.dbg_items(); - return Err(e); - } + let item = self.items.remove(0); + self.played.push(item); + // dbg!(&to_item.item, &self.items[ind].item); } else if empty { return Err(QueueError::EmptyQueue); @@ -227,35 +312,52 @@ impl Queue { self.items.swap(a, b) } - pub fn move_item(&mut self, a: usize, b: usize) { - let item = self.items[a].to_owned(); - if a != b { - self.items.remove(a); + pub fn move_item(&mut self, from: usize, to: usize) { + let item = self.items[from].to_owned(); + if from != to { + self.items.remove(from); } - self.items.insert(b, item); + self.items.insert(to, item); } #[allow(clippy::should_implement_trait)] - pub fn next(&mut self) -> Result<&QueueItem, Box<dyn Error>> { + pub fn next(&mut self) -> Result<&QueueItem, QueueError> { if self.items.is_empty() { if self.loop_ { - return Err(QueueError::EmptyQueue.into()); // TODO: add function to loop the queue + unimplemented!() // TODO: add function to loop the queue } else { - return Err(QueueError::EmptyQueue.into()); + return Err(QueueError::EmptyQueue); } } - // TODO: add an algorithm to detect if the song should be skipped - let item = self.items[0].clone(); + + let item = self.items.remove(0); if self.items[0].state == QueueState::AddHere || !self.has_addhere() { self.items[1].state = QueueState::AddHere; } self.played.push(item); - self.items.remove(0); Ok(&self.items[1]) } - pub fn prev() {} + pub fn prev(&mut self) -> Result<&QueueItem, QueueError> { + if self.items[0].state == QueueState::First && self.loop_ { + todo!() + } + if let Some(item) = self.played.pop() { + self.items.insert(0, item); + Ok(&self.items[0]) + } else { + Err(QueueError::EmptyPlayed) + } + } + + pub fn now_playing(&self) -> Result<&QueueItem, QueueError> { + if !self.items.is_empty() { + Ok(&self.items[0]) + } else { + Err(QueueError::EmptyQueue) + } + } pub fn check_played(&mut self) { while self.played.len() > 50 { @@ -263,3 +365,39 @@ impl Queue { } } } + +#[cfg(test)] +mod test_super { + #![allow(unused)] + use crate::{ + config::config::tests::{new_config_lib, read_config_lib}, + music_storage::library, + }; + + use super::*; + + #[test] + fn move_test() { + let (_, library) = read_config_lib(); + let mut q = Queue::default(); + q.add_multi(library.library.clone(), PlayerLocation::Library, true); + q.add_multi_next(library.library, PlayerLocation::Library, true); + + + q.dbg_items(); + dbg!(&q.played); + + // q.move_to(2).inspect_err(|e| println!("{e:?}")); + // q.dbg_items(); + // dbg!(&q.played.iter().map(|i| i.item.uuid).collect::<Vec<_>>()); + + // let a = q + // .prev() + // .inspect_err(|e| println!("{e:?}")) + // .unwrap() + // .item + // .uuid; + // q.dbg_items(); + // dbg!(a, &q.played.iter().map(|i| i.item.uuid).collect::<Vec<_>>()); + } +} From 32c0cf31054f16dd4ba525e2f22fb06445b3576b Mon Sep 17 00:00:00 2001 From: MrDulfin <mrdulfin@mrdulfin.com> Date: Wed, 22 May 2024 00:45:35 -0400 Subject: [PATCH 126/136] Tested and adjusted queue functions --- src/music_controller/queue.rs | 83 ++++++++++++++++++++--------------- 1 file changed, 48 insertions(+), 35 deletions(-) diff --git a/src/music_controller/queue.rs b/src/music_controller/queue.rs index 85675ff..c26ae05 100644 --- a/src/music_controller/queue.rs +++ b/src/music_controller/queue.rs @@ -1,6 +1,4 @@ use crate::music_storage::library::Song; -use chrono::format::Item; -use std::error::Error; use uuid::Uuid; use thiserror::Error; @@ -13,6 +11,8 @@ pub enum QueueError { EmptyQueue, #[error("There are no past played songs!")] EmptyPlayed, + #[error("There is no item after this in the Queue")] + NoNext, } #[derive(Debug, PartialEq, Clone, Copy)] @@ -43,12 +43,12 @@ pub struct QueueItem { pub(super) by_human: bool, } -impl From<Song> for QueueItem { - fn from(song: Song) -> Self { +impl QueueItem { + fn from_song(song: Song, source: PlayerLocation) -> Self { QueueItem { item: song, state: QueueState::NoState, - source: PlayerLocation::Library, + source, by_human: false, } } @@ -62,7 +62,7 @@ pub struct Queue { pub shuffle: bool, } -// TODO: HAndle the First QueueState +// TODO: HAndle the First QueueState[looping] and shuffle impl Queue { fn has_addhere(&self) -> bool { for item in &self.items { @@ -73,6 +73,7 @@ impl Queue { false } + #[allow(unused)] fn dbg_items(&self) { dbg!( self.items @@ -163,7 +164,7 @@ impl Queue { let empty = self.items.is_empty(); let len = items.len(); - for item in items { + for item in items.into_iter().rev() { self.items.insert( i + if empty { 0 } else { 1 }, QueueItem { @@ -178,7 +179,7 @@ impl Queue { } /// Add multiple Songs after the currently playing Song - pub fn add_multi_next(&mut self, items: Vec<Song>, source: PlayerLocation, by_human: bool) { + pub fn add_multi_next(&mut self, items: Vec<Song>, source: PlayerLocation) { use QueueState::*; let empty = self.items.is_empty(); @@ -219,12 +220,24 @@ impl Queue { } } - pub fn insert<T>(&mut self, index: usize, new_item: T, addhere: bool) - where - QueueItem: std::convert::From<T>, - { + pub fn insert( + &mut self, + index: usize, + new_item: Song, + source: PlayerLocation, + addhere: bool, + ) -> Result<(), QueueError> { + if self.items.get_mut(index).is_none() + && index > 0 + && self.items.get_mut(index - 1).is_none() + { + return Err(QueueError::OutOfBounds { + index, + len: self.items.len(), + }); + } if addhere { - let mut new_item = QueueItem::from(new_item); + let mut new_item = QueueItem::from_song(new_item, source); for item in &mut self.items { if item.state == QueueState::AddHere { item.state = QueueState::NoState @@ -233,9 +246,10 @@ impl Queue { new_item.state = QueueState::AddHere; self.items.insert(index, new_item); } else { - let new_item = QueueItem::from(new_item); + let new_item = QueueItem::from_song(new_item, source); self.items.insert(index, new_item); } + Ok(()) } pub fn clear(&mut self) { @@ -330,20 +344,27 @@ impl Queue { } } - let item = self.items.remove(0); if self.items[0].state == QueueState::AddHere || !self.has_addhere() { - self.items[1].state = QueueState::AddHere; + self.items[0].state = QueueState::NoState; + if self.items.get_mut(1).is_some() { + self.items[1].state = QueueState::AddHere; + } } + let item = self.items.remove(0); self.played.push(item); - Ok(&self.items[1]) + if self.items.is_empty() { + Err(QueueError::NoNext) + } else { + Ok(&self.items[0]) + } } pub fn prev(&mut self) -> Result<&QueueItem, QueueError> { - if self.items[0].state == QueueState::First && self.loop_ { - todo!() - } if let Some(item) = self.played.pop() { + if item.state == QueueState::First && self.loop_ { + todo!() + } self.items.insert(0, item); Ok(&self.items[0]) } else { @@ -380,24 +401,16 @@ mod test_super { fn move_test() { let (_, library) = read_config_lib(); let mut q = Queue::default(); - q.add_multi(library.library.clone(), PlayerLocation::Library, true); - q.add_multi_next(library.library, PlayerLocation::Library, true); - + q.insert(0, library.library[2].to_owned(), PlayerLocation::File, true) + .inspect_err(|e| println!("{e}")); + q.insert(1, library.library[2].to_owned(), PlayerLocation::File, true) + .inspect_err(|e| println!("{e}")); + // q.next(); + // q.clear(); q.dbg_items(); - dbg!(&q.played); + dbg!(&q.played.len()); - // q.move_to(2).inspect_err(|e| println!("{e:?}")); // q.dbg_items(); - // dbg!(&q.played.iter().map(|i| i.item.uuid).collect::<Vec<_>>()); - - // let a = q - // .prev() - // .inspect_err(|e| println!("{e:?}")) - // .unwrap() - // .item - // .uuid; - // q.dbg_items(); - // dbg!(a, &q.played.iter().map(|i| i.item.uuid).collect::<Vec<_>>()); } } From 42ff5b20a8447f775d6c57623753da69be60f0a9 Mon Sep 17 00:00:00 2001 From: G2-Games <ke0bhogsg@gmail.com> Date: Tue, 28 May 2024 23:12:42 -0500 Subject: [PATCH 127/136] Polished some things --- src/music_player/gstreamer.rs | 25 ++++++++++++++----------- src/music_storage/library.rs | 31 +++++++++++++++++-------------- 2 files changed, 31 insertions(+), 25 deletions(-) diff --git a/src/music_player/gstreamer.rs b/src/music_player/gstreamer.rs index db35c03..a503ae6 100644 --- a/src/music_player/gstreamer.rs +++ b/src/music_player/gstreamer.rs @@ -94,6 +94,7 @@ impl GStreamer { } // Make sure the playback tracker knows the stuff is stopped + println!("Beginning switch"); self.playback_tx.send(PlaybackInfo::Switching).unwrap(); let uri = self.playbin.read().unwrap().property_value("current-uri"); @@ -133,12 +134,11 @@ impl GStreamer { .unwrap() .set_property("uri", source.as_uri()); - self.play().unwrap(); + if self.state() != PlayerState::Playing { + self.play().unwrap(); + } - while uri.get::<&str>().unwrap_or("") - == self.property("current-uri").get::<&str>().unwrap_or("") - || self.position().is_none() - { + while self.raw_duration().is_none() { std::thread::sleep(std::time::Duration::from_millis(10)); } @@ -249,7 +249,7 @@ impl Player for GStreamer { .ok_or(PlayerError::Build)?; playbin.write().unwrap().set_property_from_value("flags", &flags); - playbin.write().unwrap().set_property("instant-uri", true); + //playbin.write().unwrap().set_property("instant-uri", true); let position = Arc::new(RwLock::new(None)); @@ -258,8 +258,7 @@ impl Player for GStreamer { let (status_tx, status_rx) = unbounded::<PlaybackInfo>(); let position_update = Arc::clone(&position); - let _playback_monitor = - std::thread::spawn(|| playback_monitor(playbin_arc, status_rx, playback_tx, position_update)); + std::thread::spawn(|| playback_monitor(playbin_arc, status_rx, playback_tx, position_update)); // Set up the thread to monitor bus messages let playbin_bus_ctrl = Arc::clone(&playbin); @@ -477,7 +476,7 @@ fn playback_monitor( let mut sent_atf = false; loop { // Check for new messages to decide how to proceed - if let Ok(result) = status_rx.recv_timeout(std::time::Duration::from_millis(10)) { + if let Ok(result) = status_rx.recv_timeout(std::time::Duration::from_millis(50)) { stats = result } @@ -490,8 +489,9 @@ fn playback_monitor( match stats { PlaybackInfo::Playing{start, end} if pos_temp.is_some() => { // Check if the current playback position is close to the end - let finish_point = end - Duration::milliseconds(250); + let finish_point = end - Duration::milliseconds(2000); if pos_temp.unwrap().num_microseconds() >= end.num_microseconds() { + println!("MONITOR: End of stream"); let _ = playback_tx.try_send(PlayerCommand::EndOfStream); playbin .write() @@ -500,7 +500,9 @@ fn playback_monitor( .expect("Unable to set the pipeline state"); sent_atf = false } else if pos_temp.unwrap().num_microseconds() >= finish_point.num_microseconds() - && !sent_atf { + && !sent_atf + { + println!("MONITOR: About to finish"); let _ = playback_tx.try_send(PlayerCommand::AboutToFinish); sent_atf = true; } @@ -510,6 +512,7 @@ fn playback_monitor( pos_temp = Some(pos_temp.unwrap() - start) }, PlaybackInfo::Finished => { + println!("MONITOR: Shutting down"); *position.write().unwrap() = None; break }, diff --git a/src/music_storage/library.rs b/src/music_storage/library.rs index 1dd63b5..a2fed42 100644 --- a/src/music_storage/library.rs +++ b/src/music_storage/library.rs @@ -283,8 +283,8 @@ impl Song { } // Find images around the music file that can be used - let mut found_images = find_images(target_file.as_ref()).unwrap(); - album_art.append(&mut found_images); + let found_images = find_images(target_file.as_ref()).unwrap(); + album_art.extend_from_slice(&found_images); // Get the format as a string let format: Option<FileFormat> = match FileFormat::from_file(target_file) { @@ -624,8 +624,6 @@ impl Album<'_> { } } -const BLOCKED_EXTENSIONS: [&str; 4] = ["vob", "log", "txt", "sf2"]; - #[derive(Debug, Serialize, Deserialize)] pub struct MusicLibrary { pub name: String, @@ -636,6 +634,8 @@ pub struct MusicLibrary { } impl MusicLibrary { + const BLOCKED_EXTENSIONS: &'static [&'static str] = &["vob", "log", "txt", "sf2"]; + /// Create a new library from a name and [Uuid] fn new(name: String, uuid: Uuid) -> Self { MusicLibrary { @@ -654,7 +654,7 @@ impl MusicLibrary { /// the [MusicLibrary] Vec pub fn init(config: Arc<RwLock<Config>>, uuid: Uuid) -> Result<Self, Box<dyn Error>> { let global_config = &*config.read().unwrap(); - let path = global_config.libraries.get_library(&uuid)?.path; + let path = global_config.libraries.get_library(&uuid)?.path.clone(); let library: MusicLibrary = match path.exists() { true => read_file(path)?, @@ -671,7 +671,8 @@ impl MusicLibrary { } //#[cfg(debug_assertions)] // We probably wouldn't want to use this for real, but maybe it would have some utility? - pub fn from_path(path: PathBuf) -> Result<Self, Box<dyn Error>> { + pub fn from_path<P: ?Sized + AsRef<Path>>(path: &P) -> Result<Self, Box<dyn Error>> { + let path: PathBuf = path.as_ref().to_path_buf(); let library: MusicLibrary = match path.exists() { true => read_file(path)?, false => { @@ -684,8 +685,8 @@ impl MusicLibrary { } /// Serializes the database out to the file specified in the config - pub fn save(&self, config: Config) -> Result<(), Box<dyn Error>> { - let path = config.libraries.get_library(&self.uuid)?.path; + pub fn save(&self, config: Arc<RwLock<Config>>) -> Result<(), Box<dyn Error>> { + let path = config.read().unwrap().libraries.get_library(&self.uuid)?.path.clone(); match path.try_exists() { Ok(_) => write_file(self, path)?, Err(error) => return Err(error.into()), @@ -715,7 +716,7 @@ impl MusicLibrary { for location in &track.location { //TODO: check that this works if path == location { - return std::ops::ControlFlow::Break((track, i)); + return Break((track, i)); } } Continue(()) @@ -765,7 +766,7 @@ impl MusicLibrary { } /// Finds all the audio files within a specified folder - pub fn scan_folder(&mut self, target_path: &str) -> Result<i32, Box<dyn std::error::Error>> { + pub fn scan_folder<P: ?Sized + AsRef<Path>>(&mut self, target_path: &P) -> Result<i32, Box<dyn std::error::Error>> { let mut total = 0; let mut errors = 0; for target_file in WalkDir::new(target_path) @@ -803,27 +804,29 @@ impl MusicLibrary { // 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) - && !BLOCKED_EXTENSIONS.contains(&extension.as_str()) + && !Self::BLOCKED_EXTENSIONS.contains(&extension.as_str()) { match self.add_file(target_file.path()) { Ok(_) => total += 1, Err(_error) => { errors += 1; - //println!("{}, {:?}: {}", format, target_file.file_name(), _error) + println!("{:?}: {}", target_file.file_name(), _error) } // TODO: Handle more of these errors }; } else if extension == "cue" { total += match self.add_cuesheet(target_file.path()) { Ok(added) => added, - Err(error) => { + Err(_error) => { errors += 1; - //println!("{}", error); + println!("{:?}: {}", target_file.file_name(), _error); 0 } } } } + println!("Total scanning errors: {}", errors); + Ok(total) } From 407a1b16bdeb143511f802cf18dfd322e32aa60d Mon Sep 17 00:00:00 2001 From: G2-Games <ke0bhogsg@gmail.com> Date: Tue, 28 May 2024 23:58:56 -0500 Subject: [PATCH 128/136] Modified method on `Player` trait, improved documentation --- src/music_player/gstreamer.rs | 45 ++++++++++++----------------------- src/music_player/player.rs | 40 ++++++++++++++++++++++++------- 2 files changed, 46 insertions(+), 39 deletions(-) diff --git a/src/music_player/gstreamer.rs b/src/music_player/gstreamer.rs index a503ae6..cefd74e 100644 --- a/src/music_player/gstreamer.rs +++ b/src/music_player/gstreamer.rs @@ -213,6 +213,11 @@ impl GStreamer { fn property(&self, property: &str) -> glib::Value { self.playbin().unwrap().property_value(property) } + + fn ready(&mut self) -> Result<(), PlayerError> { + self.set_state(gst::State::Ready)?; + Ok(()) + } } impl Player for GStreamer { @@ -329,62 +334,46 @@ impl Player for GStreamer { &self.source } - /// Insert a new track to be played. This method should be called at the - /// beginning to start playback of something, and once the [PlayerCommand] - /// indicates the track is about to finish to enqueue gaplessly. fn enqueue_next(&mut self, next_track: &URI) -> Result<(), PlayerError> { self.set_source(next_track) } - /// Set the playback volume, accepts a float from 0 to 1 fn set_volume(&mut self, volume: f64) { self.volume = volume.clamp(0.0, 1.0); self.set_gstreamer_volume(self.volume); } - /// Returns the current volume level, a float from 0 to 1 - fn volume(&mut self) -> f64 { + fn volume(&self) -> f64 { self.volume } - fn ready(&mut self) -> Result<(), PlayerError> { - self.set_state(gst::State::Ready)?; - Ok(()) - } - - /// If the player is paused or stopped, starts playback fn play(&mut self) -> Result<(), PlayerError> { + if self.state() == PlayerState::Playing { + return Ok(()) + } *self.paused.write().unwrap() = false; self.set_state(gst::State::Playing)?; Ok(()) } - /// Pause, if playing fn pause(&mut self) -> Result<(), PlayerError> { + if self.state() == PlayerState::Paused || *self.paused.read().unwrap() { + return Ok(()) + } *self.paused.write().unwrap() = true; self.set_state(gst::State::Paused)?; Ok(()) } - /// Resume from being paused - fn resume(&mut self) -> Result<(), PlayerError> { - *self.paused.write().unwrap() = false; - self.set_state(gst::State::Playing)?; - Ok(()) - } - - /// Check if playback is paused - fn is_paused(&mut self) -> bool { + fn is_paused(&self) -> bool { self.playbin().unwrap().current_state() == gst::State::Paused } - /// Get the current playback position of the player - fn position(&mut self) -> Option<Duration> { + fn position(&self) -> Option<Duration> { *self.position.read().unwrap() } - /// Get the duration of the currently playing track - fn duration(&mut self) -> Option<Duration> { + fn duration(&self) -> Option<Duration> { if self.end.is_some() && self.start.is_some() { Some(self.end.unwrap() - self.start.unwrap()) } else { @@ -392,7 +381,6 @@ impl Player for GStreamer { } } - /// Seek relative to the current position fn seek_by(&mut self, seek_amount: Duration) -> Result<(), PlayerError> { let time_pos = match *self.position.read().unwrap() { Some(pos) => pos, @@ -404,7 +392,6 @@ impl Player for GStreamer { Ok(()) } - /// Seek absolutely fn seek_to(&mut self, target_pos: Duration) -> Result<(), PlayerError> { let start = if self.start.is_none() { return Err(PlayerError::Seek("No START time".into())); @@ -432,7 +419,6 @@ impl Player for GStreamer { Ok(()) } - /// Stop the playback entirely fn stop(&mut self) -> Result<(), PlayerError> { self.pause()?; self.ready()?; @@ -447,7 +433,6 @@ impl Player for GStreamer { Ok(()) } - /// Return a reference to the player message channel fn message_channel(&self) -> &crossbeam::channel::Receiver<PlayerCommand> { &self.message_rx } diff --git a/src/music_player/player.rs b/src/music_player/player.rs index 3a9e76e..1bfe18c 100644 --- a/src/music_player/player.rs +++ b/src/music_player/player.rs @@ -40,36 +40,58 @@ pub enum PlayerCommand { } pub trait Player { - /// Create a new player + /// Create a new player. fn new() -> Result<Self, PlayerError> where Self: Sized; + /// Get the currently playing [URI] from the player. fn source(&self) -> &Option<URI>; + /// Insert a new [`URI`] to be played. This method should be called at the + /// beginning to start playback of something, and once the [`PlayerCommand`] + /// indicates the track is about to finish to enqueue gaplessly. + /// + /// For backends which do not support gapless playback, `AboutToFinish` + /// will not be called, and the next [`URI`] should be enqueued once `Eos` + /// occurs. fn enqueue_next(&mut self, next_track: &URI) -> Result<(), PlayerError>; + /// Set the playback volume, accepts a float from `0` to `1`. + /// + /// Values outside the range of `0` to `1` will be capped. fn set_volume(&mut self, volume: f64); - fn volume(&mut self) -> f64; - - fn ready(&mut self) -> Result<(), PlayerError>; + /// Returns the current volume level, a float from `0` to `1`. + fn volume(&self) -> f64; + /// If the player is paused or stopped, starts playback. fn play(&mut self) -> Result<(), PlayerError>; - fn resume(&mut self) -> Result<(), PlayerError>; - + /// If the player is playing, pause playback. fn pause(&mut self) -> Result<(), PlayerError>; + /// Stop the playback entirely, removing the current [`URI`] from the player. fn stop(&mut self) -> Result<(), PlayerError>; - fn is_paused(&mut self) -> bool; + /// Convenience function to check if playback is paused. + fn is_paused(&self) -> bool; - fn position(&mut self) -> Option<Duration>; + /// Get the current playback position of the player. + fn position(&self) -> Option<Duration>; - fn duration(&mut self) -> Option<Duration>; + /// Get the duration of the currently playing track. + fn duration(&self) -> Option<Duration>; + /// Seek relative to the current position. + /// + /// The position is capped at the duration of the song, and zero. fn seek_by(&mut self, seek_amount: Duration) -> Result<(), PlayerError>; + /// Seek absolutely within the song. + /// + /// The position is capped at the duration of the song, and zero. fn seek_to(&mut self, target_pos: Duration) -> Result<(), PlayerError>; + /// Return a reference to the player message channel, which can be cloned + /// in order to monitor messages from the player. fn message_channel(&self) -> &crossbeam::channel::Receiver<PlayerCommand>; } From 54704260babbb81eeb79bafc89a4ff99a8effe9a Mon Sep 17 00:00:00 2001 From: MrDulfin <mrdulfin@mrdulfin.com> Date: Wed, 29 May 2024 01:09:00 -0400 Subject: [PATCH 129/136] Added controller & queue changes --- src/music_controller/controller.rs | 31 +++++++++++++++----- src/music_controller/queue.rs | 2 +- src/music_storage/db_reader/itunes/reader.rs | 2 +- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/music_controller/controller.rs b/src/music_controller/controller.rs index e3f1e68..5664121 100644 --- a/src/music_controller/controller.rs +++ b/src/music_controller/controller.rs @@ -6,6 +6,7 @@ use crossbeam_channel; use crossbeam_channel::{Receiver, Sender}; use std::path::PathBuf; use std::sync::{Arc, RwLock}; +use std::thread::spawn; use thiserror::Error; use crossbeam_channel::unbounded; @@ -13,18 +14,18 @@ use std::error::Error; use uuid::Uuid; use crate::config::config::ConfigError; -use crate::music_player::player::{Player, PlayerError}; +use crate::music_player::player::{Player, PlayerCommand, PlayerError}; use crate::{ config::config::Config, music_controller::queue::Queue, music_storage::library::MusicLibrary, }; use super::queue::QueueError; -pub struct Controller<P: Player> { +pub struct Controller<P: Player + Send + Sync> { pub queue: Queue, pub config: Arc<RwLock<Config>>, pub library: MusicLibrary, - pub player: Box<P>, + pub player: Arc<RwLock<Box<P>>>, } #[derive(Error, Debug)] @@ -69,7 +70,7 @@ impl<T: Send, U: Send> MailMan<T, U> { } #[allow(unused_variables)] -impl<P: Player> Controller<P> { +impl<P: Player + Send + Sync> Controller<P> { pub fn start<T>(config_path: T) -> Result<Self, Box<dyn Error>> where std::path::PathBuf: std::convert::From<T>, @@ -83,12 +84,28 @@ impl<P: Player> Controller<P> { let config_ = Arc::new(RwLock::from(config)); let library = MusicLibrary::init(config_.clone(), uuid)?; - Ok(Controller { + let controller = Controller { queue: Queue::default(), config: config_.clone(), library, - player: Box::new(P::new()?), - }) + player: Arc::new(RwLock::new(Box::new(P::new()?))), + }; + + + let player = controller.player.clone(); + let controler_thread = spawn(move || { + match player.read().unwrap().message_channel().recv().unwrap() { + PlayerCommand::AboutToFinish => {}, + PlayerCommand::EndOfStream => { + + player.write().unwrap().enqueue_next(todo!()); + }, + _ => {} + } + }); + + + Ok(controller) } pub fn q_add(&mut self, item: &Uuid, source: super::queue::PlayerLocation, by_human: bool) { diff --git a/src/music_controller/queue.rs b/src/music_controller/queue.rs index c26ae05..519f8c6 100644 --- a/src/music_controller/queue.rs +++ b/src/music_controller/queue.rs @@ -59,7 +59,7 @@ pub struct Queue { pub items: Vec<QueueItem>, pub played: Vec<QueueItem>, pub loop_: bool, - pub shuffle: bool, + pub shuffle: Option<Vec<usize>>, } // TODO: HAndle the First QueueState[looping] and shuffle diff --git a/src/music_storage/db_reader/itunes/reader.rs b/src/music_storage/db_reader/itunes/reader.rs index f6032db..a900fa8 100644 --- a/src/music_storage/db_reader/itunes/reader.rs +++ b/src/music_storage/db_reader/itunes/reader.rs @@ -353,6 +353,6 @@ mod tests { songs.iter().for_each(|song| library.add_song(song.to_owned()).unwrap()); config.write_file().unwrap(); - library.save(config).unwrap(); + library.save(Arc::new(RwLock::from(config))).unwrap(); } } From 0513109de819ac7f4c27291daaec7f9be780a767 Mon Sep 17 00:00:00 2001 From: G2-Games <ke0bhogsg@gmail.com> Date: Wed, 29 May 2024 18:40:32 -0500 Subject: [PATCH 130/136] Moved `config` to a `mod.rs` file, made controller Player static --- src/config/{config.rs => mod.rs} | 6 +++-- src/lib.rs | 7 ++--- src/music_controller/controller.rs | 28 +++++++++++++------- src/music_controller/queue.rs | 2 +- src/music_storage/db_reader/itunes/reader.rs | 2 +- src/music_storage/library.rs | 4 +-- src/music_storage/playlist.rs | 2 +- 7 files changed, 30 insertions(+), 21 deletions(-) rename src/config/{config.rs => mod.rs} (97%) diff --git a/src/config/config.rs b/src/config/mod.rs similarity index 97% rename from src/config/config.rs rename to src/config/mod.rs index 18ab894..431498f 100644 --- a/src/config/config.rs +++ b/src/config/mod.rs @@ -1,3 +1,5 @@ +pub mod other_settings; + use std::{ fs::{self, File, OpenOptions}, io::{Error, Read, Write}, @@ -214,7 +216,7 @@ pub mod tests { ) .unwrap(); lib.scan_folder("test-config/music/").unwrap(); - lib.save(config.clone()).unwrap(); + lib.save(Arc::new(RwLock::new(config.clone()))).unwrap(); (config, lib) } @@ -232,7 +234,7 @@ pub mod tests { lib.scan_folder("test-config/music/").unwrap(); - lib.save(config.clone()).unwrap(); + lib.save(Arc::new(RwLock::new(config.clone()))).unwrap(); (config, lib) } diff --git a/src/lib.rs b/src/lib.rs index 9f6bb0a..6f93ec1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,8 +18,5 @@ pub mod music_player { pub mod gstreamer; pub mod player; } -#[allow(clippy::module_inception)] -pub mod config { - pub mod config; - pub mod other_settings; -} + +pub mod config; diff --git a/src/music_controller/controller.rs b/src/music_controller/controller.rs index 5664121..9d5c0b4 100644 --- a/src/music_controller/controller.rs +++ b/src/music_controller/controller.rs @@ -13,10 +13,10 @@ use crossbeam_channel::unbounded; use std::error::Error; use uuid::Uuid; -use crate::config::config::ConfigError; +use crate::config::ConfigError; use crate::music_player::player::{Player, PlayerCommand, PlayerError}; use crate::{ - config::config::Config, music_controller::queue::Queue, music_storage::library::MusicLibrary, + config::Config, music_controller::queue::Queue, music_storage::library::MusicLibrary, }; use super::queue::QueueError; @@ -25,7 +25,7 @@ pub struct Controller<P: Player + Send + Sync> { pub queue: Queue, pub config: Arc<RwLock<Config>>, pub library: MusicLibrary, - pub player: Arc<RwLock<Box<P>>>, + pub player: Arc<RwLock<P>>, } #[derive(Error, Debug)] @@ -70,7 +70,7 @@ impl<T: Send, U: Send> MailMan<T, U> { } #[allow(unused_variables)] -impl<P: Player + Send + Sync> Controller<P> { +impl<P: Player + Send + Sync + Sized + 'static> Controller<P> { pub fn start<T>(config_path: T) -> Result<Self, Box<dyn Error>> where std::path::PathBuf: std::convert::From<T>, @@ -88,12 +88,11 @@ impl<P: Player + Send + Sync> Controller<P> { queue: Queue::default(), config: config_.clone(), library, - player: Arc::new(RwLock::new(Box::new(P::new()?))), + player: Arc::new(RwLock::new(P::new()?)), }; - - let player = controller.player.clone(); - let controler_thread = spawn(move || { + let player = Arc::clone(&controller.player); + let controller_thread = spawn(move || { match player.read().unwrap().message_channel().recv().unwrap() { PlayerCommand::AboutToFinish => {}, PlayerCommand::EndOfStream => { @@ -115,4 +114,15 @@ impl<P: Player + Send + Sync> Controller<P> { } #[cfg(test)] -mod test_super {} +mod test_super { + use crate::{config::tests::read_config_lib, music_player::gstreamer::GStreamer}; + + use super::Controller; + + #[test] + fn construct_controller() { + let config = read_config_lib(); + + let controller = Controller::<GStreamer>::start("test-config/config_test.json").unwrap(); + } +} diff --git a/src/music_controller/queue.rs b/src/music_controller/queue.rs index 519f8c6..a2965c8 100644 --- a/src/music_controller/queue.rs +++ b/src/music_controller/queue.rs @@ -391,7 +391,7 @@ impl Queue { mod test_super { #![allow(unused)] use crate::{ - config::config::tests::{new_config_lib, read_config_lib}, + config::tests::{new_config_lib, read_config_lib}, music_storage::library, }; diff --git a/src/music_storage/db_reader/itunes/reader.rs b/src/music_storage/db_reader/itunes/reader.rs index a900fa8..98ea1da 100644 --- a/src/music_storage/db_reader/itunes/reader.rs +++ b/src/music_storage/db_reader/itunes/reader.rs @@ -336,7 +336,7 @@ impl ITunesSong { mod tests { use std::{path::{Path, PathBuf}, sync::{Arc, RwLock}}; - use crate::{config::config::{Config, ConfigLibrary}, music_storage::{db_reader::extern_library::ExternalLibrary, library::MusicLibrary}}; + use crate::{config::{Config, ConfigLibrary}, music_storage::{db_reader::extern_library::ExternalLibrary, library::MusicLibrary}}; use super::ITunesLibrary; diff --git a/src/music_storage/library.rs b/src/music_storage/library.rs index a2fed42..ae189f4 100644 --- a/src/music_storage/library.rs +++ b/src/music_storage/library.rs @@ -1,7 +1,7 @@ use super::playlist::PlaylistFolder; // Crate things use super::utils::{find_images, normalize, read_file, write_file}; -use crate::config::config::Config; +use crate::config::Config; // Various std things use std::collections::BTreeMap; @@ -1127,7 +1127,7 @@ mod test { sync::{Arc, RwLock}, }; - use crate::{config::config::Config, music_storage::library::MusicLibrary}; + use crate::{config::{tests::new_config_lib, Config}, music_storage::library::MusicLibrary}; #[test] fn library_init() { diff --git a/src/music_storage/playlist.rs b/src/music_storage/playlist.rs index 1f04860..c4c0d8c 100644 --- a/src/music_storage/playlist.rs +++ b/src/music_storage/playlist.rs @@ -315,7 +315,7 @@ impl Default for Playlist { #[cfg(test)] mod test_super { use super::*; - use crate::config::config::tests::read_config_lib; + use crate::config::tests::read_config_lib; #[test] fn list_to_m3u8() { From 683b695bc6d033c5cd7f9ff74156a58fdf40a138 Mon Sep 17 00:00:00 2001 From: MrDulfin <mrdulfin@mrdulfin.com> Date: Thu, 30 May 2024 01:20:04 -0400 Subject: [PATCH 131/136] added controller changes --- .gitignore | 1 + src/music_controller/controller.rs | 80 +++++++++++++++++++++++------- src/music_controller/queue.rs | 2 +- src/music_player/gstreamer.rs | 1 + 4 files changed, 66 insertions(+), 18 deletions(-) diff --git a/.gitignore b/.gitignore index 7a0594f..b2d0597 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ music_database* *.json *.zip *.xml + diff --git a/src/music_controller/controller.rs b/src/music_controller/controller.rs index 9d5c0b4..22109cc 100644 --- a/src/music_controller/controller.rs +++ b/src/music_controller/controller.rs @@ -5,8 +5,9 @@ use crossbeam_channel; use crossbeam_channel::{Receiver, Sender}; use std::path::PathBuf; -use std::sync::{Arc, RwLock}; -use std::thread::spawn; +use std::sync::{Arc, Mutex, RwLock}; +use std::thread::{sleep, spawn}; +use std::time::Duration; use thiserror::Error; use crossbeam_channel::unbounded; @@ -22,10 +23,10 @@ use crate::{ use super::queue::QueueError; pub struct Controller<P: Player + Send + Sync> { - pub queue: Queue, + pub queue: Arc<RwLock<Queue>>, pub config: Arc<RwLock<Config>>, pub library: MusicLibrary, - pub player: Arc<RwLock<P>>, + pub player: Arc<Mutex<P>>, } #[derive(Error, Debug)] @@ -85,22 +86,43 @@ impl<P: Player + Send + Sync + Sized + 'static> Controller<P> { let library = MusicLibrary::init(config_.clone(), uuid)?; let controller = Controller { - queue: Queue::default(), + queue: Arc::new(RwLock::from(Queue::default())), config: config_.clone(), library, - player: Arc::new(RwLock::new(P::new()?)), + player: Arc::new(Mutex::new(P::new()?)), }; - let player = Arc::clone(&controller.player); - let controller_thread = spawn(move || { - match player.read().unwrap().message_channel().recv().unwrap() { - PlayerCommand::AboutToFinish => {}, - PlayerCommand::EndOfStream => { - player.write().unwrap().enqueue_next(todo!()); - }, - _ => {} + let player = controller.player.clone(); + let queue = controller.queue.clone(); + let controller_thread = spawn(move || { + loop { + let signal = { player.lock().unwrap().message_channel().recv().unwrap() }; + match signal { + PlayerCommand::AboutToFinish => { + println!("Switching songs!"); + + let mut queue = queue.write().unwrap(); + + let uri = queue + .next() + .unwrap() + .clone(); + + player + .lock() + .unwrap() + .enqueue_next(uri.item + .primary_uri() + .unwrap() + .0) + .unwrap(); + }, + PlayerCommand::EndOfStream => {dbg!()} + _ => {} + } } + }); @@ -109,20 +131,44 @@ impl<P: Player + Send + Sync + Sized + 'static> Controller<P> { pub fn q_add(&mut self, item: &Uuid, source: super::queue::PlayerLocation, by_human: bool) { let item = self.library.query_uuid(item).unwrap().0.to_owned(); - self.queue.add_item(item, source, by_human) + self.queue.write().unwrap().add_item(item, source, by_human) } } #[cfg(test)] mod test_super { - use crate::{config::tests::read_config_lib, music_player::gstreamer::GStreamer}; + use std::{thread::sleep, time::Duration}; + + use crate::{config::tests::read_config_lib, music_controller::queue::PlayerLocation, music_player::{gstreamer::GStreamer, player::Player}}; use super::Controller; #[test] fn construct_controller() { + println!("starto!"); let config = read_config_lib(); - let controller = Controller::<GStreamer>::start("test-config/config_test.json").unwrap(); + let next = config.1.library[2].clone(); + { + let controller = Controller::<GStreamer>::start("test-config/config_test.json").unwrap(); + { + let mut queue = controller.queue.write().unwrap(); + for x in config.1.library { + queue.add_item(x, PlayerLocation::Library, true); + } + } + { + controller.player.lock().unwrap().enqueue_next(next.primary_uri().unwrap().0).unwrap(); + } + { + controller.player.lock().unwrap().set_volume(0.2); + } + { + controller.player.lock().unwrap().play().unwrap(); + } + println!("I'm a tire"); + } + sleep(Duration::from_secs(600)) + } } diff --git a/src/music_controller/queue.rs b/src/music_controller/queue.rs index a2965c8..3acdd7a 100644 --- a/src/music_controller/queue.rs +++ b/src/music_controller/queue.rs @@ -74,7 +74,7 @@ impl Queue { } #[allow(unused)] - fn dbg_items(&self) { + pub(super) fn dbg_items(&self) { dbg!( self.items .iter() diff --git a/src/music_player/gstreamer.rs b/src/music_player/gstreamer.rs index cefd74e..5fa44bc 100644 --- a/src/music_player/gstreamer.rs +++ b/src/music_player/gstreamer.rs @@ -335,6 +335,7 @@ impl Player for GStreamer { } fn enqueue_next(&mut self, next_track: &URI) -> Result<(), PlayerError> { + println!("enqueuing in fn"); self.set_source(next_track) } From 81ccab01f1cb4c029b81a37413aa1f8b1d8d7154 Mon Sep 17 00:00:00 2001 From: MrDulfin <mrdulfin@mrdulfin.com> Date: Sun, 2 Jun 2024 23:51:59 -0400 Subject: [PATCH 132/136] implemented Queue changes with kushi crate --- Cargo.toml | 1 + src/lib.rs | 2 +- src/music_controller/controller.rs | 58 +++++++++++++++++++++--------- src/music_controller/queue.rs | 11 +----- src/music_storage/library.rs | 4 +++ 5 files changed, 48 insertions(+), 28 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 08c25dc..c12abb7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,3 +48,4 @@ tempfile = "3.10.1" listenbrainz = "0.7.0" discord-rpc-client = "0.4.0" nestify = "0.3.3" +kushi = "0.1.1" diff --git a/src/lib.rs b/src/lib.rs index 6f93ec1..d25f68f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,7 +11,7 @@ pub mod music_storage { pub mod music_controller { pub mod controller; pub mod connections; - pub mod queue; + // pub mod queue; } pub mod music_player { diff --git a/src/music_controller/controller.rs b/src/music_controller/controller.rs index 22109cc..8c64baa 100644 --- a/src/music_controller/controller.rs +++ b/src/music_controller/controller.rs @@ -4,6 +4,9 @@ use crossbeam_channel; use crossbeam_channel::{Receiver, Sender}; +use kushi::error::QueueError; +use kushi::traits::Location; +use kushi::{Queue, QueueItemType}; use std::path::PathBuf; use std::sync::{Arc, Mutex, RwLock}; use std::thread::{sleep, spawn}; @@ -16,14 +19,13 @@ use uuid::Uuid; use crate::config::ConfigError; use crate::music_player::player::{Player, PlayerCommand, PlayerError}; +use crate::music_storage::library::{Album, Song}; use crate::{ - config::Config, music_controller::queue::Queue, music_storage::library::MusicLibrary, + config::Config, music_storage::library::MusicLibrary, }; -use super::queue::QueueError; - -pub struct Controller<P: Player + Send + Sync> { - pub queue: Arc<RwLock<Queue>>, +pub struct Controller<'a, P: Player + Send + Sync> { + pub queue: Arc<RwLock<Queue<Song, Album<'a>, PlayerLocation>>>, pub config: Arc<RwLock<Config>>, pub library: MusicLibrary, pub player: Arc<Mutex<P>>, @@ -39,6 +41,19 @@ pub enum ControllerError { ConfigError(#[from] ConfigError), } +// TODO: move this to a different location to be used elsewhere +#[derive(Debug, Clone, Copy, PartialEq)] +#[non_exhaustive] +pub enum PlayerLocation { + Test, + Library, + Playlist(Uuid), + File, + Custom, +} + +impl Location for PlayerLocation {} + #[derive(Debug)] pub(super) struct MailMan<T: Send, U: Send> { pub tx: Sender<T>, @@ -71,8 +86,8 @@ impl<T: Send, U: Send> MailMan<T, U> { } #[allow(unused_variables)] -impl<P: Player + Send + Sync + Sized + 'static> Controller<P> { - pub fn start<T>(config_path: T) -> Result<Self, Box<dyn Error>> +impl<P: Player + Send + Sync + Sized + 'static> Controller<'static, P> { + pub fn start<T>(config_path: T) -> Result <Self, Box<dyn Error>> where std::path::PathBuf: std::convert::From<T>, P: Player, @@ -85,8 +100,15 @@ impl<P: Player + Send + Sync + Sized + 'static> Controller<P> { let config_ = Arc::new(RwLock::from(config)); let library = MusicLibrary::init(config_.clone(), uuid)?; + let queue: Queue<Song, Album, PlayerLocation> = Queue { + items: Vec::new(), + played: Vec::new(), + loop_: false, + shuffle: None + }; + let controller = Controller { - queue: Arc::new(RwLock::from(Queue::default())), + queue: Arc::new(RwLock::from(queue)), config: config_.clone(), library, player: Arc::new(Mutex::new(P::new()?)), @@ -112,10 +134,12 @@ impl<P: Player + Send + Sync + Sized + 'static> Controller<P> { player .lock() .unwrap() - .enqueue_next(uri.item - .primary_uri() - .unwrap() - .0) + .enqueue_next(&{ + match uri.item { + QueueItemType::Single(song) => song.primary_uri().unwrap().0.clone(), + _ => unimplemented!() + } + }) .unwrap(); }, PlayerCommand::EndOfStream => {dbg!()} @@ -129,7 +153,7 @@ impl<P: Player + Send + Sync + Sized + 'static> Controller<P> { Ok(controller) } - pub fn q_add(&mut self, item: &Uuid, source: super::queue::PlayerLocation, by_human: bool) { + pub fn q_add(&mut self, item: &Uuid, source: Option<PlayerLocation>, by_human: bool) { let item = self.library.query_uuid(item).unwrap().0.to_owned(); self.queue.write().unwrap().add_item(item, source, by_human) } @@ -139,7 +163,7 @@ impl<P: Player + Send + Sync + Sized + 'static> Controller<P> { mod test_super { use std::{thread::sleep, time::Duration}; - use crate::{config::tests::read_config_lib, music_controller::queue::PlayerLocation, music_player::{gstreamer::GStreamer, player::Player}}; + use crate::{config::tests::read_config_lib, music_controller::controller::PlayerLocation, music_player::{gstreamer::GStreamer, player::Player}}; use super::Controller; @@ -154,21 +178,21 @@ mod test_super { { let mut queue = controller.queue.write().unwrap(); for x in config.1.library { - queue.add_item(x, PlayerLocation::Library, true); + queue.add_item(x, Some(PlayerLocation::Library), true); } } { controller.player.lock().unwrap().enqueue_next(next.primary_uri().unwrap().0).unwrap(); } { - controller.player.lock().unwrap().set_volume(0.2); + controller.player.lock().unwrap().set_volume(0.1); } { controller.player.lock().unwrap().play().unwrap(); } println!("I'm a tire"); } - sleep(Duration::from_secs(600)) + sleep(Duration::from_secs(10)) } } diff --git a/src/music_controller/queue.rs b/src/music_controller/queue.rs index 3acdd7a..d13ab35 100644 --- a/src/music_controller/queue.rs +++ b/src/music_controller/queue.rs @@ -23,16 +23,7 @@ pub enum QueueState { NoState, } -// TODO: move this to a different location to be used elsewhere -#[derive(Debug, Clone, Copy, PartialEq)] -#[non_exhaustive] -pub enum PlayerLocation { - Test, - Library, - Playlist(Uuid), - File, - Custom, -} + #[derive(Debug, Clone, PartialEq)] #[non_exhaustive] diff --git a/src/music_storage/library.rs b/src/music_storage/library.rs index ae189f4..022e1de 100644 --- a/src/music_storage/library.rs +++ b/src/music_storage/library.rs @@ -11,6 +11,7 @@ use std::ops::ControlFlow::{Break, Continue}; // Files use file_format::{FileFormat, Kind}; use glib::filename_to_uri; +use kushi::traits::TrackGroup; use lofty::{AudioFile, ItemKey, ItemValue, ParseOptions, Probe, TagType, TaggedFileExt}; use rcue::parser::parse_from_file; use std::fs; @@ -624,6 +625,9 @@ impl Album<'_> { } } +impl TrackGroup for Album<'_> {} + + #[derive(Debug, Serialize, Deserialize)] pub struct MusicLibrary { pub name: String, From 47483127ed4830053f38757080997b4bbb900640 Mon Sep 17 00:00:00 2001 From: G2-Games <ke0bhogsg@gmail.com> Date: Fri, 28 Jun 2024 22:20:13 -0500 Subject: [PATCH 133/136] Made albums own their contents --- src/music_controller/controller.rs | 9 +- src/music_storage/library.rs | 140 ++++++++++++++--------------- src/music_storage/utils.rs | 12 +-- 3 files changed, 78 insertions(+), 83 deletions(-) diff --git a/src/music_controller/controller.rs b/src/music_controller/controller.rs index 8c64baa..caf6581 100644 --- a/src/music_controller/controller.rs +++ b/src/music_controller/controller.rs @@ -9,8 +9,7 @@ use kushi::traits::Location; use kushi::{Queue, QueueItemType}; use std::path::PathBuf; use std::sync::{Arc, Mutex, RwLock}; -use std::thread::{sleep, spawn}; -use std::time::Duration; +use std::thread::spawn; use thiserror::Error; use crossbeam_channel::unbounded; @@ -24,8 +23,8 @@ use crate::{ config::Config, music_storage::library::MusicLibrary, }; -pub struct Controller<'a, P: Player + Send + Sync> { - pub queue: Arc<RwLock<Queue<Song, Album<'a>, PlayerLocation>>>, +pub struct Controller<P: Player + Send + Sync> { + pub queue: Arc<RwLock<Queue<Song, Album, PlayerLocation>>>, pub config: Arc<RwLock<Config>>, pub library: MusicLibrary, pub player: Arc<Mutex<P>>, @@ -86,7 +85,7 @@ impl<T: Send, U: Send> MailMan<T, U> { } #[allow(unused_variables)] -impl<P: Player + Send + Sync + Sized + 'static> Controller<'static, P> { +impl<P: Player + Send + Sync + Sized + 'static> Controller<P> { pub fn start<T>(config_path: T) -> Result <Self, Box<dyn Error>> where std::path::PathBuf: std::convert::From<T>, diff --git a/src/music_storage/library.rs b/src/music_storage/library.rs index 022e1de..84867df 100644 --- a/src/music_storage/library.rs +++ b/src/music_storage/library.rs @@ -4,7 +4,7 @@ use super::utils::{find_images, normalize, read_file, write_file}; use crate::config::Config; // Various std things -use std::collections::BTreeMap; +use std::collections::{BTreeMap, HashMap}; use std::error::Error; use std::ops::ControlFlow::{Break, Continue}; @@ -320,7 +320,6 @@ impl Song { } /// creates a `Vec<Song>` from a cue file - pub fn from_cue(cuesheet: &Path) -> Result<Vec<(Self, PathBuf)>, Box<dyn Error>> { let mut tracks = Vec::new(); @@ -574,43 +573,43 @@ pub enum Service { } #[derive(Clone, Debug, PartialEq)] -pub struct Album<'a> { - title: &'a String, - artist: Option<&'a String>, - cover: Option<&'a AlbumArt>, - discs: BTreeMap<usize, Vec<&'a Song>>, +pub struct Album { + title: String, + artist: Option<String>, + cover: Option<AlbumArt>, + discs: BTreeMap<u16, Vec<Uuid>>, } #[allow(clippy::len_without_is_empty)] -impl Album<'_> { +impl Album { //returns the Album title pub fn title(&self) -> &String { - self.title + &self.title } /// Returns the album cover as an AlbumArt struct, if it exists - fn cover(&self) -> Option<&AlbumArt> { - self.cover + fn cover(&self) -> &Option<AlbumArt> { + &self.cover } /// Returns the Album Artist, if they exist - pub fn artist(&self) -> Option<&String> { - self.artist + pub fn artist(&self) -> &Option<String> { + &self.artist } - pub fn discs(&self) -> &BTreeMap<usize, Vec<&Song>> { + pub fn discs(&self) -> &BTreeMap<u16, Vec<Uuid>> { &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]) + pub fn track(&self, disc: u16, index: usize) -> Option<&Uuid> { + self.discs.get(&disc)?.get(index) } - fn tracks(&self) -> Vec<&Song> { + fn tracks(&self) -> Vec<Uuid> { let mut songs = Vec::new(); - for disc in &self.discs { - songs.append(&mut disc.1.clone()) + for disc in self.discs.values() { + songs.extend_from_slice(&disc) } songs } @@ -618,15 +617,14 @@ impl Album<'_> { /// 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(); + for disc in self.discs.values() { + total += disc.len(); } total } } -impl TrackGroup for Album<'_> {} - +impl TrackGroup for Album {} #[derive(Debug, Serialize, Deserialize)] pub struct MusicLibrary { @@ -688,6 +686,17 @@ impl MusicLibrary { Ok(library) } + /// Serializes the database out to the file specified in the config + pub fn save_path<P: ?Sized + AsRef<Path>>(&self, path: &P) -> Result<(), Box<dyn Error>> { + let path = path.as_ref(); + match path.try_exists() { + Ok(_) => write_file(self, path)?, + Err(error) => return Err(error.into()), + } + + Ok(()) + } + /// Serializes the database out to the file specified in the config pub fn save(&self, config: Arc<RwLock<Config>>) -> Result<(), Box<dyn Error>> { let path = config.read().unwrap().libraries.get_library(&self.uuid)?.path.clone(); @@ -711,6 +720,7 @@ impl MusicLibrary { /// Queries for a [Song] by its [URI], returning a single `Song` /// with the `URI` that matches along with its position in the library + #[inline(always)] pub fn query_uri(&self, path: &URI) -> Option<(&Song, usize)> { let result = self .library @@ -759,9 +769,10 @@ impl MusicLibrary { self.library.par_iter().for_each(|track| { if path == track.primary_uri().unwrap().0.path() { //TODO: make this also not unwrap - result.clone().lock().unwrap().push(track); + Arc::clone(&result).lock().unwrap().push(track); } }); + if result.lock().unwrap().len() > 0 { Some(Arc::try_unwrap(result).unwrap().into_inner().unwrap()) } else { @@ -786,19 +797,11 @@ 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; } - // Save periodically while scanning - i += 1; - if i % 500 == 0 { - self.save(config).unwrap(); - } - */ - let format = FileFormat::from_file(path)?; let extension = match path.extension() { Some(ext) => ext.to_string_lossy().to_ascii_lowercase(), @@ -834,6 +837,22 @@ impl MusicLibrary { Ok(total) } + pub fn remove_missing(&mut self) { + let target_removals = Arc::new(Mutex::new(Vec::new())); + self.library.par_iter().for_each(|t|{ + for location in &t.location { + if !location.exists().unwrap() { + Arc::clone(&target_removals).lock().unwrap().push(location.clone()); + } + } + }); + + let target_removals = Arc::try_unwrap(target_removals).unwrap().into_inner().unwrap(); + for location in target_removals { + self.remove_uri(&location).unwrap(); + } + } + pub fn add_file(&mut self, target_file: &Path) -> Result<(), Box<dyn Error>> { let new_song = Song::from_file(target_file)?; match self.add_song(new_song) { @@ -1038,65 +1057,42 @@ impl MusicLibrary { /// Generates all albums from the track list pub fn albums(&self) -> BTreeMap<String, Album> { let mut albums: BTreeMap<String, Album> = BTreeMap::new(); - for result in &self.library { - let title = match result.get_tag(&Tag::Album) { - Some(title) => title, + for song in &self.library { + let album_title = match song.get_tag(&Tag::Album) { + Some(title) => title.clone(), None => continue, }; - let norm_title = normalize(title); + //let norm_title = normalize(&album_title); - let disc_num = result + let disc_num = song .get_tag(&Tag::Disk) .unwrap_or(&"".to_string()) - .parse::<usize>() + .parse::<u16>() .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 + match albums.get_mut(&album_title) { + // If the album is in the list, add the track to the appropriate disc within the album Some(album) => match album.discs.get_mut(&disc_num) { - Some(disc) => disc.push(result), + Some(disc) => disc.push(song.uuid), None => { - album.discs.insert(disc_num, vec![result]); + album.discs.insert(disc_num, vec![song.uuid]); } }, - // If the album is not in the list, make a new one and add it + // If the album is not in the list, make it new one and add it None => { - let album_art = result.album_art.first(); + let album_art = song.album_art.first(); let new_album = Album { - title, - artist: result.get_tag(&Tag::AlbumArtist), - discs: BTreeMap::from([(disc_num, vec![result])]), - cover: album_art, + title: album_title.clone(), + artist: song.get_tag(&Tag::AlbumArtist).cloned(), + discs: BTreeMap::from([(disc_num, vec![song.uuid])]), + cover: album_art.cloned(), }; - albums.insert(norm_title, new_album); + albums.insert(album_title, new_album); } } } - // 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| { - 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::<i32>(), b_track.parse::<i32>()) - { - // 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().to_string()); - let path_b = PathBuf::from(b.get_field("location").unwrap().to_string()); - - 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 e29a6e0..b1171da 100644 --- a/src/music_storage/utils.rs +++ b/src/music_storage/utils.rs @@ -37,13 +37,13 @@ pub(super) fn write_file< writer_name.set_extension("tmp"); // Create a new BufWriter on the file and a snap frame encoder - let writer = BufWriter::new(File::create(&writer_name)?); - let mut e = snap::write::FrameEncoder::new(writer); + let mut writer = BufWriter::new(File::create(&writer_name)?); + //let mut e = snap::write::FrameEncoder::new(writer); // Write out the data bincode::serde::encode_into_std_write( library, - &mut e, + &mut writer, bincode::config::standard() .with_little_endian() .with_variable_int_encoding(), @@ -59,12 +59,12 @@ pub(super) fn read_file<T: for<'de> serde::Deserialize<'de>>( path: PathBuf, ) -> Result<T, Box<dyn Error>> { // Create a new snap reader over the file - let file_reader = BufReader::new(File::open(path)?); - let mut d = snap::read::FrameDecoder::new(file_reader); + let mut file_reader = BufReader::new(File::open(path)?); + //let mut d = snap::read::FrameDecoder::new(file_reader); // Decode the library from the serialized data into the vec let library: T = bincode::serde::decode_from_std_read( - &mut d, + &mut file_reader, bincode::config::standard() .with_little_endian() .with_variable_int_encoding(), From 67f2385c9d782153c00ea1b61b3f9b12ff71fc8d Mon Sep 17 00:00:00 2001 From: MrDulfin <mrdulfin@mrdulfin.com> Date: Sat, 29 Jun 2024 17:12:50 -0400 Subject: [PATCH 134/136] Updated the Queue, added IntoIterator and sorting for Albums --- Cargo.toml | 2 +- src/music_controller/controller.rs | 2 +- src/music_storage/library.rs | 116 ++++++++++++++++++++++++++--- 3 files changed, 108 insertions(+), 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c12abb7..d810a1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,4 +48,4 @@ tempfile = "3.10.1" listenbrainz = "0.7.0" discord-rpc-client = "0.4.0" nestify = "0.3.3" -kushi = "0.1.1" +kushi = "0.1.2" diff --git a/src/music_controller/controller.rs b/src/music_controller/controller.rs index caf6581..97af6e1 100644 --- a/src/music_controller/controller.rs +++ b/src/music_controller/controller.rs @@ -5,7 +5,7 @@ use crossbeam_channel; use crossbeam_channel::{Receiver, Sender}; use kushi::error::QueueError; -use kushi::traits::Location; +use kushi::Location; use kushi::{Queue, QueueItemType}; use std::path::PathBuf; use std::sync::{Arc, Mutex, RwLock}; diff --git a/src/music_storage/library.rs b/src/music_storage/library.rs index 84867df..f7d3e81 100644 --- a/src/music_storage/library.rs +++ b/src/music_storage/library.rs @@ -3,15 +3,17 @@ use super::playlist::PlaylistFolder; use super::utils::{find_images, normalize, read_file, write_file}; use crate::config::Config; +use std::cmp::Ordering; // Various std things use std::collections::{BTreeMap, HashMap}; use std::error::Error; use std::ops::ControlFlow::{Break, Continue}; +use std::vec::IntoIter; // Files use file_format::{FileFormat, Kind}; use glib::filename_to_uri; -use kushi::traits::TrackGroup; + use lofty::{AudioFile, ItemKey, ItemValue, ParseOptions, Probe, TagType, TaggedFileExt}; use rcue::parser::parse_from_file; use std::fs; @@ -577,7 +579,7 @@ pub struct Album { title: String, artist: Option<String>, cover: Option<AlbumArt>, - discs: BTreeMap<u16, Vec<Uuid>>, + discs: BTreeMap<u16, Vec<(u16, Uuid)>>, } #[allow(clippy::len_without_is_empty)] @@ -597,16 +599,16 @@ impl Album { &self.artist } - pub fn discs(&self) -> &BTreeMap<u16, Vec<Uuid>> { + pub fn discs(&self) -> &BTreeMap<u16, Vec<(u16, Uuid)>> { &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: u16, index: usize) -> Option<&Uuid> { + pub fn track(&self, disc: u16, index: usize) -> Option<&(u16, Uuid)> { self.discs.get(&disc)?.get(index) } - fn tracks(&self) -> Vec<Uuid> { + fn tracks(&self) -> Vec<(u16, Uuid)> { let mut songs = Vec::new(); for disc in self.discs.values() { songs.extend_from_slice(&disc) @@ -624,7 +626,50 @@ impl Album { } } -impl TrackGroup for Album {} +impl IntoIterator for Album { + type Item = AlbumTrack; + type IntoIter = IntoIter<Self::Item>; + + fn into_iter(self) -> Self::IntoIter { + let mut vec = vec![]; + + for (disc, mut tracks) in self.discs { + tracks.par_sort_by(|a, b| a.0.cmp(&b.0)); + + let mut tracks = tracks.into_iter() + .map(|(track, uuid)| + AlbumTrack { + disc, + track, + uuid + }) + .collect::<Vec<_>>(); + + vec.append(&mut tracks); + } + vec.into_iter() + } +} + +pub struct AlbumTrack { + disc: u16, + track: u16, + uuid: Uuid +} + +impl AlbumTrack { + pub fn disc(&self) -> &u16 { + &self.disc + } + + pub fn track(&self) -> &u16 { + &self.track + } + + pub fn uuid(&self) -> &Uuid { + &self.uuid + } +} #[derive(Debug, Serialize, Deserialize)] pub struct MusicLibrary { @@ -1056,6 +1101,8 @@ impl MusicLibrary { /// Generates all albums from the track list pub fn albums(&self) -> BTreeMap<String, Album> { + let mut paths = BTreeMap::new(); + let mut albums: BTreeMap<String, Album> = BTreeMap::new(); for song in &self.library { let album_title = match song.get_tag(&Tag::Album) { @@ -1073,26 +1120,75 @@ impl MusicLibrary { match albums.get_mut(&album_title) { // If the album is in the list, add the track to the appropriate disc within the album Some(album) => match album.discs.get_mut(&disc_num) { - Some(disc) => disc.push(song.uuid), + Some(disc) => disc.push(( + song.get_tag(&Tag::Track) + .unwrap_or(&String::new()) + .parse::<u16>() + .unwrap_or_default(), + song.uuid + )), None => { - album.discs.insert(disc_num, vec![song.uuid]); + album.discs.insert(disc_num, vec![( + song.get_tag(&Tag::Track) + .unwrap_or(&String::new()) + .parse::<u16>() + .unwrap_or_default(), + song.uuid + )]); } }, // If the album is not in the list, make it new one and add it None => { let album_art = song.album_art.first(); - let new_album = Album { title: album_title.clone(), artist: song.get_tag(&Tag::AlbumArtist).cloned(), - discs: BTreeMap::from([(disc_num, vec![song.uuid])]), + discs: BTreeMap::from([( + disc_num, + vec![( + song.get_tag(&Tag::Track) + .unwrap_or(&String::new()) + .parse::<u16>() + .unwrap_or_default(), + song.uuid + )])]), cover: album_art.cloned(), }; albums.insert(album_title, new_album); } + } + paths.insert(song.uuid, song.primary_uri().unwrap()); } + // Sort the tracks in each disk in each album + albums.par_iter_mut().for_each(|album| { + for disc in &mut album.1.discs { + disc.1.sort_by(|a, b| { + let num_a = a.0; + let num_b = b.0; + + if (num_a, num_b) != (0,0) + { + // If parsing the track numbers succeeds, compare as numbers + num_a.cmp(&num_b) + } else { + // If parsing doesn't succeed, compare the locations + let a = match paths.get_key_value(&a.1) { + Some((_, (uri, _))) => uri, + None => return Ordering::Equal + }; + let b = match paths.get_key_value(&b.1) { + Some((_, (uri, _))) => uri, + None => return Ordering::Equal + }; + + a.as_uri().cmp(&b.as_uri()) + } + }); + } + }); + // Return the albums! albums } From e97075401d02e970d0d6ce59523796ccad362a2d Mon Sep 17 00:00:00 2001 From: MrDulfin <mrdulfin@mrdulfin.com> Date: Sun, 7 Jul 2024 19:27:13 -0400 Subject: [PATCH 135/136] updated Kushi, mode changes to the moved location field to wrapper structs rather than as part of the queue --- Cargo.toml | 2 +- src/lib.rs | 2 +- src/music_controller/controller.rs | 23 +- src/music_controller/queue.rs | 412 ++--------------------------- 4 files changed, 28 insertions(+), 411 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d810a1e..b4ab8d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,4 +48,4 @@ tempfile = "3.10.1" listenbrainz = "0.7.0" discord-rpc-client = "0.4.0" nestify = "0.3.3" -kushi = "0.1.2" +kushi = "0.1.3" diff --git a/src/lib.rs b/src/lib.rs index d25f68f..6f93ec1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,7 +11,7 @@ pub mod music_storage { pub mod music_controller { pub mod controller; pub mod connections; - // pub mod queue; + pub mod queue; } pub mod music_player { diff --git a/src/music_controller/controller.rs b/src/music_controller/controller.rs index 97af6e1..74c3c62 100644 --- a/src/music_controller/controller.rs +++ b/src/music_controller/controller.rs @@ -4,8 +4,7 @@ use crossbeam_channel; use crossbeam_channel::{Receiver, Sender}; -use kushi::error::QueueError; -use kushi::Location; +use kushi::QueueError; use kushi::{Queue, QueueItemType}; use std::path::PathBuf; use std::sync::{Arc, Mutex, RwLock}; @@ -18,13 +17,15 @@ use uuid::Uuid; use crate::config::ConfigError; use crate::music_player::player::{Player, PlayerCommand, PlayerError}; -use crate::music_storage::library::{Album, Song}; use crate::{ config::Config, music_storage::library::MusicLibrary, }; +use super::queue::{QueueAlbum, QueueSong}; + + pub struct Controller<P: Player + Send + Sync> { - pub queue: Arc<RwLock<Queue<Song, Album, PlayerLocation>>>, + pub queue: Arc<RwLock<Queue<QueueSong, QueueAlbum>>>, pub config: Arc<RwLock<Config>>, pub library: MusicLibrary, pub player: Arc<Mutex<P>>, @@ -51,8 +52,6 @@ pub enum PlayerLocation { Custom, } -impl Location for PlayerLocation {} - #[derive(Debug)] pub(super) struct MailMan<T: Send, U: Send> { pub tx: Sender<T>, @@ -99,7 +98,7 @@ impl<P: Player + Send + Sync + Sized + 'static> Controller<P> { let config_ = Arc::new(RwLock::from(config)); let library = MusicLibrary::init(config_.clone(), uuid)?; - let queue: Queue<Song, Album, PlayerLocation> = Queue { + let queue: Queue<QueueSong, QueueAlbum> = Queue { items: Vec::new(), played: Vec::new(), loop_: false, @@ -135,7 +134,7 @@ impl<P: Player + Send + Sync + Sized + 'static> Controller<P> { .unwrap() .enqueue_next(&{ match uri.item { - QueueItemType::Single(song) => song.primary_uri().unwrap().0.clone(), + QueueItemType::Single(song) => song.song.primary_uri().unwrap().0.clone(), _ => unimplemented!() } }) @@ -152,9 +151,9 @@ impl<P: Player + Send + Sync + Sized + 'static> Controller<P> { Ok(controller) } - pub fn q_add(&mut self, item: &Uuid, source: Option<PlayerLocation>, by_human: bool) { + pub fn q_add(&mut self, item: &Uuid, source: PlayerLocation, by_human: bool) { let item = self.library.query_uuid(item).unwrap().0.to_owned(); - self.queue.write().unwrap().add_item(item, source, by_human) + self.queue.write().unwrap().add_item(QueueSong { song: item, location: source }, by_human) } } @@ -162,7 +161,7 @@ impl<P: Player + Send + Sync + Sized + 'static> Controller<P> { mod test_super { use std::{thread::sleep, time::Duration}; - use crate::{config::tests::read_config_lib, music_controller::controller::PlayerLocation, music_player::{gstreamer::GStreamer, player::Player}}; + use crate::{config::tests::read_config_lib, music_controller::controller::{PlayerLocation, QueueSong}, music_player::{gstreamer::GStreamer, player::Player}}; use super::Controller; @@ -177,7 +176,7 @@ mod test_super { { let mut queue = controller.queue.write().unwrap(); for x in config.1.library { - queue.add_item(x, Some(PlayerLocation::Library), true); + queue.add_item(QueueSong { song: x, location: PlayerLocation::Library }, true); } } { diff --git a/src/music_controller/queue.rs b/src/music_controller/queue.rs index d13ab35..27f8075 100644 --- a/src/music_controller/queue.rs +++ b/src/music_controller/queue.rs @@ -1,407 +1,25 @@ -use crate::music_storage::library::Song; -use uuid::Uuid; - -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum QueueError { - #[error("Index out of bounds! Index {index} is over len {len}")] - OutOfBounds { index: usize, len: usize }, - #[error("The Queue is empty!")] - EmptyQueue, - #[error("There are no past played songs!")] - EmptyPlayed, - #[error("There is no item after this in the Queue")] - NoNext, -} - -#[derive(Debug, PartialEq, Clone, Copy)] -pub enum QueueState { - Played, - First, - AddHere, - NoState, -} +use std::vec::IntoIter; +use crate::music_storage::library::{Album, AlbumTrack, Song}; +use super::controller::PlayerLocation; #[derive(Debug, Clone, PartialEq)] -#[non_exhaustive] -pub struct QueueItem { - pub(super) item: Song, - pub(super) state: QueueState, - pub(super) source: PlayerLocation, - pub(super) by_human: bool, +pub struct QueueSong { + pub song: Song, + pub location: PlayerLocation, } -impl QueueItem { - fn from_song(song: Song, source: PlayerLocation) -> Self { - QueueItem { - item: song, - state: QueueState::NoState, - source, - by_human: false, - } - } +#[derive(Debug, Clone, PartialEq)] +pub struct QueueAlbum { + pub album: Album, + pub location: PlayerLocation, } -#[derive(Debug, Default)] -pub struct Queue { - pub items: Vec<QueueItem>, - pub played: Vec<QueueItem>, - pub loop_: bool, - pub shuffle: Option<Vec<usize>>, -} - -// TODO: HAndle the First QueueState[looping] and shuffle -impl Queue { - fn has_addhere(&self) -> bool { - for item in &self.items { - if item.state == QueueState::AddHere { - return true; - } - } - false - } - - #[allow(unused)] - pub(super) fn dbg_items(&self) { - dbg!( - self.items - .iter() - .map(|item| (item.item.uuid, item.state)) - .collect::<Vec<(Uuid, QueueState)>>(), - self.items.len() - ); - } - - pub fn set_items(&mut self, tracks: Vec<QueueItem>) { - let mut tracks = tracks; - self.items.clear(); - self.items.append(&mut tracks); - } - - /// Inserts an item after the AddHere item - pub fn add_item(&mut self, item: Song, source: PlayerLocation, by_human: bool) { - let mut i: usize = 0; - - self.items = self - .items - .iter() - .enumerate() - .map(|(j, item_)| { - let mut item_ = item_.to_owned(); - // get the index of the current AddHere item and give it to i - if item_.state == QueueState::AddHere { - i = j; - item_.state = QueueState::NoState; - } - item_ - }) - .collect::<Vec<QueueItem>>(); - - self.items.insert( - i + if self.items.is_empty() { 0 } else { 1 }, - QueueItem { - item, - state: QueueState::AddHere, - source, - by_human, - }, - ); - } - - /// Inserts an item after the currently playing item - pub fn add_item_next(&mut self, item: Song, source: PlayerLocation) { - use QueueState::*; - let empty = self.items.is_empty(); - - self.items.insert( - if empty { 0 } else { 1 }, - QueueItem { - item, - state: if (self.items.get(1).is_none() - || !self.has_addhere() && self.items.get(1).is_some()) - || empty - { - AddHere - } else { - NoState - }, - source, - by_human: true, - }, - ) - } - - pub fn add_multi(&mut self, items: Vec<Song>, source: PlayerLocation, by_human: bool) { - let mut i: usize = 0; - - self.items = self - .items - .iter() - .enumerate() - .map(|(j, item_)| { - let mut item_ = item_.to_owned(); - // get the index of the current AddHere item and give it to i - if item_.state == QueueState::AddHere { - i = j; - item_.state = QueueState::NoState; - } - item_ - }) - .collect::<Vec<QueueItem>>(); - - let empty = self.items.is_empty(); - - let len = items.len(); - for item in items.into_iter().rev() { - self.items.insert( - i + if empty { 0 } else { 1 }, - QueueItem { - item, - state: QueueState::NoState, - source, - by_human, - }, - ); - } - self.items[i + len - if empty { 1 } else { 0 }].state = QueueState::AddHere; - } - - /// Add multiple Songs after the currently playing Song - pub fn add_multi_next(&mut self, items: Vec<Song>, source: PlayerLocation) { - use QueueState::*; - let empty = self.items.is_empty(); - - let add_here = (self.items.get(1).is_none() - || !self.has_addhere() && self.items.get(1).is_some()) - || empty; - - let len = items.len(); - - for item in items { - self.items.insert( - if empty { 0 } else { 1 }, - QueueItem { - item, - state: NoState, - source, - by_human: true, - }, - ) - } - - if add_here { - self.items[len - if empty { 1 } else { 0 }].state = QueueState::AddHere; - } - } - - pub fn remove_item(&mut self, remove_index: usize) -> Result<QueueItem, QueueError> { - // dbg!(/*&remove_index, self.current_index(), &index,*/ &self.items[remove_index]); - - if remove_index < self.items.len() { - // update the state of the next item to replace the item being removed - if self.items.get(remove_index + 1).is_some() { - self.items[remove_index + 1].state = self.items[remove_index].state; - } - Ok(self.items.remove(remove_index)) - } else { - Err(QueueError::EmptyQueue) - } - } - - pub fn insert( - &mut self, - index: usize, - new_item: Song, - source: PlayerLocation, - addhere: bool, - ) -> Result<(), QueueError> { - if self.items.get_mut(index).is_none() - && index > 0 - && self.items.get_mut(index - 1).is_none() - { - return Err(QueueError::OutOfBounds { - index, - len: self.items.len(), - }); - } - if addhere { - let mut new_item = QueueItem::from_song(new_item, source); - for item in &mut self.items { - if item.state == QueueState::AddHere { - item.state = QueueState::NoState - } - } - new_item.state = QueueState::AddHere; - self.items.insert(index, new_item); - } else { - let new_item = QueueItem::from_song(new_item, source); - self.items.insert(index, new_item); - } - Ok(()) - } - - pub fn clear(&mut self) { - self.items.clear(); - } - - pub fn clear_except(&mut self, index: usize) -> Result<(), QueueError> { - use QueueState::*; - let empty = self.items.is_empty(); - - if !empty && index < self.items.len() { - let i = self.items[index].clone(); - self.items.retain(|item| *item == i); - self.items[0].state = AddHere; - } else if empty { - return Err(QueueError::EmptyQueue); - } else { - return Err(QueueError::OutOfBounds { - index, - len: self.items.len(), - }); - } - Ok(()) - } - - pub fn clear_played(&mut self) { - self.played.clear(); - } - - pub fn clear_all(&mut self) { - self.items.clear(); - self.played.clear(); - } - - pub fn move_to(&mut self, index: usize) -> Result<(), QueueError> { - use QueueState::*; - - let empty = self.items.is_empty(); - - let index = if !empty { - index - } else { - return Err(QueueError::EmptyQueue); - }; - - if !empty && dbg!(index < self.items.len()) { - let to_item = self.items[index].clone(); - - loop { - let empty = self.items.is_empty(); - let item = self.items[0].item.to_owned(); - - if item != to_item.item && !empty { - if self.items[0].state == AddHere && self.items.get(1).is_some() { - self.items[1].state = AddHere; - } - let item = self.items.remove(0); - self.played.push(item); - - // dbg!(&to_item.item, &self.items[ind].item); - } else if empty { - return Err(QueueError::EmptyQueue); - } else { - break; - } - } - } else { - return Err(QueueError::EmptyQueue); - } - Ok(()) - } - - pub fn swap(&mut self, a: usize, b: usize) { - self.items.swap(a, b) - } - - pub fn move_item(&mut self, from: usize, to: usize) { - let item = self.items[from].to_owned(); - if from != to { - self.items.remove(from); - } - self.items.insert(to, item); - } - - #[allow(clippy::should_implement_trait)] - pub fn next(&mut self) -> Result<&QueueItem, QueueError> { - if self.items.is_empty() { - if self.loop_ { - unimplemented!() // TODO: add function to loop the queue - } else { - return Err(QueueError::EmptyQueue); - } - } - - if self.items[0].state == QueueState::AddHere || !self.has_addhere() { - self.items[0].state = QueueState::NoState; - if self.items.get_mut(1).is_some() { - self.items[1].state = QueueState::AddHere; - } - } - let item = self.items.remove(0); - self.played.push(item); - - if self.items.is_empty() { - Err(QueueError::NoNext) - } else { - Ok(&self.items[0]) - } - } - - pub fn prev(&mut self) -> Result<&QueueItem, QueueError> { - if let Some(item) = self.played.pop() { - if item.state == QueueState::First && self.loop_ { - todo!() - } - self.items.insert(0, item); - Ok(&self.items[0]) - } else { - Err(QueueError::EmptyPlayed) - } - } - - pub fn now_playing(&self) -> Result<&QueueItem, QueueError> { - if !self.items.is_empty() { - Ok(&self.items[0]) - } else { - Err(QueueError::EmptyQueue) - } - } - - pub fn check_played(&mut self) { - while self.played.len() > 50 { - self.played.remove(0); - } - } -} - -#[cfg(test)] -mod test_super { - #![allow(unused)] - use crate::{ - config::tests::{new_config_lib, read_config_lib}, - music_storage::library, - }; - - use super::*; - - #[test] - fn move_test() { - let (_, library) = read_config_lib(); - let mut q = Queue::default(); - - q.insert(0, library.library[2].to_owned(), PlayerLocation::File, true) - .inspect_err(|e| println!("{e}")); - q.insert(1, library.library[2].to_owned(), PlayerLocation::File, true) - .inspect_err(|e| println!("{e}")); - // q.next(); - // q.clear(); - q.dbg_items(); - dbg!(&q.played.len()); - - // q.dbg_items(); +impl IntoIterator for QueueAlbum { + type Item = AlbumTrack; + type IntoIter = IntoIter<Self::Item>; + fn into_iter(self) -> Self::IntoIter { + self.album.into_iter() } } From be9f28e38ffece66c60bc8b15490bb2d268ef167 Mon Sep 17 00:00:00 2001 From: MrDulfin <mrdulfin@mrdulfin.com> Date: Fri, 30 Aug 2024 23:35:28 -0400 Subject: [PATCH 136/136] removed config passing in library functions --- src/config/mod.rs | 8 ++++---- src/music_controller/controller.rs | 3 ++- src/music_player/gstreamer.rs | 1 - src/music_storage/db_reader/itunes/reader.rs | 4 ++-- src/music_storage/library.rs | 12 +++--------- 5 files changed, 11 insertions(+), 17 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index 431498f..6371afb 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -211,12 +211,12 @@ pub mod tests { config.write_file().unwrap(); let mut lib = MusicLibrary::init( - Arc::new(RwLock::from(config.clone())), + config.libraries.get_default().unwrap().path.clone(), dbg!(config.libraries.default_library), ) .unwrap(); lib.scan_folder("test-config/music/").unwrap(); - lib.save(Arc::new(RwLock::new(config.clone()))).unwrap(); + lib.save(config.libraries.get_default().unwrap().path.clone()).unwrap(); (config, lib) } @@ -227,14 +227,14 @@ pub mod tests { // dbg!(&config); let mut lib = MusicLibrary::init( - Arc::new(RwLock::from(config.clone())), + config.libraries.get_default().unwrap().path.clone(), config.libraries.get_default().unwrap().uuid, ) .unwrap(); lib.scan_folder("test-config/music/").unwrap(); - lib.save(Arc::new(RwLock::new(config.clone()))).unwrap(); + lib.save(config.libraries.get_default().unwrap().path.clone()).unwrap(); (config, lib) } diff --git a/src/music_controller/controller.rs b/src/music_controller/controller.rs index 74c3c62..3c9dcb3 100644 --- a/src/music_controller/controller.rs +++ b/src/music_controller/controller.rs @@ -95,8 +95,9 @@ impl<P: Player + Send + Sync + Sized + 'static> Controller<P> { let config = Config::read_file(config_path)?; let uuid = config.libraries.get_default()?.uuid; + let library = MusicLibrary::init(config.libraries.get_default()?.path.clone(), uuid)?; let config_ = Arc::new(RwLock::from(config)); - let library = MusicLibrary::init(config_.clone(), uuid)?; + let queue: Queue<QueueSong, QueueAlbum> = Queue { items: Vec::new(), diff --git a/src/music_player/gstreamer.rs b/src/music_player/gstreamer.rs index 5fa44bc..a6915aa 100644 --- a/src/music_player/gstreamer.rs +++ b/src/music_player/gstreamer.rs @@ -1,5 +1,4 @@ // Crate things -//use crate::music_controller::config::Config; use crate::music_storage::library::URI; use crossbeam_channel::{unbounded, Receiver, Sender}; use std::error::Error; diff --git a/src/music_storage/db_reader/itunes/reader.rs b/src/music_storage/db_reader/itunes/reader.rs index 98ea1da..6ff383c 100644 --- a/src/music_storage/db_reader/itunes/reader.rs +++ b/src/music_storage/db_reader/itunes/reader.rs @@ -348,11 +348,11 @@ mod tests { let songs = ITunesLibrary::from_file(Path::new("test-config\\iTunesLib.xml")).to_songs(); - let mut library = MusicLibrary::init(Arc::new(RwLock::from(config.clone())), config_lib.uuid).unwrap(); + let mut library = MusicLibrary::init(config.libraries.get_default().unwrap().path.clone(), config_lib.uuid).unwrap(); songs.iter().for_each(|song| library.add_song(song.to_owned()).unwrap()); config.write_file().unwrap(); - library.save(Arc::new(RwLock::from(config))).unwrap(); + library.save(config.libraries.get_default().unwrap().path.clone()).unwrap(); } } diff --git a/src/music_storage/library.rs b/src/music_storage/library.rs index f7d3e81..010fed3 100644 --- a/src/music_storage/library.rs +++ b/src/music_storage/library.rs @@ -699,21 +699,16 @@ impl MusicLibrary { /// 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 [MusicLibrary] Vec - pub fn init(config: Arc<RwLock<Config>>, uuid: Uuid) -> Result<Self, Box<dyn Error>> { - let global_config = &*config.read().unwrap(); - let path = global_config.libraries.get_library(&uuid)?.path.clone(); - + pub fn init(path: PathBuf, uuid: Uuid) -> Result<Self, Box<dyn Error>> { let library: MusicLibrary = match path.exists() { true => read_file(path)?, false => { // If the library does not exist, re-create it let lib = MusicLibrary::new(String::new(), uuid); - write_file(&lib, path)?; lib } }; - Ok(library) } @@ -743,8 +738,7 @@ impl MusicLibrary { } /// Serializes the database out to the file specified in the config - pub fn save(&self, config: Arc<RwLock<Config>>) -> Result<(), Box<dyn Error>> { - let path = config.read().unwrap().libraries.get_library(&self.uuid)?.path.clone(); + pub fn save(&self, path: PathBuf) -> Result<(), Box<dyn Error>> { match path.try_exists() { Ok(_) => write_file(self, path)?, Err(error) => return Err(error.into()), @@ -1229,7 +1223,7 @@ mod test { fn library_init() { let config = Config::read_file(PathBuf::from("test_config/config_test.json")).unwrap(); let target_uuid = config.libraries.libraries[0].uuid; - let a = MusicLibrary::init(Arc::new(RwLock::from(config)), target_uuid).unwrap(); + let a = MusicLibrary::init(config.libraries.get_default().unwrap().path.clone(), target_uuid).unwrap(); dbg!(a); } }