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>, // disc #, track # current: (i32, i32) }, + Playlist { + uuid: Uuid, + shuffled: bool, + order: Option>, + current: Uuid + }, None, Test } + impl QueueItemType<'_> { fn get_uri(&self, lib: Arc>) -> Option { 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>, + pub played: Vec>, + 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::>(), self.items.len()); } - pub fn new() -> Result> { + + pub fn new() -> Result { 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 { - let mut e = self.items.iter().filter(|song| song.state == QueueState::Played ).collect::>().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::>().is_empty() - } - - fn is_empty(&self) -> bool { - self.items.iter().filter(|item| item.state != QueueState::Played).collect::>().is_empty() - } - 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: QueueItemType<'a>, source: QueueSource, by_human: bool) -> Result<(), Box> { - 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::>(); - 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> { - 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, 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> { - 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> { - 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>) -> Result> { + + + + 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, } -#[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> { + pub fn open_album_art(&self, index: usize, temp_dir: &TempDir) -> Result<(), Box> { 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> { + 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(), @@ -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