diff --git a/dmp-core/Cargo.toml b/dmp-core/Cargo.toml index 916eb73..4a2b78c 100644 --- a/dmp-core/Cargo.toml +++ b/dmp-core/Cargo.toml @@ -22,16 +22,13 @@ file-format = { version = "0.23.0", features = [ "reader-xml", "serde", ] } -lofty = "0.18.2" +lofty = "0.21" serde = { version = "1.0.195", features = ["derive"] } walkdir = "2.4.0" chrono = { version = "0.4.31", features = ["serde"] } rayon = "1.8.0" log = "0.4" -base64 = "0.21.5" rcue = "0.1.3" -gstreamer = "0.21.3" -glib = "0.18.5" crossbeam-channel = "0.5.8" crossbeam = "0.8.2" quick-xml = "0.31.0" @@ -44,8 +41,6 @@ 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" moro = "0.4.0" moro-local = "0.4.0" @@ -56,3 +51,4 @@ async-channel = "2.3.1" ciborium = "0.2.2" itertools = "0.13.0" directories = "5.0.1" +prismriver = { git = "https://github.com/Dangoware/prismriver.git" } diff --git a/dmp-core/src/config/mod.rs b/dmp-core/src/config/mod.rs index b11bb38..8897627 100644 --- a/dmp-core/src/config/mod.rs +++ b/dmp-core/src/config/mod.rs @@ -1,7 +1,7 @@ use std::{ fs::{self, File, OpenOptions}, io::{Error, Read, Write}, - path::{Path, PathBuf}, + path::PathBuf, }; use serde::{Deserialize, Serialize}; @@ -189,9 +189,7 @@ pub enum ConfigError { pub mod tests { use super::{Config, ConfigLibrary}; use crate::music_storage::library::MusicLibrary; - use std::{ - path::PathBuf, - }; + use std::path::PathBuf; pub fn new_config_lib() -> (Config, MusicLibrary) { _ = std::fs::create_dir_all("test-config/music/"); diff --git a/dmp-core/src/lib.rs b/dmp-core/src/lib.rs index 0f41571..cc2779c 100644 --- a/dmp-core/src/lib.rs +++ b/dmp-core/src/lib.rs @@ -14,9 +14,4 @@ pub mod music_controller { pub mod queue; } -pub mod music_player { - pub mod gstreamer; - pub mod player; -} - pub mod config; diff --git a/dmp-core/src/music_controller/controller.rs b/dmp-core/src/music_controller/controller.rs index 3ddb4c1..729922d 100644 --- a/dmp-core/src/music_controller/controller.rs +++ b/dmp-core/src/music_controller/controller.rs @@ -3,37 +3,35 @@ //! other functions #![allow(while_true)] -use itertools::Itertools; use kushi::{Queue, QueueItemType}; use kushi::{QueueError, QueueItem}; +use prismriver::{Prismriver, Volume}; 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::marker::PhantomData; use std::path::{Path, PathBuf}; use std::sync::{Arc, RwLock}; use thiserror::Error; use uuid::Uuid; -use crate::config::{self, ConfigError}; -use crate::music_player::player::{Player, PlayerError}; +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<'a, P>(&'a PhantomData

); +pub struct Controller(); #[derive(Error, Debug)] pub enum ControllerError { #[error("{0:?}")] QueueError(#[from] QueueError), #[error("{0:?}")] - PlayerError(#[from] PlayerError), + PlayerError(#[from] prismriver::Error), #[error("{0:?}")] ConfigError(#[from] ConfigError), } @@ -213,7 +211,7 @@ impl ControllerState { } #[allow(unused_variables)] -impl<'c, P: Player + Send + Sync> Controller<'c, P> { +impl Controller { pub async fn start( ControllerInput { player_mail, @@ -222,10 +220,7 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> { mut library, config }: ControllerInput - ) -> Result<(), Box> - where - P: Player, - { + ) -> Result<(), Box> { let queue: Queue = Queue { items: Vec::new(), played: Vec::new(), @@ -250,13 +245,13 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> { futures::executor::block_on(async { moro::async_scope!(|scope| { println!("async scope created"); - let player = Arc::new(RwLock::new(P::new().unwrap())); + 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( + Controller::player_command_loop( _player, player_mail.1, queue_mail.0, @@ -268,7 +263,7 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> { }); scope .spawn(async move { - Controller::

