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/Cargo.toml b/Cargo.toml index 0267611..08c25dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,15 @@ keywords = [] categories = [] [dependencies] -file-format = { version = "0.23.0", features = ["reader-asf", "reader-ebml", "reader-mp4", "reader-rm", "reader-txt", "reader-xml", "serde"] } +file-format = { version = "0.23.0", features = [ + "reader-asf", + "reader-ebml", + "reader-mp4", + "reader-rm", + "reader-txt", + "reader-xml", + "serde", +] } lofty = "0.18.2" serde = { version = "1.0.195", features = ["derive"] } walkdir = "2.4.0" @@ -32,7 +40,11 @@ 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" +listenbrainz = "0.7.0" +discord-rpc-client = "0.4.0" +nestify = "0.3.3" diff --git a/src/config/config.rs b/src/config/config.rs index 9314d65..1c19b15 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -1,16 +1,14 @@ use std::{ path::PathBuf, fs::{File, OpenOptions, self}, - io::{Error, Write, Read}, sync::{Arc, RwLock}, + io::{Error, Write, Read}, }; -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Serialize}; 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, @@ -71,7 +69,9 @@ impl ConfigLibraries { } pub fn get_library(&self, uuid: &Uuid) -> Result { + for library in &self.libraries { + // dbg!(&library.uuid, &uuid); if &library.uuid == uuid { return Ok(library.to_owned()) } @@ -90,11 +90,18 @@ impl ConfigLibraries { } #[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct ConfigConnections { + pub listenbrainz_token: Option +} + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +#[serde(default)] pub struct Config { pub path: PathBuf, pub backup_folder: Option, pub libraries: ConfigLibraries, pub volume: f32, + pub connections: ConfigConnections, } impl Config { @@ -124,12 +131,36 @@ impl Config { Ok(()) } + pub fn save_backup(&self) -> Result<(), Box> { + 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 { 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::(&bun)?; - Ok(ny) + let config: Config = serde_json::from_str::(&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); } } @@ -142,50 +173,56 @@ pub enum ConfigError { //TODO: do something about playlists #[error("Please provide a better m3u8 Playlist")] BadPlaylist, + #[error("No backup Config folder present")] + NoBackupLibrary, } -#[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)] +pub mod tests { + use std::{path::PathBuf, sync::{Arc, RwLock}}; + use crate::music_storage::library::MusicLibrary; + use super::{Config, ConfigLibraries, ConfigLibrary}; + + 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"), ..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(); + }; + 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(); + lib.scan_folder("test-config/music/").unwrap(); + lib.save(config.clone()).unwrap(); + + (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] + fn test3() { + let (config, lib) = read_config_lib(); + + _ = config.write_file(); + + dbg!(config); + } } - -#[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.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 mut 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 3119019..9f6bb0a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,12 +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 86d8f29..b2ceb85 100644 --- a/src/music_controller/connections.rs +++ b/src/music_controller/connections.rs @@ -1,8 +1,103 @@ -use std::{env, thread, time}; +// use std::{ +// sync::{Arc, RwLock}, +// error::Error, +// }; -use super::controller::Controller; +// 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 super::controller::DatabaseResponse; -impl Controller { - //more stuff goes here -} + +// impl Controller { +// pub fn listenbrainz_authenticate(&mut self) -> Result> { +// 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> { +// 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> { +// 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 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}; + +// 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 7f62871..2ab8811 100644 --- a/src/music_controller/controller.rs +++ b/src/music_controller/controller.rs @@ -2,25 +2,139 @@ //! player. It manages queues, playback, library access, and //! other functions +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, - music_storage::library::Song, - config::config::Config + config::config::Config, + music_controller::queue::Queue, + music_storage::library::{MusicLibrary, Song}, }; -struct Queue { - player: Player, - name: String, - songs: Vec, -} - pub struct Controller { - queues: Vec, - config: Arc>, + pub queue: Queue, + pub config: Arc>, + pub library: MusicLibrary, + player_mail: MailMan, } -impl Controller { - // more stuff to come +#[derive(Debug)] +pub(super) struct MailMan { + pub tx: Sender, + rx: Receiver, +} + +impl MailMan { + pub fn new() -> Self { + let (tx, rx) = unbounded::(); + MailMan { tx, rx } + } +} +impl MailMan { + pub fn double() -> (MailMan, MailMan) { + let (tx, rx) = unbounded::(); + let (tx1, rx1) = unbounded::(); + + (MailMan { tx, rx: rx1 }, MailMan { tx: tx1, rx }) + } + + pub fn send(&self, mail: T) -> Result<(), Box> { + self.tx.send(mail).unwrap(); + Ok(()) + } + + pub fn recv(&self) -> Result> { + let u = self.rx.recv()?; + Ok(u) + } +} + +enum PlayerCmd { + Test(URI), +} + +enum PlayerRes { + Test, +} + +#[allow(unused_variables)] +impl Controller { + pub fn start

