From e1a0ffbd05279433c9c26c05a16cfe8ca7b27596 Mon Sep 17 00:00:00 2001 From: MrDulfin Date: Sun, 29 Dec 2024 00:57:51 -0500 Subject: [PATCH] Optimised the album art display --- dmp-core/src/lib.rs | 35 +- dmp-core/src/music_controller/connections.rs | 100 -- dmp-core/src/music_controller/controller.rs | 1283 +++++++++--------- src-tauri/src/commands.rs | 114 +- src-tauri/src/lib.rs | 432 +++--- 5 files changed, 949 insertions(+), 1015 deletions(-) delete mode 100644 dmp-core/src/music_controller/connections.rs diff --git a/dmp-core/src/lib.rs b/dmp-core/src/lib.rs index eea4dd5..9731980 100644 --- a/dmp-core/src/lib.rs +++ b/dmp-core/src/lib.rs @@ -1,18 +1,17 @@ -pub mod music_storage { - pub mod library; - pub mod music_collection; - pub mod playlist; - mod utils; - - #[allow(dead_code)] - pub mod db_reader; -} - -pub mod music_controller { - pub mod connections; - pub mod controller; - pub mod controller_handle; - pub mod queue; -} - -pub mod config; +pub mod music_storage { + pub mod library; + pub mod music_collection; + pub mod playlist; + mod utils; + + #[allow(dead_code)] + pub mod db_reader; +} + +pub mod music_controller { + pub mod controller; + pub mod controller_handle; + pub mod queue; +} + +pub mod config; diff --git a/dmp-core/src/music_controller/connections.rs b/dmp-core/src/music_controller/connections.rs deleted file mode 100644 index 5ea823e..0000000 --- a/dmp-core/src/music_controller/connections.rs +++ /dev/null @@ -1,100 +0,0 @@ -// use std::{ -// sync::{Arc, RwLock}, -// error::Error, -// }; - -// 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 { -// 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/dmp-core/src/music_controller/controller.rs b/dmp-core/src/music_controller/controller.rs index 85b8c92..9bd01e5 100644 --- a/dmp-core/src/music_controller/controller.rs +++ b/dmp-core/src/music_controller/controller.rs @@ -1,630 +1,653 @@ -//! The [Controller] is the input and output for the entire -//! player. It manages queues, playback, library access, and -//! other functions -#![allow(while_true)] - -use kushi::{Queue, QueueItemType}; -use kushi::{QueueError, QueueItem}; -use prismriver::{Prismriver, Volume, Error as PrismError}; -use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator}; -use serde::{Deserialize, Serialize}; -use serde_json::to_string_pretty; -use std::error::Error; -use std::fs::OpenOptions; -use std::io::Write; -use std::path::{Path, PathBuf}; -use std::sync::{Arc, RwLock}; -use thiserror::Error; -use uuid::Uuid; - -use crate::config::ConfigError; -use crate::music_storage::library::Song; -use crate::music_storage::playlist::{ExternalPlaylist, Playlist, PlaylistFolderItem}; -use crate::{config::Config, music_storage::library::MusicLibrary}; - -use super::queue::{QueueAlbum, QueueSong}; - -pub struct Controller(); - -type QueueItem_ = QueueItem; - -#[derive(Error, Debug)] -pub enum ControllerError { - #[error("{0:?}")] - QueueError(#[from] QueueError), - #[error("{0:?}")] - PlayerError(#[from] prismriver::Error), - #[error("{0:?}")] - ConfigError(#[from] ConfigError), -} - -// TODO: move this to a different location to be used elsewhere -#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize, Deserialize)] -#[non_exhaustive] -pub enum PlayerLocation { - Test, - Library, - Playlist(Uuid), - File, - Custom, -} - -#[derive(Debug, Clone)] -pub struct MailMan { - tx: async_channel::Sender, - rx: async_channel::Receiver, -} - -impl MailMan { - pub fn double() -> (MailMan, MailMan) { - let (tx, rx) = async_channel::unbounded::(); - let (tx1, rx1) = async_channel::unbounded::(); - - (MailMan { tx, rx: rx1 }, MailMan { tx: tx1, rx }) - } - - pub async fn send(&self, mail: Tx) -> Result<(), async_channel::SendError> { - self.tx.send(mail).await - } - - pub async fn recv(&self) -> Result { - self.rx.recv().await - } -} - -#[derive(Debug, PartialEq, PartialOrd, Clone)] -pub enum PlayerCommand { - NextSong, - PrevSong, - Pause, - Play, - Enqueue(usize), - SetVolume(f32), - PlayNow(Uuid, PlayerLocation), -} - -#[derive(Debug, PartialEq, Clone)] -pub enum PlayerResponse { - Empty(Result<(), PlayerError>), - NowPlaying(Result) -} - -#[derive(Error, Debug, PartialEq, Clone)] -pub enum PlayerError { - #[error("{0}")] - QueueError(#[from] QueueError), - #[error("{0}")] - Prismriver(#[from] PrismError), -} - -#[derive(Debug, PartialEq, PartialOrd, Clone)] -pub enum LibraryCommand { - Song(Uuid), - AllSongs, - GetLibrary, - ExternalPlaylist(Uuid), - Playlist(Uuid), - ImportM3UPlayList(PathBuf), - Save, - Playlists, -} - -#[derive(Debug, Clone)] -pub enum LibraryResponse { - Ok, - Song(Song, usize), - AllSongs(Vec), - Library(MusicLibrary), - ExternalPlaylist(ExternalPlaylist), - Playlist(Playlist), - ImportM3UPlayList(Uuid, String), - Playlists(Vec<(Uuid, String)>), -} - -#[derive(Debug, PartialEq, Clone)] -pub enum QueueCommand { - Append(QueueItem_, bool), - Next, - Prev, - GetIndex(usize), - NowPlaying, - Get, - Clear, - Remove(usize), -} - -#[derive(Debug, PartialEq, Clone)] -pub enum QueueResponse { - Empty(Result<(), QueueError>), - Item(Result), - GetAll(Vec), -} - - -pub struct ControllerInput { - player_mail: ( - MailMan, - MailMan, - ), - lib_mail: ( - MailMan, - MailMan - ), - queue_mail: ( - MailMan, - MailMan - ), - library: MusicLibrary, - config: Arc>, -} - -pub struct ControllerHandle { - pub lib_mail: MailMan, - pub player_mail: MailMan, - pub queue_mail: MailMan, -} - -impl ControllerHandle { - pub fn new(library: MusicLibrary, config: Arc>) -> (Self, ControllerInput) { - let lib_mail = MailMan::double(); - let player_mail = MailMan::double(); - let queue_mail = MailMan::double(); - - ( - ControllerHandle { - lib_mail: lib_mail.0.clone(), - player_mail: player_mail.0.clone(), - queue_mail: queue_mail.0.clone() - }, - ControllerInput { - player_mail, - lib_mail, - queue_mail, - library, - config - } - ) - } -} - -#[derive(Debug, Default, Serialize, Deserialize, Clone)] -pub struct ControllerState { - path: PathBuf, - volume: f32, - now_playing: Uuid, -} - -impl ControllerState { - fn new(path: PathBuf) -> Self { - ControllerState { - path, - volume: 0.35, - ..Default::default() - } - } - - fn write_file(&self) -> Result<(), std::io::Error> { - OpenOptions::new() - .truncate(true) - .create(true) - .write(true) - .open(&self.path) - .unwrap() - .write_all(&to_string_pretty(self)?.into_bytes())?; - Ok(()) - } - - fn read_file(path: impl AsRef) -> Result { - let state = serde_json::from_str(&std::fs::read_to_string(path)?)?; - Ok(state) - } -} - -#[allow(unused_variables)] -impl Controller { - pub async fn start( - ControllerInput { - player_mail, - lib_mail, - queue_mail, - mut library, - config - }: ControllerInput - ) -> Result<(), Box> { - let queue: Queue = Queue { - items: Vec::new(), - played: Vec::new(), - loop_: false, - shuffle: None, - }; - - let state = { - let path = &config.read().unwrap().state_path; - if let Ok(state) = ControllerState::read_file(path) { - state - } else { - ControllerState::new(path.clone()) - } - }; - - std::thread::scope(|scope| { - let queue_mail = queue_mail; - let a = scope.spawn(|| { - futures::executor::block_on(async { - moro::async_scope!(|scope| { - println!("async scope created"); - let player = Arc::new(RwLock::new(Prismriver::new())); - - let _player = player.clone(); - let _lib_mail = lib_mail.0.clone(); - scope - .spawn(async move { - Controller::player_command_loop( - _player, - player_mail.1, - queue_mail.0, - _lib_mail, - state, - ) - .await - .unwrap(); - }); - scope - .spawn(async move { - Controller::player_event_loop( - player, - player_mail.0 - ) - .await - .unwrap(); - }); - scope - .spawn(async { - Controller::library_loop( - lib_mail.1, - &mut library, - config, - ) - .await - .unwrap(); - }); - }) - .await; - }) - }); - - let b = scope.spawn(|| { - futures::executor::block_on(async { - Controller::queue_loop(queue, queue_mail.1).await; - }) - }); - a.join().unwrap(); - b.join().unwrap(); - }); - - Ok(()) - } - - async fn player_command_loop( - player: Arc>, - player_mail: MailMan, - queue_mail: MailMan, - lib_mail: MailMan, - mut state: ControllerState, - ) -> Result<(), ()> { - player.write().unwrap().set_volume(Volume::new(state.volume)); - println!("volume set to {}", state.volume); - 'outer: while true { - let _mail = player_mail.recv().await; - if let Ok(mail) = _mail { - match mail { - PlayerCommand::Play => { - player.write().unwrap().play(); - player_mail.send(PlayerResponse::Empty(Ok(()))).await.unwrap(); - } - - PlayerCommand::Pause => { - player.write().unwrap().pause(); - player_mail.send(PlayerResponse::Empty(Ok(()))).await.unwrap(); - } - - PlayerCommand::SetVolume(volume) => { - player.write().unwrap().set_volume(Volume::new(volume)); - println!("volume set to {volume}"); - player_mail.send(PlayerResponse::Empty(Ok(()))).await.unwrap(); - - state.volume = volume; - _ = state.write_file() - } - - PlayerCommand::NextSong => { - queue_mail.send(QueueCommand::Next).await.unwrap(); - - match queue_mail.recv().await.unwrap() { - QueueResponse::Item(Ok(item)) => { - let uri = match &item.item { - QueueItemType::Single(song) => song.song.primary_uri().unwrap().0, - _ => unimplemented!(), - }; - - let prism_uri = prismriver::utils::path_to_uri(&uri.as_path().unwrap()).unwrap(); - println!("Playing song at path: {:?}", prism_uri); - - // handle error here for unknown formats - player.write().unwrap().load_new(&prism_uri).unwrap(); - player.write().unwrap().play(); - - let QueueItemType::Single(np_song) = item.item else { panic!("This is temporary, handle queueItemTypes at some point")}; - - // Append next song in library - lib_mail.send(LibraryCommand::AllSongs).await.unwrap(); - let LibraryResponse::AllSongs(songs) = lib_mail.recv().await.unwrap() else { - continue; - }; - lib_mail.send(LibraryCommand::Song(np_song.song.uuid)).await.unwrap(); - let LibraryResponse::Song(_, i) = lib_mail.recv().await.unwrap() else { - unreachable!() - }; - if let Some(song) = songs.get(i + 49) { - queue_mail.send( - QueueCommand::Append( - QueueItem::from_item_type( - QueueItemType::Single( - QueueSong { - song: song.clone(), - location: np_song.location - } - ) - ), - false - ) - ).await - .unwrap(); - let QueueResponse::Empty(Ok(())) = queue_mail.recv().await.unwrap() else { - unreachable!() - }; - } else { - println!("Library Empty"); - } - - player_mail.send(PlayerResponse::NowPlaying(Ok(np_song.song.clone()))).await.unwrap(); - } QueueResponse::Item(Err(e)) => { - player_mail.send(PlayerResponse::NowPlaying(Err(e.into()))).await.unwrap(); - } - _ => continue - } - } - - PlayerCommand::PrevSong => { - queue_mail.send(QueueCommand::Prev).await.unwrap(); - match queue_mail.recv().await.unwrap() { - QueueResponse::Item(Ok(item)) => { - let uri = match &item.item { - QueueItemType::Single(song) => song.song.primary_uri().unwrap().0, - _ => unimplemented!(), - }; - - let prism_uri = prismriver::utils::path_to_uri(&uri.as_path().unwrap()).unwrap(); - player.write().unwrap().load_new(&prism_uri).unwrap(); - player.write().unwrap().play(); - - let QueueItemType::Single(np_song) = item.item else { panic!("This is temporary, handle queueItemTypes at some point")}; - player_mail.send(PlayerResponse::NowPlaying(Ok(np_song.song.clone()))).await.unwrap(); - } - QueueResponse::Item(Err(e)) => { - player_mail.send(PlayerResponse::NowPlaying(Err(e.into()))).await.unwrap(); - } - _ => continue - } - } - - PlayerCommand::Enqueue(index) => { - queue_mail - .send(QueueCommand::GetIndex(index)) - .await - .unwrap(); - match queue_mail.recv().await.unwrap() { - QueueResponse::Item(Ok(item)) => { - match item.item { - QueueItemType::Single(song) => { - let prism_uri = prismriver::utils::path_to_uri(&song.song.primary_uri().unwrap().0.as_path().unwrap()).unwrap(); - player.write().unwrap().load_new(&prism_uri).unwrap(); - player.write().unwrap().play(); - } - _ => unimplemented!(), - } - player_mail.send(PlayerResponse::Empty(Ok(()))).await.unwrap(); - } - QueueResponse::Item(Err(e)) => { - player_mail.send(PlayerResponse::Empty(Err(e.into()))).await.unwrap(); - } - _ => continue - } - } - - PlayerCommand::PlayNow(uuid, location) => { - // TODO: This assumes the uuid doesn't point to an album. we've been over this. - lib_mail.send(LibraryCommand::Song(uuid)).await.unwrap(); - let LibraryResponse::Song(song, index) = lib_mail.recv().await.unwrap() else { - unreachable!() - }; - queue_mail.send(QueueCommand::Clear).await.unwrap(); - match queue_mail.recv().await.unwrap() { - QueueResponse::Empty(Ok(())) => (), - QueueResponse::Empty(Err(e)) => { - player_mail.send(PlayerResponse::NowPlaying(Err(e.into()))).await.unwrap(); - continue; - } - _ => unreachable!() - } - queue_mail.send(QueueCommand::Append(QueueItem::from_item_type(QueueItemType::Single(QueueSong { song: song.clone(), location })), true)).await.unwrap(); - match queue_mail.recv().await.unwrap() { - QueueResponse::Empty(Ok(())) => (), - QueueResponse::Empty(Err(e)) => { - player_mail.send(PlayerResponse::NowPlaying(Err(e.into()))).await.unwrap(); - continue; - } - _ => unreachable!() - } - - // TODO: Handle non Local URIs here, and whenever `load_new()` or `load_gapless()` is called - let prism_uri = prismriver::utils::path_to_uri(&song.primary_uri().unwrap().0.as_path().unwrap()).unwrap(); - player.write().unwrap().load_new(&prism_uri).unwrap(); - player.write().unwrap().play(); - - // how grab all the songs in a certain subset of the library, I reckon? - // ... - // let's just pretend I figured that out already - - let (songs, index) = match location { - PlayerLocation::Library => { - lib_mail.send(LibraryCommand::AllSongs).await.unwrap(); - let LibraryResponse::AllSongs(songs) = lib_mail.recv().await.unwrap() else { - unreachable!() - }; - (songs, index) - } - PlayerLocation::Playlist(uuid) => { - lib_mail.send(LibraryCommand::ExternalPlaylist(uuid)).await.unwrap(); - let LibraryResponse::ExternalPlaylist(list) = lib_mail.recv().await.unwrap() else { - unreachable!() - }; - let index = list.get_index(song.uuid).unwrap(); - (list.tracks, index) - } - _ => todo!("Got Location other than Library or Playlist") - }; - - - for i in index+1..(index+50) { - if let Some(song) = songs.get(i) { - queue_mail.send(QueueCommand::Append(QueueItem::from_item_type(QueueItemType::Single(QueueSong { song: song.clone(), location })), false)).await.unwrap(); - match queue_mail.recv().await.unwrap() { - QueueResponse::Empty(Ok(())) => (), - QueueResponse::Empty(Err(e)) => { - player_mail.send(PlayerResponse::NowPlaying(Err(e.into()))).await.unwrap(); - continue 'outer; - } - _ => unreachable!() - } - } else { - println!("End of Library / Playlist"); - break; - } - } - // ^ This be my solution for now ^ - player_mail.send(PlayerResponse::NowPlaying(Ok(song.clone()))).await.unwrap(); - } - } - } else { - return Err(()); - } - } - Ok(()) - } - - async fn library_loop( - lib_mail: MailMan, - library: &mut MusicLibrary, - config: Arc>, - ) -> Result<(), ()> { - while true { - match lib_mail.recv().await.unwrap() { - LibraryCommand::Song(uuid) => { - let (song, i) = library.query_uuid(&uuid).unwrap(); - lib_mail.send(LibraryResponse::Song(song.clone(), i)).await.unwrap(); - } - LibraryCommand::AllSongs => { - lib_mail.send(LibraryResponse::AllSongs(library.library.clone())).await.unwrap(); - }, - LibraryCommand::ExternalPlaylist(uuid) => { - let playlist = library.query_playlist_uuid(&uuid).unwrap(); - lib_mail.send(LibraryResponse::ExternalPlaylist(ExternalPlaylist::from_playlist(playlist, library))).await.unwrap(); - } - LibraryCommand::ImportM3UPlayList(path) => { - let playlist = Playlist::from_m3u(path, library).unwrap(); - let uuid = playlist.uuid; - let name = playlist.title.clone(); - library.playlists.items.push(PlaylistFolderItem::List(playlist)); - - lib_mail.send(LibraryResponse::ImportM3UPlayList(uuid, name)).await.unwrap(); - } - LibraryCommand::Save => { - library.save(config.read().unwrap().libraries.get_library(&library.uuid).unwrap().path.clone()).unwrap(); - lib_mail.send(LibraryResponse::Ok).await.unwrap(); - } - LibraryCommand::Playlists => { - let mut lists = vec![]; - library.playlists.lists_recursive().into_par_iter().map(|list| (list.uuid, list.title.clone())).collect_into_vec(&mut lists); - - lib_mail.send(LibraryResponse::Playlists(lists)).await.unwrap(); - } - _ => { todo!() } - } - } - Ok(()) - } - - async fn player_event_loop( - player: Arc>, - player_mail: MailMan, - ) -> Result<(), ()> { - // just pretend this does something - Ok(()) - } - - async fn queue_loop( - mut queue: Queue, - queue_mail: MailMan, - ) { - while true { - match queue_mail.recv().await.unwrap() { - QueueCommand::Append(item, by_human) => { - match item.item { - QueueItemType::Single(song) => queue.add_item(song, by_human), - _ => unimplemented!(), - } - queue_mail - .send(QueueResponse::Empty(Ok(()))) - .await - .unwrap(); - }, - QueueCommand::Next => { - let next = queue.next().map_or( Err(QueueError::NoNext), |s| Ok(s.clone())); - queue_mail - .send(QueueResponse::Item(next.clone())) - .await - .unwrap(); - } - QueueCommand::Prev => { - let prev = queue.prev().map_or( Err(QueueError::EmptyPlayed), |s| Ok(s.clone())); - queue_mail - .send(QueueResponse::Item(prev.clone())) - .await - .unwrap(); - } - QueueCommand::GetIndex(index) => { - let item = queue.items.get(index).map_or( Err(QueueError::OutOfBounds { index, len: queue.items.len() }), |s| Ok(s.clone())); - queue_mail.send(QueueResponse::Item(item)).await.unwrap(); - } - QueueCommand::NowPlaying => { - let item = queue.current().map(|t| t.clone()); - queue_mail - .send(QueueResponse::Item(item)) - .await - .unwrap(); - } - QueueCommand::Get => { - queue_mail.send(QueueResponse::GetAll(queue.items.clone())).await.unwrap(); - } - QueueCommand::Clear => { - queue.clear(); - queue_mail.send(QueueResponse::Empty(Ok(()))).await.unwrap(); - } - QueueCommand::Remove(index) => { - queue_mail.send(QueueResponse::Item(queue.remove_item(index))).await.unwrap(); - } - } - } - } -} +//! The [Controller] is the input and output for the entire +//! player. It manages queues, playback, library access, and +//! other functions +#![allow(while_true)] + +use chrono::TimeDelta; +use kushi::{Queue, QueueItemType}; +use kushi::{QueueError, QueueItem}; +use prismriver::{Prismriver, Volume, Error as PrismError}; +use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator}; +use serde::{Deserialize, Serialize}; +use serde_json::to_string_pretty; +use std::error::Error; +use std::fs::OpenOptions; +use std::io::Write; +use std::path::{Path, PathBuf}; +use std::sync::{Arc, RwLock}; +use std::time::Duration; +use thiserror::Error; +use uuid::Uuid; + +use crate::config::ConfigError; +use crate::music_storage::library::Song; +use crate::music_storage::playlist::{ExternalPlaylist, Playlist, PlaylistFolderItem}; +use crate::{config::Config, music_storage::library::MusicLibrary}; + +use super::queue::{QueueAlbum, QueueSong}; + +pub struct Controller(); + +type QueueItem_ = QueueItem; + +#[derive(Error, Debug)] +pub enum ControllerError { + #[error("{0:?}")] + QueueError(#[from] QueueError), + #[error("{0:?}")] + PlayerError(#[from] prismriver::Error), + #[error("{0:?}")] + ConfigError(#[from] ConfigError), +} + +// TODO: move this to a different location to be used elsewhere +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize, Deserialize)] +#[non_exhaustive] +pub enum PlayerLocation { + Test, + Library, + Playlist(Uuid), + File, + Custom, +} + +#[derive(Debug, Clone)] +pub struct MailMan { + tx: async_channel::Sender, + rx: async_channel::Receiver, +} + +impl MailMan { + pub fn double() -> (MailMan, MailMan) { + let (tx, rx) = async_channel::unbounded::(); + let (tx1, rx1) = async_channel::unbounded::(); + + (MailMan { tx, rx: rx1 }, MailMan { tx: tx1, rx }) + } + + pub async fn send(&self, mail: Tx) -> Result<(), async_channel::SendError> { + self.tx.send(mail).await + } + + pub async fn recv(&self) -> Result { + self.rx.recv().await + } +} + +#[derive(Debug, PartialEq, PartialOrd, Clone)] +pub enum PlayerCommand { + NextSong, + PrevSong, + Pause, + Play, + Enqueue(usize), + SetVolume(f32), + PlayNow(Uuid, PlayerLocation), +} + +#[derive(Debug, PartialEq, Clone)] +pub enum PlayerResponse { + Empty(Result<(), PlayerError>), + NowPlaying(Result) +} + +#[derive(Error, Debug, PartialEq, Clone)] +pub enum PlayerError { + #[error("{0}")] + QueueError(#[from] QueueError), + #[error("{0}")] + Prismriver(#[from] PrismError), +} + +#[derive(Debug, PartialEq, PartialOrd, Clone)] +pub enum LibraryCommand { + Song(Uuid), + AllSongs, + GetLibrary, + ExternalPlaylist(Uuid), + Playlist(Uuid), + ImportM3UPlayList(PathBuf), + Save, + Playlists, +} + +#[derive(Debug, Clone)] +pub enum LibraryResponse { + Ok, + Song(Song, usize), + AllSongs(Vec), + Library(MusicLibrary), + ExternalPlaylist(ExternalPlaylist), + Playlist(Playlist), + ImportM3UPlayList(Uuid, String), + Playlists(Vec<(Uuid, String)>), +} + +#[derive(Debug, PartialEq, Clone)] +pub enum QueueCommand { + Append(QueueItem_, bool), + Next, + Prev, + GetIndex(usize), + NowPlaying, + Get, + Clear, + Remove(usize), +} + +#[derive(Debug, PartialEq, Clone)] +pub enum QueueResponse { + Empty(Result<(), QueueError>), + Item(Result), + GetAll(Vec), +} + + +pub struct ControllerInput { + player_mail: ( + MailMan, + MailMan, + ), + lib_mail: ( + MailMan, + MailMan + ), + queue_mail: ( + MailMan, + MailMan + ), + library: MusicLibrary, + config: Arc>, + playback_info: crossbeam::channel::Sender, +} + +pub struct ControllerHandle { + pub lib_mail: MailMan, + pub player_mail: MailMan, + pub queue_mail: MailMan, +} + +impl ControllerHandle { + pub fn new(library: MusicLibrary, config: Arc>) -> (Self, ControllerInput, crossbeam::channel::Receiver) { + let lib_mail = MailMan::double(); + let player_mail = MailMan::double(); + let queue_mail = MailMan::double(); + let (playback_info_rx, playback_info_tx) = crossbeam::channel::bounded(1); + ( + ControllerHandle { + lib_mail: lib_mail.0.clone(), + player_mail: player_mail.0.clone(), + queue_mail: queue_mail.0.clone() + }, + ControllerInput { + player_mail, + lib_mail, + queue_mail, + library, + config, + playback_info: playback_info_rx, + }, + playback_info_tx, + ) + } +} + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct ControllerState { + path: PathBuf, + volume: f32, + now_playing: Uuid, +} + +impl ControllerState { + fn new(path: PathBuf) -> Self { + ControllerState { + path, + volume: 0.35, + ..Default::default() + } + } + + fn write_file(&self) -> Result<(), std::io::Error> { + OpenOptions::new() + .truncate(true) + .create(true) + .write(true) + .open(&self.path) + .unwrap() + .write_all(&to_string_pretty(self)?.into_bytes())?; + Ok(()) + } + + fn read_file(path: impl AsRef) -> Result { + let state = serde_json::from_str(&std::fs::read_to_string(path)?)?; + Ok(state) + } +} + +#[allow(unused_variables)] +impl Controller { + pub async fn start( + ControllerInput { + player_mail, + lib_mail, + queue_mail, + mut library, + config, + playback_info, + }: ControllerInput + ) -> Result<(), Box> { + let queue: Queue = Queue { + items: Vec::new(), + played: Vec::new(), + loop_: false, + shuffle: None, + }; + + let state = { + let path = &config.read().unwrap().state_path; + if let Ok(state) = ControllerState::read_file(path) { + state + } else { + ControllerState::new(path.clone()) + } + }; + + std::thread::scope(|scope| { + let queue_mail = queue_mail; + let a = scope.spawn(|| { + futures::executor::block_on(async { + moro::async_scope!(|scope| { + println!("async scope created"); + let player = Arc::new(RwLock::new(Prismriver::new())); + + let _player = player.clone(); + let _lib_mail = lib_mail.0.clone(); + let _queue_mail = queue_mail.0.clone(); + scope + .spawn(async move { + Controller::player_command_loop( + _player, + player_mail.1, + _queue_mail, + _lib_mail, + state, + ) + .await + .unwrap(); + }); + scope + .spawn(async move { + Controller::player_monitor_loop( + player, + player_mail.0, + queue_mail.0, + playback_info, + ) + .await + .unwrap(); + }); + scope + .spawn(async { + Controller::library_loop( + lib_mail.1, + &mut library, + config, + ) + .await + .unwrap(); + }); + }) + .await; + }) + }); + + let b = scope.spawn(|| { + futures::executor::block_on(async { + Controller::queue_loop(queue, queue_mail.1).await; + }) + }); + a.join().unwrap(); + b.join().unwrap(); + }); + + Ok(()) + } + + async fn player_command_loop( + player: Arc>, + player_mail: MailMan, + queue_mail: MailMan, + lib_mail: MailMan, + mut state: ControllerState, + ) -> Result<(), ()> { + player.write().unwrap().set_volume(Volume::new(state.volume)); + println!("volume set to {}", state.volume); + 'outer: while true { + let _mail = player_mail.recv().await; + if let Ok(mail) = _mail { + match mail { + PlayerCommand::Play => { + player.write().unwrap().play(); + player_mail.send(PlayerResponse::Empty(Ok(()))).await.unwrap(); + } + + PlayerCommand::Pause => { + player.write().unwrap().pause(); + player_mail.send(PlayerResponse::Empty(Ok(()))).await.unwrap(); + } + + PlayerCommand::SetVolume(volume) => { + player.write().unwrap().set_volume(Volume::new(volume)); + println!("volume set to {volume}"); + player_mail.send(PlayerResponse::Empty(Ok(()))).await.unwrap(); + + state.volume = volume; + _ = state.write_file() + } + + PlayerCommand::NextSong => { + queue_mail.send(QueueCommand::Next).await.unwrap(); + + match queue_mail.recv().await.unwrap() { + QueueResponse::Item(Ok(item)) => { + let uri = match &item.item { + QueueItemType::Single(song) => song.song.primary_uri().unwrap().0, + _ => unimplemented!(), + }; + + let prism_uri = prismriver::utils::path_to_uri(&uri.as_path().unwrap()).unwrap(); + println!("Playing song at path: {:?}", prism_uri); + + // handle error here for unknown formats + player.write().unwrap().load_new(&prism_uri).unwrap(); + player.write().unwrap().play(); + + let QueueItemType::Single(np_song) = item.item else { panic!("This is temporary, handle queueItemTypes at some point")}; + + // Append next song in library + lib_mail.send(LibraryCommand::AllSongs).await.unwrap(); + let LibraryResponse::AllSongs(songs) = lib_mail.recv().await.unwrap() else { + continue; + }; + lib_mail.send(LibraryCommand::Song(np_song.song.uuid)).await.unwrap(); + let LibraryResponse::Song(_, i) = lib_mail.recv().await.unwrap() else { + unreachable!() + }; + if let Some(song) = songs.get(i + 49) { + queue_mail.send( + QueueCommand::Append( + QueueItem::from_item_type( + QueueItemType::Single( + QueueSong { + song: song.clone(), + location: np_song.location + } + ) + ), + false + ) + ).await + .unwrap(); + let QueueResponse::Empty(Ok(())) = queue_mail.recv().await.unwrap() else { + unreachable!() + }; + } else { + println!("Library Empty"); + } + + player_mail.send(PlayerResponse::NowPlaying(Ok(np_song.song.clone()))).await.unwrap(); + } QueueResponse::Item(Err(e)) => { + player_mail.send(PlayerResponse::NowPlaying(Err(e.into()))).await.unwrap(); + } + _ => continue + } + } + + PlayerCommand::PrevSong => { + queue_mail.send(QueueCommand::Prev).await.unwrap(); + match queue_mail.recv().await.unwrap() { + QueueResponse::Item(Ok(item)) => { + let uri = match &item.item { + QueueItemType::Single(song) => song.song.primary_uri().unwrap().0, + _ => unimplemented!(), + }; + + let prism_uri = prismriver::utils::path_to_uri(&uri.as_path().unwrap()).unwrap(); + player.write().unwrap().load_new(&prism_uri).unwrap(); + player.write().unwrap().play(); + + let QueueItemType::Single(np_song) = item.item else { panic!("This is temporary, handle queueItemTypes at some point")}; + player_mail.send(PlayerResponse::NowPlaying(Ok(np_song.song.clone()))).await.unwrap(); + } + QueueResponse::Item(Err(e)) => { + player_mail.send(PlayerResponse::NowPlaying(Err(e.into()))).await.unwrap(); + } + _ => continue + } + } + + PlayerCommand::Enqueue(index) => { + queue_mail + .send(QueueCommand::GetIndex(index)) + .await + .unwrap(); + match queue_mail.recv().await.unwrap() { + QueueResponse::Item(Ok(item)) => { + match item.item { + QueueItemType::Single(song) => { + let prism_uri = prismriver::utils::path_to_uri(&song.song.primary_uri().unwrap().0.as_path().unwrap()).unwrap(); + player.write().unwrap().load_new(&prism_uri).unwrap(); + player.write().unwrap().play(); + } + _ => unimplemented!(), + } + player_mail.send(PlayerResponse::Empty(Ok(()))).await.unwrap(); + } + QueueResponse::Item(Err(e)) => { + player_mail.send(PlayerResponse::Empty(Err(e.into()))).await.unwrap(); + } + _ => continue + } + } + + PlayerCommand::PlayNow(uuid, location) => { + // TODO: This assumes the uuid doesn't point to an album. we've been over this. + lib_mail.send(LibraryCommand::Song(uuid)).await.unwrap(); + let LibraryResponse::Song(song, index) = lib_mail.recv().await.unwrap() else { + unreachable!() + }; + queue_mail.send(QueueCommand::Clear).await.unwrap(); + match queue_mail.recv().await.unwrap() { + QueueResponse::Empty(Ok(())) => (), + QueueResponse::Empty(Err(e)) => { + player_mail.send(PlayerResponse::NowPlaying(Err(e.into()))).await.unwrap(); + continue; + } + _ => unreachable!() + } + queue_mail.send(QueueCommand::Append(QueueItem::from_item_type(QueueItemType::Single(QueueSong { song: song.clone(), location })), true)).await.unwrap(); + match queue_mail.recv().await.unwrap() { + QueueResponse::Empty(Ok(())) => (), + QueueResponse::Empty(Err(e)) => { + player_mail.send(PlayerResponse::NowPlaying(Err(e.into()))).await.unwrap(); + continue; + } + _ => unreachable!() + } + + // TODO: Handle non Local URIs here, and whenever `load_new()` or `load_gapless()` is called + let prism_uri = prismriver::utils::path_to_uri(&song.primary_uri().unwrap().0.as_path().unwrap()).unwrap(); + player.write().unwrap().load_new(&prism_uri).unwrap(); + player.write().unwrap().play(); + + // how grab all the songs in a certain subset of the library, I reckon? + // ... + // let's just pretend I figured that out already + + let (songs, index) = match location { + PlayerLocation::Library => { + lib_mail.send(LibraryCommand::AllSongs).await.unwrap(); + let LibraryResponse::AllSongs(songs) = lib_mail.recv().await.unwrap() else { + unreachable!() + }; + (songs, index) + } + PlayerLocation::Playlist(uuid) => { + lib_mail.send(LibraryCommand::ExternalPlaylist(uuid)).await.unwrap(); + let LibraryResponse::ExternalPlaylist(list) = lib_mail.recv().await.unwrap() else { + unreachable!() + }; + let index = list.get_index(song.uuid).unwrap(); + (list.tracks, index) + } + _ => todo!("Got Location other than Library or Playlist") + }; + + + for i in index+1..(index+50) { + if let Some(song) = songs.get(i) { + queue_mail.send(QueueCommand::Append(QueueItem::from_item_type(QueueItemType::Single(QueueSong { song: song.clone(), location })), false)).await.unwrap(); + match queue_mail.recv().await.unwrap() { + QueueResponse::Empty(Ok(())) => (), + QueueResponse::Empty(Err(e)) => { + player_mail.send(PlayerResponse::NowPlaying(Err(e.into()))).await.unwrap(); + continue 'outer; + } + _ => unreachable!() + } + } else { + println!("End of Library / Playlist"); + break; + } + } + // ^ This be my solution for now ^ + player_mail.send(PlayerResponse::NowPlaying(Ok(song.clone()))).await.unwrap(); + } + } + } else { + return Err(()); + } + } + Ok(()) + } + + async fn player_monitor_loop( + player: Arc>, + player_mail: MailMan, + queue_mail: MailMan, + player_info: crossbeam_channel::Sender + ) -> Result<(), ()> { + Ok(()) + // std::thread::scope(|s| { + // }); + + // // Check for duration and spit it out + // // Check for end of song to get the next + + // Ok(()) + } + + + async fn library_loop( + lib_mail: MailMan, + library: &mut MusicLibrary, + config: Arc>, + ) -> Result<(), ()> { + while true { + match lib_mail.recv().await.unwrap() { + LibraryCommand::Song(uuid) => { + let (song, i) = library.query_uuid(&uuid).unwrap(); + lib_mail.send(LibraryResponse::Song(song.clone(), i)).await.unwrap(); + } + LibraryCommand::AllSongs => { + lib_mail.send(LibraryResponse::AllSongs(library.library.clone())).await.unwrap(); + }, + LibraryCommand::ExternalPlaylist(uuid) => { + let playlist = library.query_playlist_uuid(&uuid).unwrap(); + lib_mail.send(LibraryResponse::ExternalPlaylist(ExternalPlaylist::from_playlist(playlist, library))).await.unwrap(); + } + LibraryCommand::ImportM3UPlayList(path) => { + let playlist = Playlist::from_m3u(path, library).unwrap(); + let uuid = playlist.uuid; + let name = playlist.title.clone(); + library.playlists.items.push(PlaylistFolderItem::List(playlist)); + + lib_mail.send(LibraryResponse::ImportM3UPlayList(uuid, name)).await.unwrap(); + } + LibraryCommand::Save => { + library.save(config.read().unwrap().libraries.get_library(&library.uuid).unwrap().path.clone()).unwrap(); + lib_mail.send(LibraryResponse::Ok).await.unwrap(); + } + LibraryCommand::Playlists => { + let mut lists = vec![]; + library.playlists.lists_recursive().into_par_iter().map(|list| (list.uuid, list.title.clone())).collect_into_vec(&mut lists); + + lib_mail.send(LibraryResponse::Playlists(lists)).await.unwrap(); + } + _ => { todo!() } + } + } + Ok(()) + } + async fn queue_loop( + mut queue: Queue, + queue_mail: MailMan, + ) { + while true { + match queue_mail.recv().await.unwrap() { + QueueCommand::Append(item, by_human) => { + match item.item { + QueueItemType::Single(song) => queue.add_item(song, by_human), + _ => unimplemented!(), + } + queue_mail + .send(QueueResponse::Empty(Ok(()))) + .await + .unwrap(); + }, + QueueCommand::Next => { + let next = queue.next().map_or( Err(QueueError::NoNext), |s| Ok(s.clone())); + queue_mail + .send(QueueResponse::Item(next.clone())) + .await + .unwrap(); + } + QueueCommand::Prev => { + let prev = queue.prev().map_or( Err(QueueError::EmptyPlayed), |s| Ok(s.clone())); + queue_mail + .send(QueueResponse::Item(prev.clone())) + .await + .unwrap(); + } + QueueCommand::GetIndex(index) => { + let item = queue.items.get(index).map_or( Err(QueueError::OutOfBounds { index, len: queue.items.len() }), |s| Ok(s.clone())); + queue_mail.send(QueueResponse::Item(item)).await.unwrap(); + } + QueueCommand::NowPlaying => { + let item = queue.current().map(|t| t.clone()); + queue_mail + .send(QueueResponse::Item(item)) + .await + .unwrap(); + } + QueueCommand::Get => { + queue_mail.send(QueueResponse::GetAll(queue.items.clone())).await.unwrap(); + } + QueueCommand::Clear => { + queue.clear(); + queue_mail.send(QueueResponse::Empty(Ok(()))).await.unwrap(); + } + QueueCommand::Remove(index) => { + queue_mail.send(QueueResponse::Item(queue.remove_item(index))).await.unwrap(); + } + } + } + } +} + +#[derive(Debug)] +pub struct PlaybackInfo { + pub duration: TimeDelta, + pub position: TimeDelta +} \ No newline at end of file diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index f049df1..9d679f8 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -1,57 +1,59 @@ -use std::{fs::OpenOptions, io::{Read, Write}}; - -use dmp_core::music_controller::{controller::{ControllerHandle, LibraryResponse, PlayerCommand, PlayerLocation, PlayerResponse, QueueResponse}, queue::QueueSong}; -use kushi::QueueItem; -use tauri::{AppHandle, Emitter, State, Wry}; -use tempfile::TempDir; -use uuid::Uuid; - -use crate::wrappers::_Song; - - - -#[tauri::command] -pub async fn add_song_to_queue(app: AppHandle, ctrl_handle: State<'_, ControllerHandle>, uuid: Uuid, location: PlayerLocation) -> Result<(), String> { - dbg!(&location); - let (song, _) = ctrl_handle.lib_get_song(uuid).await; - match ctrl_handle.queue_append(QueueItem::from_item_type(kushi::QueueItemType::Single(QueueSong { song, location }))).await { - Ok(()) => (), - Err(e) => return Err(e.to_string()) - } - app.emit("queue_updated", ()).unwrap(); - Ok(()) -} - -#[tauri::command] -pub async fn play_now(app: AppHandle, ctrl_handle: State<'_, ControllerHandle>, uuid: Uuid, location: PlayerLocation) -> Result<(), String> { - let song = match ctrl_handle.play_now(uuid, location).await { - Ok(song) => song, - Err(e) => return Err(e.to_string()) - }; - app.emit("queue_updated", ()).unwrap(); - app.emit("now_playing_change", _Song::from(&song)).unwrap(); - app.emit("playing", ()).unwrap(); - Ok(()) -} - -#[tauri::command] -pub async fn display_album_art(ctrl_handle: State<'_, ControllerHandle>, temp_dir: State<'_, TempDir>, uuid: Uuid) -> Result<(), String> { - match ctrl_handle.lib_get_song(uuid.clone()).await.0.album_art(0) { - Ok(art) => { - let mut art = art.unwrap(); - let path = temp_dir.path().join(format!("CoverArt_{uuid}.{}", file_format::FileFormat::from_bytes(&art).extension())); - // TODO: This can be optimised later - let mut file = OpenOptions::new() - .create(true) - .truncate(true) - .write(true) - .read(true) - .open(path.clone()) - .unwrap(); - file.write_all(&mut art).unwrap(); - opener::open(path).unwrap(); - } - Err(e) => return Err(e.to_string()) - }; - Ok(()) +use std::{fs::OpenOptions, io::{Read, Write}}; + +use dmp_core::music_controller::{controller::{ControllerHandle, LibraryResponse, PlayerCommand, PlayerLocation, PlayerResponse, QueueResponse}, queue::QueueSong}; +use kushi::QueueItem; +use tauri::{AppHandle, Emitter, State, Wry}; +use tempfile::TempDir; +use uuid::Uuid; + +use crate::wrappers::_Song; + + + +#[tauri::command] +pub async fn add_song_to_queue(app: AppHandle, ctrl_handle: State<'_, ControllerHandle>, uuid: Uuid, location: PlayerLocation) -> Result<(), String> { + dbg!(&location); + let (song, _) = ctrl_handle.lib_get_song(uuid).await; + match ctrl_handle.queue_append(QueueItem::from_item_type(kushi::QueueItemType::Single(QueueSong { song, location }))).await { + Ok(()) => (), + Err(e) => return Err(e.to_string()) + } + app.emit("queue_updated", ()).unwrap(); + Ok(()) +} + +#[tauri::command] +pub async fn play_now(app: AppHandle, ctrl_handle: State<'_, ControllerHandle>, uuid: Uuid, location: PlayerLocation) -> Result<(), String> { + let song = match ctrl_handle.play_now(uuid, location).await { + Ok(song) => song, + Err(e) => return Err(e.to_string()) + }; + app.emit("queue_updated", ()).unwrap(); + app.emit("now_playing_change", _Song::from(&song)).unwrap(); + app.emit("playing", ()).unwrap(); + Ok(()) +} + +#[tauri::command] +pub async fn display_album_art(ctrl_handle: State<'_, ControllerHandle>, temp_dir: State<'_, TempDir>, uuid: Uuid) -> Result<(), String> { + match ctrl_handle.lib_get_song(uuid.clone()).await.0.album_art(0) { + Ok(art) => { + let mut art = art.unwrap(); + let path = temp_dir.path().join(format!("CoverArt_{uuid}.{}", file_format::FileFormat::from_bytes(&art).extension())); + if !path.exists() { + // TODO: This can be optimised later + let mut file = OpenOptions::new() + .create(true) + .truncate(true) + .write(true) + .read(true) + .open(path.clone()) + .unwrap(); + file.write_all(&mut art).unwrap(); + } + opener::open(path).unwrap(); + } + Err(e) => return Err(e.to_string()) + }; + Ok(()) } \ No newline at end of file diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 5b6bdd5..36b9b64 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,211 +1,221 @@ -use std::{fs, path::PathBuf, str::FromStr, thread::spawn}; - -use crossbeam::channel::{unbounded, Receiver, Sender}; -use dmp_core::{config::{Config, ConfigLibrary}, music_controller::controller::{Controller, ControllerHandle, LibraryResponse}, music_storage::library::MusicLibrary}; -use rfd::FileHandle; -use tauri::{http::Response, Emitter, Manager, State, WebviewWindowBuilder, Wry}; -use uuid::Uuid; - -use crate::wrappers::{get_library, play, pause, prev, set_volume, get_song, next, get_queue, import_playlist, get_playlist, get_playlists, remove_from_queue}; -use commands::{add_song_to_queue, play_now, display_album_art}; - - -pub mod wrappers; -pub mod commands; - -const DEFAULT_IMAGE: &[u8] = include_bytes!("../icons/icon.png"); - -#[cfg_attr(mobile, tauri::mobile_entry_point)] -pub fn run() { - let (rx, tx) = unbounded::(); - let (lib_rx, lib_tx) = unbounded::>(); - let (handle_rx, handle_tx) = unbounded::(); - - let controller_thread = spawn(move || { - let mut config = { tx.recv().unwrap() } ; - let scan_path = { lib_tx.recv().unwrap() }; - let _temp_config = ConfigLibrary::default(); - let _lib = config.libraries.get_default().unwrap_or(&_temp_config); - - let save_path = if _lib.path == PathBuf::default() { - scan_path.as_ref().unwrap().clone().canonicalize().unwrap().join("library.dlib") - } else { - _lib.path.clone() - }; - println!("save_path: {}\nscan_path:{scan_path:?}", save_path.display()); - - let mut library = MusicLibrary::init( - save_path.clone(), - _lib.uuid - ).unwrap(); - - let scan_path = scan_path.unwrap_or_else(|| config.libraries.get_default().unwrap().scan_folders.as_ref().unwrap()[0].clone()); - - if config.libraries.get_default().is_err() { - library.scan_folder(&scan_path).unwrap(); - config.push_library( ConfigLibrary::new(save_path.clone(), String::from("Library"), Some(vec![scan_path.clone()]), Some(library.uuid))); - } - if library.library.is_empty() { - println!("library is empty"); - } else { - config.write_file().unwrap(); - } - println!("scan_path: {}", scan_path.display()); - - library.save(save_path).unwrap(); - - let (handle, input) = ControllerHandle::new( - library, - std::sync::Arc::new(std::sync::RwLock::new(config)) - ); - - handle_rx.send(handle).unwrap(); - - let _controller = futures::executor::block_on(Controller::start(input)).unwrap(); - }); - let app = tauri::Builder::default() - .plugin(tauri_plugin_shell::init()) - .invoke_handler(tauri::generate_handler![ - get_config, - create_new_library, - get_library, - play, - pause, - set_volume, - next, - prev, - get_song, - lib_already_created, - get_queue, - add_song_to_queue, - play_now, - import_playlist, - get_playlist, - get_playlists, - remove_from_queue, - display_album_art, - ]).manage(ConfigRx(rx)) - .manage(LibRx(lib_rx)) - .manage(HandleTx(handle_tx)) - .manage(tempfile::TempDir::new().unwrap()) - .register_asynchronous_uri_scheme_protocol("asset", move |ctx, req, res| { - let query = req - .clone() - .uri() - .clone() - .into_parts() - .path_and_query - .unwrap() - .query() - .unwrap() - .to_string(); - - let bytes = if query.as_str() == "default" { - Some(DEFAULT_IMAGE.to_vec()) - } else {futures::executor::block_on(async move { - let controller = ctx.app_handle().state::(); - controller.lib_mail.send(dmp_core::music_controller::controller::LibraryCommand::Song(Uuid::parse_str(query.as_str()).unwrap())).await.unwrap(); - let LibraryResponse::Song(song, _) = controller.lib_mail.recv().await.unwrap() else { - return None - }; - Some(song.album_art(0).unwrap_or_else(|_| None).unwrap_or(DEFAULT_IMAGE.to_vec())) - })}; - - res.respond( - Response::builder() - .header("Origin", "*") - .header("Content-Length", bytes.as_ref().unwrap_or(&vec![]).len()) - .status(200) - .body(bytes.unwrap_or_default()) - .unwrap() - ); - }) - .build(tauri::generate_context!()) - .expect("error while building tauri application"); - - app - .run(|_app_handle, event| match event { - tauri::RunEvent::ExitRequested { api, .. } => { - // api.prevent_exit(); - panic!("does this kill the player?") - } - _ => {} - }); -} - -struct ConfigRx(Sender); - -struct LibRx(Sender>); -struct HandleTx(Receiver); - - -#[tauri::command] -async fn get_config(state: State<'_, ConfigRx>) -> Result { - if let Some(dir) = directories::ProjectDirs::from("", "Dangoware", "dmp") { - let path = dir.config_dir(); - fs::create_dir_all(path).or_else(|err| { - if err.kind() == std::io::ErrorKind::AlreadyExists { - Ok(()) - } else { - Err(err) - } - }).unwrap(); - - let config = if let Ok(mut c) = Config::read_file(PathBuf::from(path).join("config")) { - if c.state_path == PathBuf::default() { - c.state_path = PathBuf::from(path).join("state"); - } - c - } else { - let c = Config { - path: PathBuf::from(path).join("config"), - state_path: PathBuf::from(path).join("state"), - ..Default::default() - }; - c.write_file().unwrap(); - c - }; - - state.inner().0.send(config.clone()).unwrap(); - - Ok(config) - } else { - panic!("No config dir for DMP") - } -} - -#[tauri::command] -async fn create_new_library( - app: tauri::AppHandle, - lib_rx: State<'_, LibRx>, - handle_tx: State<'_, HandleTx>, -) -> Result<(), String> { - let dir = rfd::AsyncFileDialog::new() - .set_title("Pick a library path") - .pick_folder() - .await - .unwrap(); - - let path = dir.path().canonicalize().unwrap(); - println!("{}", path.display()); - - if !path.exists() { - panic!("Path {} does not exist!", path.display()) - } else if !path.is_dir() { - panic!("Path {} is not a directory!", path.display()) - } - - lib_rx.inner().0.send(Some(path)).unwrap(); - app.manage(handle_tx.inner().0.recv().unwrap()); - app.emit("library_loaded", ()).unwrap(); - Ok(()) -} - -#[tauri::command] - async fn lib_already_created(app: tauri::AppHandle, lib_rx: State<'_, LibRx>, handle_tx: State<'_, HandleTx>) -> Result<(), String> { - println!("lib already created"); - lib_rx.inner().0.send(None).unwrap(); - app.manage(handle_tx.inner().0.recv().unwrap()); - app.emit("library_loaded", ()).unwrap(); - Ok(()) -} +use std::{fs, path::PathBuf, str::FromStr, thread::{scope, spawn}}; + +use crossbeam::channel::{unbounded, Receiver, Sender}; +use dmp_core::{config::{Config, ConfigLibrary}, music_controller::controller::{Controller, ControllerHandle, LibraryResponse, PlaybackInfo}, music_storage::library::MusicLibrary}; +use rfd::FileHandle; +use tauri::{http::Response, Emitter, Manager, State, WebviewWindowBuilder, Wry}; +use uuid::Uuid; + +use crate::wrappers::{get_library, play, pause, prev, set_volume, get_song, next, get_queue, import_playlist, get_playlist, get_playlists, remove_from_queue}; +use commands::{add_song_to_queue, play_now, display_album_art}; + + +pub mod wrappers; +pub mod commands; + +const DEFAULT_IMAGE: &[u8] = include_bytes!("../icons/icon.png"); + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + let (rx, tx) = unbounded::(); + let (lib_rx, lib_tx) = unbounded::>(); + let (handle_rx, handle_tx) = unbounded::(); + let (playback_handle_rx, playback_handle_tx) = unbounded::>(); + + let controller_thread = spawn(move || { + let mut config = { tx.recv().unwrap() } ; + let scan_path = { lib_tx.recv().unwrap() }; + let _temp_config = ConfigLibrary::default(); + let _lib = config.libraries.get_default().unwrap_or(&_temp_config); + + let save_path = if _lib.path == PathBuf::default() { + scan_path.as_ref().unwrap().clone().canonicalize().unwrap().join("library.dlib") + } else { + _lib.path.clone() + }; + println!("save_path: {}\nscan_path:{scan_path:?}", save_path.display()); + + let mut library = MusicLibrary::init( + save_path.clone(), + _lib.uuid + ).unwrap(); + + let scan_path = scan_path.unwrap_or_else(|| config.libraries.get_default().unwrap().scan_folders.as_ref().unwrap()[0].clone()); + + if config.libraries.get_default().is_err() { + library.scan_folder(&scan_path).unwrap(); + config.push_library( ConfigLibrary::new(save_path.clone(), String::from("Library"), Some(vec![scan_path.clone()]), Some(library.uuid))); + } + if library.library.is_empty() { + println!("library is empty"); + } else { + config.write_file().unwrap(); + } + println!("scan_path: {}", scan_path.display()); + + library.save(save_path).unwrap(); + + let (handle, input, playback_tx) = ControllerHandle::new( + library, + std::sync::Arc::new(std::sync::RwLock::new(config)) + ); + + handle_rx.send(handle).unwrap(); + + scope(|s| { + s.spawn(|| { + let _controller = futures::executor::block_on(Controller::start(input)).unwrap(); + }); + s.spawn(|| { + + }); + }) + + + }); + let app = tauri::Builder::default() + .plugin(tauri_plugin_shell::init()) + .invoke_handler(tauri::generate_handler![ + get_config, + create_new_library, + get_library, + play, + pause, + set_volume, + next, + prev, + get_song, + lib_already_created, + get_queue, + add_song_to_queue, + play_now, + import_playlist, + get_playlist, + get_playlists, + remove_from_queue, + display_album_art, + ]).manage(ConfigRx(rx)) + .manage(LibRx(lib_rx)) + .manage(HandleTx(handle_tx)) + .manage(tempfile::TempDir::new().unwrap()) + .register_asynchronous_uri_scheme_protocol("asset", move |ctx, req, res| { + let query = req + .clone() + .uri() + .clone() + .into_parts() + .path_and_query + .unwrap() + .query() + .unwrap() + .to_string(); + + let bytes = if query.as_str() == "default" { + Some(DEFAULT_IMAGE.to_vec()) + } else {futures::executor::block_on(async move { + let controller = ctx.app_handle().state::(); + controller.lib_mail.send(dmp_core::music_controller::controller::LibraryCommand::Song(Uuid::parse_str(query.as_str()).unwrap())).await.unwrap(); + let LibraryResponse::Song(song, _) = controller.lib_mail.recv().await.unwrap() else { + return None + }; + Some(song.album_art(0).unwrap_or_else(|_| None).unwrap_or(DEFAULT_IMAGE.to_vec())) + })}; + + res.respond( + Response::builder() + .header("Origin", "*") + .header("Content-Length", bytes.as_ref().unwrap_or(&vec![]).len()) + .status(200) + .body(bytes.unwrap_or_default()) + .unwrap() + ); + }) + .build(tauri::generate_context!()) + .expect("error while building tauri application"); + + app + .run(|_app_handle, event| match event { + tauri::RunEvent::ExitRequested { api, .. } => { + // api.prevent_exit(); + panic!("does this kill the player?") + } + _ => {} + }); +} + +struct ConfigRx(Sender); + +struct LibRx(Sender>); +struct HandleTx(Receiver); + + +#[tauri::command] +async fn get_config(state: State<'_, ConfigRx>) -> Result { + if let Some(dir) = directories::ProjectDirs::from("", "Dangoware", "dmp") { + let path = dir.config_dir(); + fs::create_dir_all(path).or_else(|err| { + if err.kind() == std::io::ErrorKind::AlreadyExists { + Ok(()) + } else { + Err(err) + } + }).unwrap(); + + let config = if let Ok(mut c) = Config::read_file(PathBuf::from(path).join("config")) { + if c.state_path == PathBuf::default() { + c.state_path = PathBuf::from(path).join("state"); + } + c + } else { + let c = Config { + path: PathBuf::from(path).join("config"), + state_path: PathBuf::from(path).join("state"), + ..Default::default() + }; + c.write_file().unwrap(); + c + }; + + state.inner().0.send(config.clone()).unwrap(); + + Ok(config) + } else { + panic!("No config dir for DMP") + } +} + +#[tauri::command] +async fn create_new_library( + app: tauri::AppHandle, + lib_rx: State<'_, LibRx>, + handle_tx: State<'_, HandleTx>, +) -> Result<(), String> { + let dir = rfd::AsyncFileDialog::new() + .set_title("Pick a library path") + .pick_folder() + .await + .unwrap(); + + let path = dir.path().canonicalize().unwrap(); + println!("{}", path.display()); + + if !path.exists() { + panic!("Path {} does not exist!", path.display()) + } else if !path.is_dir() { + panic!("Path {} is not a directory!", path.display()) + } + + lib_rx.inner().0.send(Some(path)).unwrap(); + app.manage(handle_tx.inner().0.recv().unwrap()); + app.emit("library_loaded", ()).unwrap(); + Ok(()) +} + +#[tauri::command] + async fn lib_already_created(app: tauri::AppHandle, lib_rx: State<'_, LibRx>, handle_tx: State<'_, HandleTx>) -> Result<(), String> { + println!("lib already created"); + lib_rx.inner().0.send(None).unwrap(); + app.manage(handle_tx.inner().0.recv().unwrap()); + app.emit("library_loaded", ()).unwrap(); + Ok(()) +}