::player_event_loop( + Controller::player_event_loop( player, player_mail.0 ) @@ -277,7 +272,7 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> { }); scope .spawn(async { - Controller::

::library_loop( + Controller::library_loop( lib_mail.1, &mut library, config, @@ -292,7 +287,7 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> { let b = scope.spawn(|| { futures::executor::block_on(async { - Controller::

::queue_loop(queue, queue_mail.1).await; + Controller::queue_loop(queue, queue_mail.1).await; }) }); a.join().unwrap(); @@ -303,7 +298,7 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> { } async fn player_command_loop( - player: Arc>, + player: Arc>, player_mail: MailMan, queue_mail: MailMan, lib_mail: MailMan, @@ -311,9 +306,8 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> { ) -> Result<(), ()> { let mut first = true; { - let volume = state.volume as f64; - player.write().unwrap().set_volume(volume); - println!("volume set to {volume}"); + player.write().unwrap().set_volume(Volume::new(state.volume)); + println!("volume set to {}", state.volume); } while true { let _mail = player_mail.recv().await; @@ -324,20 +318,24 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> { queue_mail.send(QueueCommand::NowPlaying).await.unwrap(); let QueueResponse::Item(item) = queue_mail.recv().await.unwrap() else { unimplemented!() }; let QueueItemType::Single(song) = item.item else { unimplemented!("This is temporary, handle queueItemTypes at some point") }; - player.write().unwrap().enqueue_next(song.song.primary_uri().unwrap().0).unwrap(); + + 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(); + player_mail.send(PlayerResponse::NowPlaying(song.song)).await.unwrap(); first = false } else { - player.write().unwrap().play().unwrap(); + player.write().unwrap().play(); player_mail.send(PlayerResponse::Empty).await.unwrap(); } } PlayerCommand::Pause => { - player.write().unwrap().pause().unwrap(); + player.write().unwrap().pause(); player_mail.send(PlayerResponse::Empty).await.unwrap(); } PlayerCommand::SetVolume(volume) => { - player.write().unwrap().set_volume(volume as f64); + player.write().unwrap().set_volume(Volume::new(volume)); println!("volume set to {volume}"); player_mail.send(PlayerResponse::Empty).await.unwrap(); @@ -352,7 +350,11 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> { QueueItemType::Single(song) => song.song.primary_uri().unwrap().0, _ => unimplemented!(), }; - player.write().unwrap().enqueue_next(uri).unwrap(); + + 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")}; // Append next song in library @@ -398,6 +400,11 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> { 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(np_song.song.clone())).await.unwrap(); } @@ -410,11 +417,9 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> { if let QueueResponse::Item(item) = queue_mail.recv().await.unwrap() { match item.item { QueueItemType::Single(song) => { - player - .write() - .unwrap() - .enqueue_next(song.song.primary_uri().unwrap().0) - .unwrap(); + 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!(), } @@ -436,7 +441,10 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> { unreachable!() }; - player.write().unwrap().enqueue_next(song.primary_uri().unwrap().0).unwrap(); + // 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? // ... @@ -486,7 +494,7 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> { async fn library_loop( lib_mail: MailMan, - library: &'c mut MusicLibrary, + library: &mut MusicLibrary, config: Arc>, ) -> Result<(), ()> { while true { @@ -511,7 +519,7 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> { 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(); + library.save(config.read().unwrap().libraries.get_library(&library.uuid).unwrap().path.clone()).unwrap(); lib_mail.send(LibraryResponse::Ok).await.unwrap(); } LibraryCommand::Playlists => { @@ -527,7 +535,7 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> { } async fn player_event_loop( - player: Arc>, + player: Arc>, player_mail: MailMan, ) -> Result<(), ()> { // just pretend this does something @@ -586,82 +594,3 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> { } } } - -#[cfg(test)] -mod test_super { - use std::{ - path::PathBuf, - sync::{Arc, RwLock}, - thread::spawn, - }; - - use crate::{ - config::{tests::new_config_lib, Config}, - music_controller::controller::{ - LibraryCommand, LibraryResponse, MailMan, PlayerCommand, PlayerResponse, ControllerHandle - }, - music_player::gstreamer::GStreamer, - music_storage::library::MusicLibrary, - }; - - use super::Controller; - - #[tokio::test] - async fn construct_controller() { - // use if you don't have a config setup and add music to the music folder - new_config_lib(); - - let config = Config::read_file(PathBuf::from(std::env!("CONFIG-PATH"))).unwrap(); - let library = { - MusicLibrary::init( - config.libraries.get_default().unwrap().path.clone(), - config.libraries.get_default().unwrap().uuid, - ) - .unwrap() - }; - - let (handle, input) = ControllerHandle::new(library, Arc::new(RwLock::new(config))); - - let b = spawn(move || { - futures::executor::block_on(async { - handle.player_mail - .send(PlayerCommand::SetVolume(0.01)) - .await - .unwrap(); - loop { - let buf: String = text_io::read!(); - dbg!(&buf); - handle.player_mail - .send(match buf.to_lowercase().as_str() { - "next" => PlayerCommand::NextSong, - "prev" => PlayerCommand::PrevSong, - "pause" => PlayerCommand::Pause, - "play" => PlayerCommand::Play, - x if x.parse::().is_ok() => { - PlayerCommand::Enqueue(x.parse::().unwrap()) - } - _ => continue, - }) - .await - .unwrap(); - println!("sent it"); - println!("{:?}", handle.player_mail.recv().await.unwrap()) - } - }) - }); - - let a = spawn(move || { - futures::executor::block_on(async { - - - Controller::::start(input) - .await - .unwrap(); - }); - }); - - b.join().unwrap(); - a.join().unwrap(); - } -} - diff --git a/dmp-core/src/music_player/gstreamer.rs b/dmp-core/src/music_player/gstreamer.rs deleted file mode 100644 index 7733d85..0000000 --- a/dmp-core/src/music_player/gstreamer.rs +++ /dev/null @@ -1,521 +0,0 @@ -// Crate things -use crate::music_storage::library::URI; -use crossbeam_channel::{unbounded, Receiver, Sender}; -use std::error::Error; -use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}; - -// GStreamer things -use glib::FlagsClass; -use gst::{ClockTime, Element}; -use gstreamer as gst; -use gstreamer::prelude::*; - -// Extra things -use chrono::Duration; - -use super::player::{Player, PlayerCommand, PlayerError, PlayerState}; - -impl From for PlayerState { - fn from(value: gst::State) -> Self { - match value { - gst::State::VoidPending => Self::VoidPending, - gst::State::Playing => Self::Playing, - gst::State::Paused => Self::Paused, - gst::State::Ready => Self::Ready, - gst::State::Null => Self::Null, - } - } -} - -impl TryInto for PlayerState { - fn try_into(self) -> Result> { - match self { - Self::VoidPending => Ok(gst::State::VoidPending), - Self::Playing => Ok(gst::State::Playing), - Self::Paused => Ok(gst::State::Paused), - Self::Ready => Ok(gst::State::Ready), - Self::Null => Ok(gst::State::Null), - state => Err(format!("Invalid gst::State: {:?}", state).into()), - } - } - - type Error = Box; -} - -#[derive(Debug, PartialEq, Eq)] -enum PlaybackInfo { - Idle, - Switching, - Playing { - start: Duration, - end: Duration, - }, - - /// When this is sent, the thread will die! Use it when the [Player] is - /// done playing - Finished, -} - -/// An instance of a music player with a GStreamer backend -#[derive(Debug)] -pub struct GStreamer { - source: Option, - - message_rx: crossbeam::channel::Receiver, - playback_tx: crossbeam::channel::Sender, - - playbin: Arc>, - volume: f64, - start: Option, - end: Option, - paused: Arc>, - position: Arc>>, -} - -impl From for PlayerError { - fn from(value: gst::StateChangeError) -> Self { - PlayerError::StateChange(value.to_string()) - } -} - -impl From for PlayerError { - fn from(value: glib::BoolError) -> Self { - PlayerError::General(value.to_string()) - } -} - -impl GStreamer { - /// Set the playback URI - fn set_source(&mut self, source: &URI) -> Result<(), PlayerError> { - if !source.exists().is_ok_and(|x| x) { - // If the source doesn't exist, gstreamer will crash! - return Err(PlayerError::NotFound); - } - - // Make sure the playback tracker knows the stuff is stopped - println!("Beginning switch"); - self.playback_tx.send(PlaybackInfo::Switching).unwrap(); - - let uri = self.playbin.read().unwrap().property_value("current-uri"); - self.source = Some(source.clone()); - match source { - URI::Cue { start, end, .. } => { - self.playbin - .write() - .unwrap() - .set_property("uri", source.as_uri()); - - // Set the start and end positions of the CUE file - self.start = Some(Duration::from_std(*start).unwrap()); - self.end = Some(Duration::from_std(*end).unwrap()); - - // Send the updated position to the tracker - self.playback_tx - .send(PlaybackInfo::Playing { - start: self.start.unwrap(), - end: self.end.unwrap(), - }) - .unwrap(); - - // Wait for it to be ready, and then move to the proper position - self.play().unwrap(); - let now = std::time::Instant::now(); - while now.elapsed() < std::time::Duration::from_millis(20) { - if self.seek_to(Duration::from_std(*start).unwrap()).is_ok() { - return Ok(()); - } - std::thread::sleep(std::time::Duration::from_millis(1)); - } - //panic!("Couldn't seek to beginning of cue track in reasonable time (>20ms)"); - return Err(PlayerError::StateChange( - "Could not seek to beginning of CUE track".into(), - )); - } - _ => { - self.playbin - .write() - .unwrap() - .set_property("uri", source.as_uri()); - - if self.state() != PlayerState::Playing { - self.play().unwrap(); - } - - while self.raw_duration().is_none() { - std::thread::sleep(std::time::Duration::from_millis(10)); - } - - self.start = Some(Duration::seconds(0)); - self.end = self.raw_duration(); - - // Send the updated position to the tracker - self.playback_tx - .send(PlaybackInfo::Playing { - start: self.start.unwrap(), - end: self.end.unwrap(), - }) - .unwrap(); - } - } - - Ok(()) - } - - /// Gets a mutable reference to the playbin element - fn playbin_mut( - &mut self, - ) -> Result, std::sync::PoisonError>> - { - let element = match self.playbin.write() { - Ok(element) => element, - Err(err) => return Err(err), - }; - Ok(element) - } - - /// Gets a read-only reference to the playbin element - fn playbin( - &self, - ) -> Result, std::sync::PoisonError>> - { - let element = match self.playbin.read() { - Ok(element) => element, - Err(err) => return Err(err), - }; - Ok(element) - } - - /// Set volume of the internal playbin player, can be - /// used to bypass the main volume control for seeking - fn set_gstreamer_volume(&mut self, volume: f64) { - self.playbin_mut().unwrap().set_property("volume", volume) - } - - fn set_state(&mut self, state: gst::State) -> Result<(), gst::StateChangeError> { - self.playbin_mut().unwrap().set_state(state)?; - - Ok(()) - } - - fn raw_duration(&self) -> Option { - self.playbin() - .unwrap() - .query_duration::() - .map(|pos| Duration::nanoseconds(pos.nseconds() as i64)) - } - - /// Get the current state of the playback - fn state(&mut self) -> PlayerState { - self.playbin().unwrap().current_state().into() - /* - match *self.buffer.read().unwrap() { - None => self.playbin().unwrap().current_state().into(), - Some(value) => PlayerState::Buffering(value), - } - */ - } - - fn property(&self, property: &str) -> glib::Value { - self.playbin().unwrap().property_value(property) - } - - fn ready(&mut self) -> Result<(), PlayerError> { - self.set_state(gst::State::Ready)?; - Ok(()) - } -} - -impl Player for GStreamer { - fn new() -> Result { - // Initialize GStreamer, maybe figure out how to nicely fail here - if let Err(err) = gst::init() { - return Err(PlayerError::Init(err.to_string())); - }; - let ctx = glib::MainContext::default(); - let _guard = ctx.acquire(); - let mainloop = glib::MainLoop::new(Some(&ctx), false); - - let playbin_arc = Arc::new(RwLock::new( - match gst::ElementFactory::make("playbin3").build() { - Ok(playbin) => playbin, - Err(error) => return Err(PlayerError::Init(error.to_string())), - }, - )); - - let playbin = playbin_arc.clone(); - - let flags = playbin.read().unwrap().property_value("flags"); - let flags_class = FlagsClass::with_type(flags.type_()).unwrap(); - - // Set up the Playbin flags to only play audio - let flags = flags_class - .builder_with_value(flags) - .ok_or(PlayerError::Build)? - .set_by_nick("audio") - .set_by_nick("download") - .unset_by_nick("video") - .unset_by_nick("text") - .build() - .ok_or(PlayerError::Build)?; - - playbin - .write() - .unwrap() - .set_property_from_value("flags", &flags); - //playbin.write().unwrap().set_property("instant-uri", true); - - let position = Arc::new(RwLock::new(None)); - - // Set up the thread to monitor the position - let (playback_tx, playback_rx) = unbounded(); - let (status_tx, status_rx) = unbounded::(); - let position_update = Arc::clone(&position); - - std::thread::spawn(|| { - playback_monitor(playbin_arc, status_rx, playback_tx, position_update) - }); - - // Set up the thread to monitor bus messages - let playbin_bus_ctrl = Arc::clone(&playbin); - let paused = Arc::new(RwLock::new(false)); - let bus_paused = Arc::clone(&paused); - let bus_watch = playbin - .read() - .unwrap() - .bus() - .expect("Failed to get GStreamer message bus") - .add_watch(move |_bus, msg| { - match msg.view() { - gst::MessageView::Eos(_) => println!("End of stream"), - gst::MessageView::StreamStart(_) => println!("Stream start"), - gst::MessageView::Error(err) => { - println!("Error recieved: {}", err); - return glib::ControlFlow::Break; - } - gst::MessageView::Buffering(buffering) => { - if *bus_paused.read().unwrap() == true { - return glib::ControlFlow::Continue; - } - - // If the player is not paused, pause it - let percent = buffering.percent(); - if percent < 100 { - playbin_bus_ctrl - .write() - .unwrap() - .set_state(gst::State::Paused) - .unwrap(); - } else if percent >= 100 { - println!("Finished buffering"); - playbin_bus_ctrl - .write() - .unwrap() - .set_state(gst::State::Playing) - .unwrap(); - } - } - _ => (), - } - glib::ControlFlow::Continue - }) - .expect("Failed to connect to GStreamer message bus"); - - // Set up a thread to watch the messages - std::thread::spawn(move || { - let _watch = bus_watch; - mainloop.run() - }); - - let source = None; - Ok(Self { - source, - playbin, - message_rx: playback_rx, - playback_tx: status_tx, - volume: 1.0, - start: None, - end: None, - paused, - position, - }) - } - - fn source(&self) -> &Option { - &self.source - } - - fn enqueue_next(&mut self, next_track: &URI) -> Result<(), PlayerError> { - println!("enqueuing in fn"); - self.set_source(next_track) - } - - fn set_volume(&mut self, volume: f64) { - self.volume = volume.clamp(0.0, 1.0); - self.set_gstreamer_volume(self.volume); - } - - fn volume(&self) -> f64 { - self.volume - } - - fn play(&mut self) -> Result<(), PlayerError> { - if self.state() == PlayerState::Playing { - return Ok(()); - } - *self.paused.write().unwrap() = false; - self.set_state(gst::State::Playing)?; - Ok(()) - } - - fn pause(&mut self) -> Result<(), PlayerError> { - if self.state() == PlayerState::Paused || *self.paused.read().unwrap() { - return Ok(()); - } - *self.paused.write().unwrap() = true; - self.set_state(gst::State::Paused)?; - Ok(()) - } - - fn is_paused(&self) -> bool { - self.playbin().unwrap().current_state() == gst::State::Paused - } - - fn position(&self) -> Option { - *self.position.read().unwrap() - } - - fn duration(&self) -> Option { - if self.end.is_some() && self.start.is_some() { - Some(self.end.unwrap() - self.start.unwrap()) - } else { - self.raw_duration() - } - } - - fn seek_by(&mut self, seek_amount: Duration) -> Result<(), PlayerError> { - let time_pos = match *self.position.read().unwrap() { - Some(pos) => pos, - None => return Err(PlayerError::Seek("No position".into())), - }; - let seek_pos = time_pos + seek_amount; - - self.seek_to(seek_pos)?; - Ok(()) - } - - fn seek_to(&mut self, target_pos: Duration) -> Result<(), PlayerError> { - let start = if self.start.is_none() { - return Err(PlayerError::Seek("No START time".into())); - } else { - self.start.unwrap() - }; - - let end = if self.end.is_none() { - return Err(PlayerError::Seek("No END time".into())); - } else { - self.end.unwrap() - }; - - let adjusted_target = target_pos + start; - let clamped_target = adjusted_target.clamp(start, end); - - let seek_pos_clock = - ClockTime::from_useconds(clamped_target.num_microseconds().unwrap() as u64); - - self.set_gstreamer_volume(0.0); - self.playbin_mut() - .unwrap() - .seek_simple(gst::SeekFlags::FLUSH, seek_pos_clock)?; - self.set_gstreamer_volume(self.volume); - Ok(()) - } - - fn stop(&mut self) -> Result<(), PlayerError> { - self.pause()?; - self.ready()?; - - // Send the updated position to the tracker - self.playback_tx.send(PlaybackInfo::Idle).unwrap(); - - // Set all positions to none - *self.position.write().unwrap() = None; - self.start = None; - self.end = None; - Ok(()) - } - - fn message_channel(&self) -> &crossbeam::channel::Receiver { - &self.message_rx - } -} - -impl Drop for GStreamer { - /// Cleans up the `GStreamer` pipeline and the monitoring - /// thread when [Player] is dropped. - fn drop(&mut self) { - self.playbin_mut() - .unwrap() - .set_state(gst::State::Null) - .expect("Unable to set the pipeline to the `Null` state"); - let _ = self.playback_tx.send(PlaybackInfo::Finished); - } -} - -fn playback_monitor( - playbin: Arc>, - status_rx: Receiver, - playback_tx: Sender, - position: Arc>>, -) { - let mut stats = PlaybackInfo::Idle; - let mut pos_temp; - let mut sent_atf = false; - loop { - // Check for new messages to decide how to proceed - if let Ok(result) = status_rx.recv_timeout(std::time::Duration::from_millis(50)) { - stats = result - } - - pos_temp = playbin - .read() - .unwrap() - .query_position::() - .map(|pos| Duration::nanoseconds(pos.nseconds() as i64)); - - match stats { - PlaybackInfo::Playing { start, end } if pos_temp.is_some() => { - // Check if the current playback position is close to the end - let finish_point = end - Duration::milliseconds(2000); - if pos_temp.unwrap().num_microseconds() >= end.num_microseconds() { - println!("MONITOR: End of stream"); - let _ = playback_tx.try_send(PlayerCommand::EndOfStream); - playbin - .write() - .unwrap() - .set_state(gst::State::Ready) - .expect("Unable to set the pipeline state"); - sent_atf = false - } else if pos_temp.unwrap().num_microseconds() >= finish_point.num_microseconds() - && !sent_atf - { - println!("MONITOR: About to finish"); - let _ = playback_tx.try_send(PlayerCommand::AboutToFinish); - sent_atf = true; - } - - // This has to be done AFTER the current time in the file - // is calculated, or everything else is wrong - pos_temp = Some(pos_temp.unwrap() - start) - } - PlaybackInfo::Finished => { - println!("MONITOR: Shutting down"); - *position.write().unwrap() = None; - break; - } - PlaybackInfo::Idle | PlaybackInfo::Switching => sent_atf = false, - _ => (), - } - - *position.write().unwrap() = pos_temp; - } -} diff --git a/dmp-core/src/music_player/kira.rs b/dmp-core/src/music_player/kira.rs deleted file mode 100644 index e69de29..0000000 diff --git a/dmp-core/src/music_player/player.rs b/dmp-core/src/music_player/player.rs deleted file mode 100644 index 4b74498..0000000 --- a/dmp-core/src/music_player/player.rs +++ /dev/null @@ -1,99 +0,0 @@ -use chrono::Duration; -use thiserror::Error; - -use crate::music_storage::library::URI; - -#[derive(Error, Debug)] -pub enum PlayerError { - #[error("player initialization failed: {0}")] - Init(String), - #[error("could not change playback state")] - StateChange(String), - #[error("seeking failed: {0}")] - Seek(String), - #[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(String), -} - -#[derive(Debug, PartialEq, Eq)] -pub enum PlayerState { - Playing, - Paused, - Ready, - Buffering(u8), - Null, - VoidPending, -} - -#[derive(Debug, PartialEq, Eq)] -pub enum PlayerCommand { - Play, - Pause, - EndOfStream, - AboutToFinish, -} - -pub trait Player { - /// Create a new player. - fn new() -> Result - where - Self: Sized; - - /// Get the currently playing [URI] from the player. - fn source(&self) -> &Option; - - /// Insert a new [`URI`] to be played. This method should be called at the - /// beginning to start playback of something, and once the [`PlayerCommand`] - /// indicates the track is about to finish to enqueue gaplessly. - /// - /// For backends which do not support gapless playback, `AboutToFinish` - /// will not be called, and the next [`URI`] should be enqueued once `Eos` - /// occurs. - fn enqueue_next(&mut self, next_track: &URI) -> Result<(), PlayerError>; - - /// Set the playback volume, accepts a float from `0` to `1`. - /// - /// Values outside the range of `0` to `1` will be capped. - fn set_volume(&mut self, volume: f64); - - /// Returns the current volume level, a float from `0` to `1`. - fn volume(&self) -> f64; - - /// If the player is paused or stopped, starts playback. - fn play(&mut self) -> Result<(), PlayerError>; - - /// If the player is playing, pause playback. - fn pause(&mut self) -> Result<(), PlayerError>; - - /// Stop the playback entirely, removing the current [`URI`] from the player. - fn stop(&mut self) -> Result<(), PlayerError>; - - /// Convenience function to check if playback is paused. - fn is_paused(&self) -> bool; - - /// Get the current playback position of the player. - fn position(&self) -> Option; - - /// Get the duration of the currently playing track. - fn duration(&self) -> Option; - - /// Seek relative to the current position. - /// - /// The position is capped at the duration of the song, and zero. - fn seek_by(&mut self, seek_amount: Duration) -> Result<(), PlayerError>; - - /// Seek absolutely within the song. - /// - /// The position is capped at the duration of the song, and zero. - fn seek_to(&mut self, target_pos: Duration) -> Result<(), PlayerError>; - - /// Return a reference to the player message channel, which can be cloned - /// in order to monitor messages from the player. - fn message_channel(&self) -> &crossbeam::channel::Receiver; -} diff --git a/dmp-core/src/music_storage/db_reader/itunes/reader.rs b/dmp-core/src/music_storage/db_reader/itunes/reader.rs index 05075f2..868d1d8 100644 --- a/dmp-core/src/music_storage/db_reader/itunes/reader.rs +++ b/dmp-core/src/music_storage/db_reader/itunes/reader.rs @@ -1,5 +1,7 @@ use file_format::FileFormat; -use lofty::{AudioFile, LoftyError, ParseOptions, Probe, TagType, TaggedFileExt}; +use lofty::file::{AudioFile as _, TaggedFileExt as _}; +use lofty::probe::Probe; +use lofty::tag::TagType; use quick_xml::events::Event; use quick_xml::reader::Reader; use std::collections::{BTreeMap, HashMap}; @@ -221,7 +223,7 @@ fn to_tag(string: String) -> Tag { _ => Tag::Key(string), } } -fn get_duration(file: &Path) -> Result { +fn get_duration(file: &Path) -> Result { let dur = match Probe::open(file)?.read() { Ok(tagged_file) => tagged_file.properties().duration(), @@ -229,12 +231,12 @@ fn get_duration(file: &Path) -> Result { }; Ok(dur) } -fn get_art(file: &Path) -> Result, LoftyError> { +fn get_art(file: &Path) -> Result, lofty::error::LoftyError> { let mut album_art: Vec = Vec::new(); - let blank_tag = &lofty::Tag::new(TagType::Id3v2); - let normal_options = ParseOptions::new().parsing_mode(lofty::ParsingMode::Relaxed); - let tagged_file: lofty::TaggedFile; + let blank_tag = &lofty::tag::Tag::new(TagType::Id3v2); + let normal_options = lofty::config::ParseOptions::new().parsing_mode(lofty::config::ParsingMode::Relaxed); + let tagged_file: lofty::file::TaggedFile; let tag = match Probe::open(file)?.options(normal_options).read() { Ok(e) => { @@ -288,7 +290,7 @@ impl ITunesSong { Default::default() } - fn from_hashmap(map: &mut HashMap) -> Result { + fn from_hashmap(map: &mut HashMap) -> Result { let mut song = ITunesSong::new(); //get the path with the first bit chopped off let path_: String = map.get_key_value("Location").unwrap().1.clone(); @@ -339,10 +341,7 @@ impl ITunesSong { #[cfg(test)] mod tests { - use std::{ - path::{Path, PathBuf}, - sync::{Arc, RwLock}, - }; + use std::path::{Path, PathBuf}; use crate::{ config::{Config, ConfigLibrary}, diff --git a/dmp-core/src/music_storage/library.rs b/dmp-core/src/music_storage/library.rs index a494d6c..cb78b0d 100644 --- a/dmp-core/src/music_storage/library.rs +++ b/dmp-core/src/music_storage/library.rs @@ -1,12 +1,11 @@ use super::playlist::{Playlist, PlaylistFolder}; // Crate things use super::utils::{find_images, normalize, read_file, write_file}; -use crate::config::Config; use crate::music_storage::playlist::PlaylistFolderItem; use std::cmp::Ordering; // Various std things -use std::collections::{BTreeMap, HashMap}; +use std::collections::BTreeMap; use std::error::Error; use std::io::Read; use std::ops::ControlFlow::{Break, Continue}; @@ -14,9 +13,10 @@ use std::vec::IntoIter; // Files use file_format::{FileFormat, Kind}; -use glib::filename_to_uri; -use lofty::{AudioFile, ItemKey, ItemValue, ParseOptions, Probe, TagType, TaggedFileExt}; +use lofty::file::{AudioFile as _, TaggedFileExt as _}; +use lofty::probe::Probe; +use lofty::tag::{ItemKey, ItemValue, TagType}; use rcue::parser::parse_from_file; use std::fs; use std::path::{Path, PathBuf}; @@ -28,12 +28,11 @@ use chrono::{serde::ts_milliseconds_option, DateTime, Utc}; use std::time::Duration; // Serialization/Compression -use base64::{engine::general_purpose, Engine as _}; use serde::{Deserialize, Serialize}; // Fun parallel stuff use rayon::prelude::*; -use std::sync::{Arc, Mutex, RwLock}; +use std::sync::{Arc, Mutex}; #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] pub enum AlbumArt { @@ -225,10 +224,10 @@ impl Song { /// Creates a `Song` from a music file pub fn from_file>(target_file: &P) -> Result> { - let normal_options = ParseOptions::new().parsing_mode(lofty::ParsingMode::Relaxed); + let normal_options = lofty::config::ParseOptions::new().parsing_mode(lofty::config::ParsingMode::Relaxed); - let blank_tag = &lofty::Tag::new(TagType::Id3v2); - let tagged_file: lofty::TaggedFile; + let blank_tag = &lofty::tag::Tag::new(TagType::Id3v2); + let tagged_file: lofty::file::TaggedFile; let mut duration = Duration::from_secs(0); let tag = match Probe::open(target_file)?.options(normal_options).read() { Ok(file) => { @@ -273,7 +272,7 @@ impl Song { let value = match item.value() { ItemValue::Text(value) => value.clone(), ItemValue::Locator(value) => value.clone(), - ItemValue::Binary(bin) => format!("BIN#{}", general_purpose::STANDARD.encode(bin)), + ItemValue::Binary(_) => continue, // TODO: Ignoring binary values for now }; tags.insert(key, value); @@ -548,10 +547,10 @@ impl URI { pub fn as_uri(&self) -> String { let path_str = match self { - URI::Local(location) => filename_to_uri(location, None) + URI::Local(location) => prismriver::utils::path_to_uri(location) .expect("couldn't convert path to URI") .to_string(), - URI::Cue { location, .. } => filename_to_uri(location, None) + URI::Cue { location, .. } => prismriver::utils::path_to_uri(location) .expect("couldn't convert path to URI") .to_string(), URI::Remote(_, location) => location.clone(), diff --git a/dmp-core/src/music_storage/utils.rs b/dmp-core/src/music_storage/utils.rs index 991d3d7..a91299d 100644 --- a/dmp-core/src/music_storage/utils.rs +++ b/dmp-core/src/music_storage/utils.rs @@ -94,4 +94,4 @@ pub fn find_images(song_path: &Path) -> Result, Box> { } Ok(images) -} \ No newline at end of file +} diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index ec4e613..0bb656d 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -28,12 +28,12 @@ futures = "0.3.31" crossbeam = "0.8.4" directories = "5.0.1" uuid = { version = "1.11.0", features = ["v4", "serde"] } -ciborium = "0.2.2" mime = "0.3.17" file-format = "0.26.0" chrono = { version = "0.4.38", features = ["serde"] } itertools = "0.13.0" rfd = "0.15.1" +colog = "1.3.0" [features] default = [ "custom-protocol" ] diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index d51c41b..5147b00 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -2,8 +2,8 @@ use std::{fs, path::PathBuf, str::FromStr, thread::spawn}; use commands::{add_song_to_queue, play_now}; use crossbeam::channel::{unbounded, Receiver, Sender}; -use dmp_core::{config::{Config, ConfigLibrary}, music_controller::controller::{Controller, ControllerHandle, LibraryResponse}, music_player::gstreamer::GStreamer, music_storage::library::{AlbumArt, MusicLibrary}}; -use tauri::{http::Response, Emitter, Manager, State, Url, WebviewWindowBuilder, Wry}; +use dmp_core::{config::{Config, ConfigLibrary}, music_controller::controller::{Controller, ControllerHandle, LibraryResponse}, music_storage::library::MusicLibrary}; +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}; @@ -55,7 +55,7 @@ pub fn run() { handle_rx.send(handle).unwrap(); - let _controller = futures::executor::block_on(Controller::::start(input)).unwrap(); + let _controller = futures::executor::block_on(Controller::start(input)).unwrap(); }); let app = tauri::Builder::default() .plugin(tauri_plugin_shell::init()) diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 0355cd9..63318ee 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -3,5 +3,7 @@ fn main() { + colog::init(); + dango_music_player_lib::run() }