(config_path: P) -> Result> + where + std::path::PathBuf: std::convert::From

, + { + 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 library = MusicLibrary::init(config_.clone(), uuid)?; + + let (player_mail, in_thread) = MailMan::::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, + }) + } + + 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) + } +} + +#[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)); + } } diff --git a/src/music_controller/queue.rs b/src/music_controller/queue.rs new file mode 100644 index 0000000..380d157 --- /dev/null +++ b/src/music_controller/queue.rs @@ -0,0 +1,278 @@ +use crate::music_storage::library::{MusicLibrary, Song, URI}; +use std::{ + error::Error, + sync::{Arc, RwLock}, +}; +use uuid::Uuid; + +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, + First, + AddHere, + NoState, +} + +// TODO: move this to a different location to be used elsewhere +#[derive(Debug, Clone, PartialEq)] +#[non_exhaustive] +pub enum PlayerLocation { + Test, + Library, + Playlist(Uuid), + File, + Custom, +} + +#[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, +} +impl From for QueueItem { + fn from(song: Song) -> Self { + QueueItem { + item: song, + state: QueueState::NoState, + source: PlayerLocation::Library, + by_human: false, + } + } +} + +#[derive(Debug)] +pub struct Queue { + pub items: Vec, + pub played: Vec, + pub loop_: bool, + pub shuffle: bool, +} + +impl Queue { + 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::>(), + self.items.len() + ); + } + + 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) { + let mut tracks = tracks; + self.items.clear(); + self.items.append(&mut tracks); + } + + 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::>(); + + 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: 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, 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]); + + 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; + } + self.items[remove_index].state = QueueState::NoState; + self.items.remove(remove_index); + Ok(()) + } else { + Err(QueueError::EmptyQueue) + } + } + + pub fn clear(&mut self) { + self.items.clear(); + } + + pub fn clear_except(&mut self, index: usize) -> Result<(), Box> { + 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("Queue is empty!".into()); + } else { + return Err("index out of bounds!".into()); + } + Ok(()) + } + + pub fn clear_played(&mut self) { + self.played.clear(); + } + + pub fn clear_all(&mut self) { + self.items.clear(); + self.played.clear(); + } + + // TODO: uh, fix this? + 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 && 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; + } + 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 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, a: usize, b: usize) { + let item = self.items[a].to_owned(); + if a != b { + self.items.remove(a); + } + self.items.insert(b, item); + } + + #[allow(clippy::should_implement_trait)] + pub fn next(&mut self) -> Result<&QueueItem, Box> { + 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(); + 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 check_played(&mut self) { + while self.played.len() > 50 { + self.played.remove(0); + } + } +} diff --git a/src/music_player.rs b/src/music_player/gstreamer.rs similarity index 93% rename from src/music_player.rs rename to src/music_player/gstreamer.rs index 4e7de23..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 for PlayerState { +impl From for GstState { fn from(value: gst::State) -> Self { match value { gst::State::VoidPending => Self::VoidPending, @@ -45,7 +47,7 @@ impl From for PlayerState { } } -impl TryInto for PlayerState { +impl TryInto for GstState { fn try_into(self) -> Result> { match self { Self::VoidPending => Ok(gst::State::VoidPending), @@ -60,22 +62,6 @@ impl TryInto for PlayerState { type Error = Box; } -#[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, -} - #[derive(Debug, PartialEq, Eq)] enum PlaybackStats { Idle, @@ -88,10 +74,11 @@ enum PlaybackStats { } /// An instance of a music player with a GStreamer backend -pub struct Player { +#[derive(Debug)] +pub struct GStreamer { source: Option, //pub message_tx: Sender, - pub message_rx: crossbeam::channel::Receiver, + pub message_rx: crossbeam::channel::Receiver, playback_tx: crossbeam::channel::Sender, playbin: Arc>, @@ -102,7 +89,7 @@ pub struct Player { position: Arc>>, } -impl Player { +impl GStreamer { pub fn new() -> Result { // Initialize GStreamer, maybe figure out how to nicely fail here gst::init()?; @@ -159,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 @@ -177,7 +164,7 @@ impl Player { *position_update.write().unwrap() = None; break }, - PlaybackStats::Idle | PlaybackStats::Switching => println!("waiting!"), + PlaybackStats::Idle | PlaybackStats::Switching => {}, _ => () } @@ -257,12 +244,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 +282,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 +313,8 @@ impl Player { }).unwrap(); } } + + Ok(()) } /// Gets a mutable reference to the playbin element @@ -458,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() { @@ -488,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; + + 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; + + fn duration(&mut self) -> Option; + + fn raw_duration(&self) -> Option; + + 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/db_reader/foobar/reader.rs b/src/music_storage/db_reader/foobar/reader.rs index 3cb417d..4d07e71 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; @@ -174,12 +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, rating: None, format: None, duration: self.duration, @@ -189,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 c2063c0..f6032db 100644 --- a/src/music_storage/db_reader/itunes/reader.rs +++ b/src/music_storage/db_reader/itunes/reader.rs @@ -2,6 +2,7 @@ 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}; @@ -12,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; @@ -146,7 +147,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() @@ -165,11 +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: sug, + location: vec![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), @@ -185,6 +194,7 @@ impl ExternalLibrary for ITunesLibrary { Err(_) => Vec::new(), }, tags: tags_, + internal_tags, }; // dbg!(&ny.tags); bun.push(ny); @@ -320,4 +330,29 @@ impl ITunesSong { // println!("{:.2?}", song); Ok(song) } -} \ No newline at end of file +} + +#[cfg(test)] +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 super::ITunesLibrary; + + #[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(); + } +} diff --git a/src/music_storage/library.rs b/src/music_storage/library.rs index 84fc2e0..4818324 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; @@ -5,17 +6,18 @@ 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::ops::Deref; // 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; +use std::fs::{self, File}; use std::path::{Path, PathBuf}; +use tempfile::TempDir; +use uuid::Uuid; use walkdir::WalkDir; // Time @@ -115,13 +117,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, Eq, PartialOrd, Ord)] +#[non_exhaustive] +pub enum DoNotTrack { + // TODO: add services to not track + LastFM, + LibreFM, + MusicBrainz, + Discord, +} + +#[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, + pub uuid: Uuid, pub plays: i32, pub skips: i32, pub favorited: bool, + pub banned: Option, pub rating: Option, pub format: Option, pub duration: Duration, @@ -134,6 +181,7 @@ pub struct Song { pub date_modified: Option>, pub album_art: Vec, pub tags: BTreeMap, + pub internal_tags: Vec, } impl Song { @@ -155,7 +203,7 @@ impl Song { pub fn get_field(&self, target_field: &str) -> Option { 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)), @@ -254,11 +302,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, rating: None, format, duration, @@ -268,6 +320,7 @@ impl Song { date_modified: Some(chrono::offset::Utc::now()), tags, album_art, + internal_tags, }; Ok(new_song) } @@ -287,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; } @@ -372,15 +424,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, rating: None, format, duration, @@ -390,11 +444,39 @@ impl Song { date_modified: Some(chrono::offset::Utc::now()), tags, album_art, + internal_tags: Vec::new(), }; tracks.push((new_song, audio_location.clone())); } } - Ok((tracks)) + 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 + #[allow(clippy::type_complexity)] + pub fn primary_uri(&self) -> Result<(&URI, Option>), Box> { + 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()), + } } } @@ -461,10 +543,18 @@ impl URI { path_str.to_string() } + pub fn as_path(&self) -> Result<&PathBuf, Box> { + if let Self::Local(path) = self { + Ok(path) + } else { + Err("This URI is not local!".into()) + } + } + pub fn exists(&self) -> Result { match self { URI::Local(loc) => loc.try_exists(), - URI::Cue {location, ..} => location.try_exists(), + URI::Cue { location, .. } => location.try_exists(), URI::Remote(_, _loc) => todo!(), } } @@ -489,7 +579,7 @@ pub enum Service { None, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct Album<'a> { title: &'a String, artist: Option<&'a String>, @@ -500,7 +590,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 } @@ -548,14 +638,8 @@ pub struct MusicLibrary { pub name: String, pub uuid: Uuid, pub library: Vec, -} - -#[test] -fn library_init() { - 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); + pub playlists: PlaylistFolder, + pub backup_songs: Vec, // maybe move this to the config instead? } impl MusicLibrary { @@ -565,6 +649,8 @@ impl MusicLibrary { name, uuid, library: Vec::new(), + playlists: PlaylistFolder::new(), + backup_songs: Vec::new(), } } @@ -575,13 +661,15 @@ impl MusicLibrary { /// the [MusicLibrary] Vec pub fn init(config: Arc>, uuid: Uuid) -> Result> { 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 } }; @@ -631,7 +719,30 @@ impl MusicLibrary { .par_iter() .enumerate() .try_for_each(|(i, track)| { - if path == &track.location { + for location in &track.location { + //TODO: check that this works + if path == location { + return std::ops::ControlFlow::Break((track, i)); + } + } + Continue(()) + }); + + match result { + Break(song) => Some(song), + Continue(_) => None, + } + } + + /// 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(()) @@ -648,7 +759,8 @@ impl MusicLibrary { fn query_path(&self, path: PathBuf) -> Option> { let result: Arc>> = 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); } }); @@ -660,10 +772,7 @@ impl MusicLibrary { } /// Finds all the audio files within a specified folder - pub fn scan_folder( - &mut self, - target_path: &str, - ) -> Result> { + pub fn scan_folder(&mut self, target_path: &str) -> Result> { let mut total = 0; let mut errors = 0; for target_file in WalkDir::new(target_path) @@ -726,7 +835,6 @@ impl MusicLibrary { } pub fn add_file(&mut self, target_file: &Path) -> Result<(), Box> { - let new_song = Song::from_file(target_file)?; match self.add_song(new_song) { Ok(_) => (), @@ -742,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; @@ -760,13 +867,14 @@ impl MusicLibrary { } pub fn add_song(&mut self, new_song: Song) -> Result<(), Box> { - 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()) } _ => (), } @@ -789,17 +897,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, ) -> Result<(), Box> { - 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); @@ -1014,3 +1123,40 @@ impl MusicLibrary { Ok(albums) } } + +#[cfg(test)] +mod test { + 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; + + #[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(); + let target_uuid = config.libraries.libraries[0].uuid; + let a = MusicLibrary::init(Arc::new(RwLock::from(config)), target_uuid).unwrap(); + dbg!(a); + } +} diff --git a/src/music_storage/playlist.rs b/src/music_storage/playlist.rs index c2c6b74..e7ae61b 100644 --- a/src/music_storage/playlist.rs +++ b/src/music_storage/playlist.rs @@ -1,59 +1,97 @@ -use std::{fs::File, path::{Path, PathBuf}, io::{Read, Error}}; - -use bincode::config; -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::{ - itunes::reader::ITunesLibrary, - extern_library::ExternalLibrary - }, +use std::error::Error; +use std::{ + fs::File, + io::Read, + path::PathBuf, + sync::{Arc, RwLock}, }; -use m3u8_rs::{MediaPlaylist, MediaPlaylistType, MediaSegment, Playlist as List2, MasterPlaylist}; +use std::time::Duration; -#[derive(Debug, Clone)] +// use chrono::Duration; +use super::library::{AlbumArt, MusicLibrary, Song, Tag, URI}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use m3u8_rs::{MediaPlaylist, MediaPlaylistType, MediaSegment, Playlist as List2}; +use nestify::nest; + +use rayon::prelude::*; + +#[derive(Debug, Clone, Serialize, Deserialize)] pub enum SortOrder { Manual, - Tag(Tag) + Tag(Vec), } -#[derive(Debug, Clone)] -pub struct Playlist<'a> { + +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, - cover: Option<&'a AlbumArt>, + cover: Option, tracks: Vec, sort_order: SortOrder, play_count: i32, play_time: Duration, } -impl<'a> Playlist<'a> { +impl Playlist { pub fn new() -> Self { Default::default() } 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 { + self.tracks.to_owned() + } pub fn set_tracks(&mut self, tracks: Vec) { 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 { // let mut index = 0; @@ -68,24 +106,65 @@ impl<'a> Playlist<'a> { // } // 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>, + ) -> 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) { - 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_file(&self, path: &str) -> Result<(), Box> { + super::utils::write_file(self, PathBuf::from(path))?; + Ok(()) + } + + pub fn from_file(path: &str) -> Result> { + super::utils::read_file(PathBuf::from(path)) + } + + pub fn to_m3u8( + &mut self, + lib: Arc>, + location: &str, + ) -> Result<(), Box> { + let lib = lib.read().unwrap(); + let seg = self + .tracks + .iter() + .filter_map(|uuid| { + // TODO: The Unwraps need to be handled here + if let Some((track, _)) = lib.query_uuid(uuid) { + if let URI::Local(_) = track.primary_uri().unwrap().0 { + Some(MediaSegment { + uri: track.primary_uri().unwrap().0.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::>(); @@ -100,19 +179,24 @@ 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) + .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, Error> { + + pub fn from_m3u8( + path: &str, + lib: Arc>, + ) -> Result> { 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(); @@ -120,56 +204,174 @@ impl<'a> Playlist<'a> { 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::>(); + 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) + } + } + } + + pub fn out_tracks(&self, lib: Arc>) -> (Vec, 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); } } - todo!() - } - fn title(&self) -> &String { - &self.title - } - fn cover(&self) -> Option<&AlbumArt> { - match self.cover { - Some(e) => Some(e), - None => None, + 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::(), tag_b.parse::()) { + // 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()) + }) } - } - fn tracks(&self) -> Vec { - self.tracks.to_owned() + + (songs, invalid_uuids) } } - - -impl Default for Playlist<'_> { +impl Default for Playlist { fn default() -> Self { Playlist { + uuid: Uuid::new_v4(), title: String::default(), cover: None, tracks: Vec::default(), sort_order: SortOrder::Manual, play_count: 0, - play_time: Duration::zero(), + play_time: Duration::from_secs(0), } } } -// #[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::>(); -// a.tracks.append(&mut b); -// a.to_m3u8() -// } +#[cfg(test)] +mod test_super { + use super::*; + use crate::{config::config::tests::read_config_lib, music_storage::playlist}; + + #[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", + ); + } + + 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(); + + 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); + } +} diff --git a/src/music_storage/utils.rs b/src/music_storage/utils.rs index f4665a3..c9347c4 100644 --- a/src/music_storage/utils.rs +++ b/src/music_storage/utils.rs @@ -1,15 +1,17 @@ -use std::fs::{File, self}; -use std::io::{BufReader, BufWriter}; -use std::path::{Path, PathBuf}; -use std::error::Error; - -use walkdir::WalkDir; +use deunicode::deunicode_with_tofu; use file_format::{FileFormat, Kind}; use snap; -use deunicode::deunicode_with_tofu; +use std::error::Error; +use std::fs::{self, File}; +use std::io::{BufReader, BufWriter}; +use std::path::{Path, PathBuf}; +use walkdir::WalkDir; 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, " "); @@ -23,12 +25,15 @@ 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( +pub(super) fn write_file< + T: serde::Serialize, + U: std::convert::AsRef + std::convert::AsRef + Clone, +>( library: T, - path: PathBuf, + path: U, ) -> Result<(), Box> { // 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 @@ -50,7 +55,9 @@ pub(super) fn write_file( /// Read a file serialized out with [write_file] and turn it into /// the desired structure -pub(super) fn read_file serde::Deserialize<'de>>(path: PathBuf) -> Result> { +pub(super) fn read_file serde::Deserialize<'de>>( + path: PathBuf, +) -> Result> { // 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); @@ -74,11 +81,12 @@ pub fn find_images(song_path: &Path) -> Result, Box> { .follow_links(true) .into_iter() .filter_map(|e| e.ok()) - .filter(|e| e.depth() < 3) // Don't recurse very deep + .filter(|e| e.depth() < 3) + // Don't recurse very deep { println!("{:?}", target_file); let path = target_file.path(); - if !path.is_file() { + if !path.is_file() || !path.exists() { continue; } @@ -87,6 +95,11 @@ pub fn find_images(song_path: &Path) -> Result, Box> { continue; } + #[cfg(target_family = "windows")] + if (4 & path.metadata().unwrap().file_attributes()) == 4 { + continue; + } + let image_uri = URI::Local(path.to_path_buf().canonicalize()?); images.push(AlbumArt::External(image_uri));