mirror of
				https://github.com/johrpan/musicus.git
				synced 2025-10-26 19:57:25 +01:00 
			
		
		
		
	Move crates to subdirectory
This commit is contained in:
		
							parent
							
								
									1db96062fb
								
							
						
					
					
						commit
						ac4b29e86d
					
				
					 115 changed files with 10 additions and 5 deletions
				
			
		
							
								
								
									
										19
									
								
								crates/backend/Cargo.toml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								crates/backend/Cargo.toml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,19 @@ | |||
| [package] | ||||
| name = "musicus_backend" | ||||
| version = "0.1.0" | ||||
| edition = "2021" | ||||
| 
 | ||||
| [dependencies] | ||||
| fragile = "1.2.0" | ||||
| gio = "0.15.11" | ||||
| glib = "0.15.11" | ||||
| gstreamer = "0.18.8" | ||||
| gstreamer-player = "0.18.0" | ||||
| log = { version = "0.4.16", features = ["std"] } | ||||
| musicus_database = { version = "0.1.0", path = "../database" } | ||||
| musicus_import = { version = "0.1.0", path = "../import" } | ||||
| thiserror = "1.0.31" | ||||
| tokio = { version = "1.18.0", features = ["sync"] } | ||||
| 
 | ||||
| [target.'cfg(target_os = "linux")'.dependencies] | ||||
| mpris-player = "0.6.1" | ||||
							
								
								
									
										17
									
								
								crates/backend/src/error.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								crates/backend/src/error.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,17 @@ | |||
| /// An error that happened within the backend.
 | ||||
| #[derive(thiserror::Error, Debug)] | ||||
| pub enum Error { | ||||
|     #[error(transparent)] | ||||
|     DatabaseError(#[from] musicus_database::Error), | ||||
| 
 | ||||
|     #[error("An error happened while decoding to UTF-8.")] | ||||
|     Utf8Error(#[from] std::str::Utf8Error), | ||||
| 
 | ||||
|     #[error("Failed to receive an event.")] | ||||
|     RecvError(#[from] tokio::sync::broadcast::error::RecvError), | ||||
| 
 | ||||
|     #[error("An error happened: {0}")] | ||||
|     Other(String), | ||||
| } | ||||
| 
 | ||||
| pub type Result<T> = std::result::Result<T, Error>; | ||||
							
								
								
									
										181
									
								
								crates/backend/src/lib.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										181
									
								
								crates/backend/src/lib.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,181 @@ | |||
| use gio::traits::SettingsExt; | ||||
| use log::warn; | ||||
| use musicus_database::Database; | ||||
| use std::{ | ||||
|     cell::{Cell, RefCell}, | ||||
|     path::PathBuf, | ||||
|     rc::Rc, | ||||
| }; | ||||
| use tokio::sync::{broadcast, broadcast::Sender}; | ||||
| 
 | ||||
| pub use musicus_database as db; | ||||
| pub use musicus_import as import; | ||||
| 
 | ||||
| pub mod error; | ||||
| pub use error::*; | ||||
| 
 | ||||
| pub mod library; | ||||
| pub use library::*; | ||||
| 
 | ||||
| mod logger; | ||||
| 
 | ||||
| pub mod player; | ||||
| pub use player::*; | ||||
| 
 | ||||
| /// General states the application can be in.
 | ||||
| #[derive(Debug, Copy, Clone)] | ||||
| pub enum BackendState { | ||||
|     /// The backend is not set up yet. This means that no backend methods except for setting the
 | ||||
|     /// music library path should be called. The user interface should adapt and only present this
 | ||||
|     /// option.
 | ||||
|     NoMusicLibrary, | ||||
| 
 | ||||
|     /// The backend is loading the music library. No methods should be called. The user interface
 | ||||
|     /// should represent that state by prohibiting all interaction.
 | ||||
|     Loading, | ||||
| 
 | ||||
|     /// The backend is ready and all methods may be called.
 | ||||
|     Ready, | ||||
| } | ||||
| 
 | ||||
| /// A collection of all backend state and functionality.
 | ||||
| pub struct Backend { | ||||
|     /// A closure that will be called whenever the backend state changes.
 | ||||
|     state_cb: RefCell<Option<Box<dyn Fn(BackendState)>>>, | ||||
| 
 | ||||
|     /// Access to GSettings.
 | ||||
|     settings: gio::Settings, | ||||
| 
 | ||||
|     /// The current path to the music library, which is used by the player and the database. This
 | ||||
|     /// is guaranteed to be Some, when the state is set to BackendState::Ready.
 | ||||
|     music_library_path: RefCell<Option<PathBuf>>, | ||||
| 
 | ||||
|     /// The sender for sending library update notifications.
 | ||||
|     library_updated_sender: Sender<()>, | ||||
| 
 | ||||
|     /// The database. This can be assumed to exist, when the state is set to BackendState::Ready.
 | ||||
|     database: RefCell<Option<Rc<Database>>>, | ||||
| 
 | ||||
|     /// The player handling playlist and playback. This can be assumed to exist, when the state is
 | ||||
|     /// set to BackendState::Ready.
 | ||||
|     player: RefCell<Option<Rc<Player>>>, | ||||
| 
 | ||||
|     /// Whether to keep playing random tracks after the playlist ends.
 | ||||
|     keep_playing: Cell<bool>, | ||||
| 
 | ||||
|     /// Whether to choose full recordings for random playback.
 | ||||
|     play_full_recordings: Cell<bool>, | ||||
| } | ||||
| 
 | ||||
| impl Backend { | ||||
|     /// Create a new backend initerface. The user interface should subscribe to the state stream
 | ||||
|     /// and call init() afterwards. There may be only one backend for a process and this method
 | ||||
|     /// may only be called exactly once. Otherwise it will panic.
 | ||||
|     pub fn new() -> Self { | ||||
|         logger::register(); | ||||
| 
 | ||||
|         let (library_updated_sender, _) = broadcast::channel(1024); | ||||
| 
 | ||||
|         Backend { | ||||
|             state_cb: RefCell::new(None), | ||||
|             settings: gio::Settings::new("de.johrpan.musicus"), | ||||
|             music_library_path: RefCell::new(None), | ||||
|             library_updated_sender, | ||||
|             database: RefCell::new(None), | ||||
|             player: RefCell::new(None), | ||||
|             keep_playing: Cell::new(false), | ||||
|             play_full_recordings: Cell::new(true), | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /// Set the closure to be called whenever the backend state changes.
 | ||||
|     pub fn set_state_cb<F: Fn(BackendState) + 'static>(&self, cb: F) { | ||||
|         self.state_cb.replace(Some(Box::new(cb))); | ||||
|     } | ||||
| 
 | ||||
|     /// Initialize the backend. A state callback should already have been registered using
 | ||||
|     /// [`set_state_cb()`] to react to the result.
 | ||||
|     pub fn init(self: Rc<Self>) -> Result<()> { | ||||
|         self.keep_playing.set(self.settings.boolean("keep-playing")); | ||||
|         self.play_full_recordings | ||||
|             .set(self.settings.boolean("play-full-recordings")); | ||||
| 
 | ||||
|         Rc::clone(&self).init_library()?; | ||||
| 
 | ||||
|         match self.get_music_library_path() { | ||||
|             None => self.set_state(BackendState::NoMusicLibrary), | ||||
|             Some(_) => self.set_state(BackendState::Ready), | ||||
|         }; | ||||
| 
 | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     /// Whether to keep playing random tracks after the playlist ends.
 | ||||
|     pub fn keep_playing(&self) -> bool { | ||||
|         self.keep_playing.get() | ||||
|     } | ||||
| 
 | ||||
|     /// Set whether to keep playing random tracks after the playlist ends.
 | ||||
|     pub fn set_keep_playing(self: Rc<Self>, keep_playing: bool) { | ||||
|         if let Err(err) = self.settings.set_boolean("keep-playing", keep_playing) { | ||||
|             warn!( | ||||
|                 "The preference \"keep-playing\" could not be saved using GSettings. It will most \ | ||||
|                 likely not be available at the next startup. Error message: {}",
 | ||||
|                 err | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         self.keep_playing.set(keep_playing); | ||||
|         self.update_track_generator(); | ||||
|     } | ||||
| 
 | ||||
|     /// Whether to choose full recordings for random playback.
 | ||||
|     pub fn play_full_recordings(&self) -> bool { | ||||
|         self.play_full_recordings.get() | ||||
|     } | ||||
| 
 | ||||
|     /// Set whether to choose full recordings for random playback.
 | ||||
|     pub fn set_play_full_recordings(self: Rc<Self>, play_full_recordings: bool) { | ||||
|         if let Err(err) = self | ||||
|             .settings | ||||
|             .set_boolean("play-full-recordings", play_full_recordings) | ||||
|         { | ||||
|             warn!( | ||||
|                 "The preference \"play-full-recordings\" could not be saved using GSettings. It \ | ||||
|                 will most likely not be available at the next startup. Error message: {}",
 | ||||
|                 err | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         self.play_full_recordings.set(play_full_recordings); | ||||
|         self.update_track_generator(); | ||||
|     } | ||||
| 
 | ||||
|     /// Set the current state and notify the user interface.
 | ||||
|     fn set_state(&self, state: BackendState) { | ||||
|         if let Some(cb) = &*self.state_cb.borrow() { | ||||
|             cb(state); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /// Apply the current track generation settings.
 | ||||
|     fn update_track_generator(self: Rc<Self>) { | ||||
|         if let Some(player) = self.get_player() { | ||||
|             if self.keep_playing() { | ||||
|                 if self.play_full_recordings() { | ||||
|                     player.set_track_generator(Some(RandomRecordingGenerator::new(self))); | ||||
|                 } else { | ||||
|                     player.set_track_generator(Some(RandomTrackGenerator::new(self))); | ||||
|                 } | ||||
|             } else { | ||||
|                 player.set_track_generator(None::<RandomRecordingGenerator>); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl Default for Backend { | ||||
|     fn default() -> Self { | ||||
|         Self::new() | ||||
|     } | ||||
| } | ||||
							
								
								
									
										86
									
								
								crates/backend/src/library.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								crates/backend/src/library.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,86 @@ | |||
| use crate::{Backend, BackendState, Player, Result}; | ||||
| use gio::prelude::*; | ||||
| use log::warn; | ||||
| use musicus_database::Database; | ||||
| use std::path::PathBuf; | ||||
| use std::rc::Rc; | ||||
| 
 | ||||
| impl Backend { | ||||
|     /// Initialize the music library if it is set in the settings.
 | ||||
|     pub(super) fn init_library(self: Rc<Self>) -> Result<()> { | ||||
|         let path = self.settings.string("music-library-path"); | ||||
|         if !path.is_empty() { | ||||
|             self.set_music_library_path_priv(PathBuf::from(path.to_string()))?; | ||||
|         } | ||||
| 
 | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     /// Set the path to the music library folder and connect to the database.
 | ||||
|     pub fn set_music_library_path(self: Rc<Self>, path: PathBuf) -> Result<()> { | ||||
|         if let Err(err) = self | ||||
|             .settings | ||||
|             .set_string("music-library-path", path.to_str().unwrap()) | ||||
|         { | ||||
|             warn!( | ||||
|                 "The music library path could not be saved using GSettings. It will most likely \ | ||||
|                 not be available at the next startup. Error message: {}",
 | ||||
|                 err | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         self.set_music_library_path_priv(path) | ||||
|     } | ||||
| 
 | ||||
|     /// Set the path to the music library folder and and connect to the database.
 | ||||
|     pub fn set_music_library_path_priv(self: Rc<Self>, path: PathBuf) -> Result<()> { | ||||
|         self.set_state(BackendState::Loading); | ||||
| 
 | ||||
|         self.music_library_path.replace(Some(path.clone())); | ||||
| 
 | ||||
|         let mut db_path = path.clone(); | ||||
|         db_path.push("musicus.db"); | ||||
| 
 | ||||
|         let database = Rc::new(Database::new(db_path.to_str().unwrap())?); | ||||
|         self.database.replace(Some(Rc::clone(&database))); | ||||
| 
 | ||||
|         let player = Player::new(path); | ||||
|         self.player.replace(Some(player)); | ||||
| 
 | ||||
|         Rc::clone(&self).update_track_generator(); | ||||
| 
 | ||||
|         self.set_state(BackendState::Ready); | ||||
| 
 | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     /// Get the currently set music library path.
 | ||||
|     pub fn get_music_library_path(&self) -> Option<PathBuf> { | ||||
|         self.music_library_path.borrow().clone() | ||||
|     } | ||||
| 
 | ||||
|     /// Get an interface to the database and panic if there is none.
 | ||||
|     pub fn db(&self) -> Rc<Database> { | ||||
|         self.database.borrow().clone().unwrap() | ||||
|     } | ||||
| 
 | ||||
|     /// Get an interface to the playback service.
 | ||||
|     pub fn get_player(&self) -> Option<Rc<Player>> { | ||||
|         self.player.borrow().clone() | ||||
|     } | ||||
| 
 | ||||
|     /// Wait for the next library update.
 | ||||
|     pub async fn library_update(&self) -> Result<()> { | ||||
|         Ok(self.library_updated_sender.subscribe().recv().await?) | ||||
|     } | ||||
| 
 | ||||
|     /// Notify the frontend that the library was changed.
 | ||||
|     pub fn library_changed(&self) { | ||||
|         self.library_updated_sender.send(()).unwrap(); | ||||
|     } | ||||
| 
 | ||||
|     /// Get an interface to the player and panic if there is none.
 | ||||
|     pub fn pl(&self) -> Rc<Player> { | ||||
|         self.get_player().unwrap() | ||||
|     } | ||||
| } | ||||
							
								
								
									
										63
									
								
								crates/backend/src/logger.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								crates/backend/src/logger.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,63 @@ | |||
| use log::{Level, LevelFilter, Log, Metadata, Record}; | ||||
| use std::{fmt::Display, sync::Mutex}; | ||||
| 
 | ||||
| /// Register the custom logger. This will panic if called more than once.
 | ||||
| pub fn register() { | ||||
|     log::set_boxed_logger(Box::new(Logger::default())) | ||||
|         .map(|()| log::set_max_level(LevelFilter::Info)) | ||||
|         .unwrap(); | ||||
| } | ||||
| 
 | ||||
| /// A simple logging handler that prints out all messages and caches them for
 | ||||
| /// later access by the user interface.
 | ||||
| struct Logger { | ||||
|     /// All messages since the start of the program.
 | ||||
|     messages: Mutex<Vec<LogMessage>>, | ||||
| } | ||||
| 
 | ||||
| impl Default for Logger { | ||||
|     fn default() -> Self { | ||||
|         Self { | ||||
|             messages: Mutex::new(Vec::new()), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl Log for Logger { | ||||
|     fn enabled(&self, metadata: &Metadata) -> bool { | ||||
|         metadata.level() <= Level::Info | ||||
|     } | ||||
| 
 | ||||
|     fn log(&self, record: &Record) { | ||||
|         if record.level() <= Level::Info { | ||||
|             let message = record.into(); | ||||
|             println!("{}", message); | ||||
|             self.messages.lock().unwrap().push(message); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fn flush(&self) {} | ||||
| } | ||||
| 
 | ||||
| /// A simplified representation of a [`Record`].
 | ||||
| struct LogMessage { | ||||
|     pub level: String, | ||||
|     pub module: String, | ||||
|     pub message: String, | ||||
| } | ||||
| 
 | ||||
| impl<'a> From<&Record<'a>> for LogMessage { | ||||
|     fn from(record: &Record<'a>) -> Self { | ||||
|         Self { | ||||
|             level: record.level().to_string(), | ||||
|             module: String::from(record.module_path().unwrap_or_else(|| record.target())), | ||||
|             message: format!("{}", record.args()), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl Display for LogMessage { | ||||
|     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||
|         write!(f, "{} ({}): {}", self.module, self.level, self.message) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										483
									
								
								crates/backend/src/player.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										483
									
								
								crates/backend/src/player.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,483 @@ | |||
| use crate::{Backend, Error, Result}; | ||||
| use glib::clone; | ||||
| use gstreamer_player::prelude::*; | ||||
| use musicus_database::Track; | ||||
| use std::cell::{Cell, RefCell}; | ||||
| use std::path::PathBuf; | ||||
| use std::rc::Rc; | ||||
| use std::sync::Arc; | ||||
| 
 | ||||
| #[cfg(target_os = "linux")] | ||||
| use mpris_player::{Metadata, MprisPlayer, PlaybackStatus}; | ||||
| 
 | ||||
| pub struct Player { | ||||
|     music_library_path: PathBuf, | ||||
|     player: gstreamer_player::Player, | ||||
|     playlist: RefCell<Vec<Track>>, | ||||
|     current_track: Cell<Option<usize>>, | ||||
|     playing: Cell<bool>, | ||||
|     duration: Cell<u64>, | ||||
|     track_generator: RefCell<Option<Box<dyn TrackGenerator>>>, | ||||
|     playlist_cbs: RefCell<Vec<Box<dyn Fn(Vec<Track>)>>>, | ||||
|     track_cbs: RefCell<Vec<Box<dyn Fn(usize)>>>, | ||||
|     duration_cbs: RefCell<Vec<Box<dyn Fn(u64)>>>, | ||||
|     playing_cbs: RefCell<Vec<Box<dyn Fn(bool)>>>, | ||||
|     position_cbs: RefCell<Vec<Box<dyn Fn(u64)>>>, | ||||
|     raise_cb: RefCell<Option<Box<dyn Fn()>>>, | ||||
| 
 | ||||
|     #[cfg(target_os = "linux")] | ||||
|     mpris: Arc<MprisPlayer>, | ||||
| } | ||||
| 
 | ||||
| impl Player { | ||||
|     pub fn new(music_library_path: PathBuf) -> Rc<Self> { | ||||
|         let dispatcher = gstreamer_player::PlayerGMainContextSignalDispatcher::new(None); | ||||
|         let player = gstreamer_player::Player::new(None, Some(&dispatcher.upcast())); | ||||
|         let mut config = player.config(); | ||||
|         config.set_position_update_interval(250); | ||||
|         player.set_config(config).unwrap(); | ||||
|         player.set_video_track_enabled(false); | ||||
| 
 | ||||
|         let result = Rc::new(Self { | ||||
|             music_library_path, | ||||
|             player: player.clone(), | ||||
|             playlist: RefCell::new(Vec::new()), | ||||
|             current_track: Cell::new(None), | ||||
|             playing: Cell::new(false), | ||||
|             duration: Cell::new(0), | ||||
|             track_generator: RefCell::new(None), | ||||
|             playlist_cbs: RefCell::new(Vec::new()), | ||||
|             track_cbs: RefCell::new(Vec::new()), | ||||
|             duration_cbs: RefCell::new(Vec::new()), | ||||
|             playing_cbs: RefCell::new(Vec::new()), | ||||
|             position_cbs: RefCell::new(Vec::new()), | ||||
|             raise_cb: RefCell::new(None), | ||||
|             #[cfg(target_os = "linux")] | ||||
|             mpris: { | ||||
|                 let mpris = MprisPlayer::new( | ||||
|                     "de.johrpan.musicus".to_string(), | ||||
|                     "Musicus".to_string(), | ||||
|                     "de.johrpan.musicus.desktop".to_string(), | ||||
|                 ); | ||||
| 
 | ||||
|                 mpris.set_can_raise(true); | ||||
|                 mpris.set_can_play(false); | ||||
|                 mpris.set_can_go_previous(false); | ||||
|                 mpris.set_can_go_next(false); | ||||
|                 mpris.set_can_seek(false); | ||||
|                 mpris.set_can_set_fullscreen(false); | ||||
| 
 | ||||
|                 mpris | ||||
|             }, | ||||
|         }); | ||||
| 
 | ||||
|         let clone = fragile::Fragile::new(result.clone()); | ||||
|         player.connect_end_of_stream(move |_| { | ||||
|             let clone = clone.get(); | ||||
|             if clone.has_next() { | ||||
|                 clone.next().unwrap(); | ||||
|             } else { | ||||
|                 clone.player.stop(); | ||||
|                 clone.playing.replace(false); | ||||
| 
 | ||||
|                 for cb in &*clone.playing_cbs.borrow() { | ||||
|                     cb(false); | ||||
|                 } | ||||
| 
 | ||||
|                 #[cfg(target_os = "linux")] | ||||
|                 clone.mpris.set_playback_status(PlaybackStatus::Paused); | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         let clone = fragile::Fragile::new(result.clone()); | ||||
|         player.connect_position_updated(move |_, position| { | ||||
|             for cb in &*clone.get().position_cbs.borrow() { | ||||
|                 cb(position.unwrap().mseconds()); | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         let clone = fragile::Fragile::new(result.clone()); | ||||
|         player.connect_duration_changed(move |_, duration| { | ||||
|             for cb in &*clone.get().duration_cbs.borrow() { | ||||
|                 let duration = duration.unwrap().mseconds(); | ||||
|                 clone.get().duration.set(duration); | ||||
|                 cb(duration); | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         #[cfg(target_os = "linux")] | ||||
|         { | ||||
|             result | ||||
|                 .mpris | ||||
|                 .connect_play_pause(clone!(@weak result => move || { | ||||
|                     result.play_pause().unwrap(); | ||||
|                 })); | ||||
| 
 | ||||
|             result.mpris.connect_play(clone!(@weak result => move || { | ||||
|                 if !result.is_playing() { | ||||
|                     result.play_pause().unwrap(); | ||||
|                 } | ||||
|             })); | ||||
| 
 | ||||
|             result.mpris.connect_pause(clone!(@weak result => move || { | ||||
|                 if result.is_playing() { | ||||
|                     result.play_pause().unwrap(); | ||||
|                 } | ||||
|             })); | ||||
| 
 | ||||
|             result | ||||
|                 .mpris | ||||
|                 .connect_previous(clone!(@weak result => move || { | ||||
|                     let _ = result.previous(); | ||||
|                 })); | ||||
| 
 | ||||
|             result.mpris.connect_next(clone!(@weak result => move || { | ||||
|                 let _ = result.next(); | ||||
|             })); | ||||
| 
 | ||||
|             result.mpris.connect_raise(clone!(@weak result => move || { | ||||
|                 let cb = result.raise_cb.borrow(); | ||||
|                 if let Some(cb) = &*cb { | ||||
|                     cb() | ||||
|                 } | ||||
|             })); | ||||
|         } | ||||
| 
 | ||||
|         result | ||||
|     } | ||||
| 
 | ||||
|     pub fn set_track_generator<G: TrackGenerator + 'static>(&self, generator: Option<G>) { | ||||
|         self.track_generator.replace(match generator { | ||||
|             Some(generator) => Some(Box::new(generator)), | ||||
|             None => None, | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     pub fn add_playlist_cb<F: Fn(Vec<Track>) + 'static>(&self, cb: F) { | ||||
|         self.playlist_cbs.borrow_mut().push(Box::new(cb)); | ||||
|     } | ||||
| 
 | ||||
|     pub fn add_track_cb<F: Fn(usize) + 'static>(&self, cb: F) { | ||||
|         self.track_cbs.borrow_mut().push(Box::new(cb)); | ||||
|     } | ||||
| 
 | ||||
|     pub fn add_duration_cb<F: Fn(u64) + 'static>(&self, cb: F) { | ||||
|         self.duration_cbs.borrow_mut().push(Box::new(cb)); | ||||
|     } | ||||
| 
 | ||||
|     pub fn add_playing_cb<F: Fn(bool) + 'static>(&self, cb: F) { | ||||
|         self.playing_cbs.borrow_mut().push(Box::new(cb)); | ||||
|     } | ||||
| 
 | ||||
|     pub fn add_position_cb<F: Fn(u64) + 'static>(&self, cb: F) { | ||||
|         self.position_cbs.borrow_mut().push(Box::new(cb)); | ||||
|     } | ||||
| 
 | ||||
|     pub fn set_raise_cb<F: Fn() + 'static>(&self, cb: F) { | ||||
|         self.raise_cb.replace(Some(Box::new(cb))); | ||||
|     } | ||||
| 
 | ||||
|     pub fn get_playlist(&self) -> Vec<Track> { | ||||
|         self.playlist.borrow().clone() | ||||
|     } | ||||
| 
 | ||||
|     pub fn get_current_track(&self) -> Option<usize> { | ||||
|         self.current_track.get() | ||||
|     } | ||||
| 
 | ||||
|     pub fn get_duration(&self) -> Option<gstreamer::ClockTime> { | ||||
|         self.player.duration() | ||||
|     } | ||||
| 
 | ||||
|     pub fn is_playing(&self) -> bool { | ||||
|         self.playing.get() | ||||
|     } | ||||
| 
 | ||||
|     /// Add some items to the playlist.
 | ||||
|     pub fn add_items(&self, mut items: Vec<Track>) -> Result<()> { | ||||
|         if items.is_empty() { | ||||
|             return Ok(()) | ||||
|         } | ||||
| 
 | ||||
|         let was_empty = { | ||||
|             let mut playlist = self.playlist.borrow_mut(); | ||||
|             let was_empty = playlist.is_empty(); | ||||
| 
 | ||||
|             playlist.append(&mut items); | ||||
| 
 | ||||
|             was_empty | ||||
|         }; | ||||
| 
 | ||||
|         for cb in &*self.playlist_cbs.borrow() { | ||||
|             cb(self.playlist.borrow().clone()); | ||||
|         } | ||||
| 
 | ||||
|         if was_empty { | ||||
|             self.set_track(0)?; | ||||
|             self.player.play(); | ||||
|             self.playing.set(true); | ||||
| 
 | ||||
|             for cb in &*self.playing_cbs.borrow() { | ||||
|                 cb(true); | ||||
|             } | ||||
| 
 | ||||
|             #[cfg(target_os = "linux")] | ||||
|             { | ||||
|                 self.mpris.set_can_play(true); | ||||
|                 self.mpris.set_playback_status(PlaybackStatus::Playing); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     pub fn play_pause(&self) -> Result<()> { | ||||
|         if self.is_playing() { | ||||
|             self.player.pause(); | ||||
|             self.playing.set(false); | ||||
| 
 | ||||
|             for cb in &*self.playing_cbs.borrow() { | ||||
|                 cb(false); | ||||
|             } | ||||
| 
 | ||||
|             #[cfg(target_os = "linux")] | ||||
|             self.mpris.set_playback_status(PlaybackStatus::Paused); | ||||
|         } else { | ||||
|             if self.current_track.get().is_none() { | ||||
|                 self.next()?; | ||||
|             } | ||||
| 
 | ||||
|             self.player.play(); | ||||
|             self.playing.set(true); | ||||
| 
 | ||||
|             for cb in &*self.playing_cbs.borrow() { | ||||
|                 cb(true); | ||||
|             } | ||||
| 
 | ||||
|             #[cfg(target_os = "linux")] | ||||
|             self.mpris.set_playback_status(PlaybackStatus::Playing); | ||||
|         } | ||||
| 
 | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     pub fn seek(&self, ms: u64) { | ||||
|         self.player.seek(gstreamer::ClockTime::from_mseconds(ms)); | ||||
|     } | ||||
| 
 | ||||
|     pub fn has_previous(&self) -> bool { | ||||
|         if let Some(current_track) = self.current_track.get() { | ||||
|             current_track > 0 | ||||
|         } else { | ||||
|             false | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     pub fn previous(&self) -> Result<()> { | ||||
|         let mut current_track = self.current_track.get().ok_or_else(|| { | ||||
|             Error::Other(String::from( | ||||
|                 "Player tried to access non existant current track.", | ||||
|             )) | ||||
|         })?; | ||||
| 
 | ||||
|         if current_track > 0 { | ||||
|             current_track -= 1; | ||||
|         } else { | ||||
|             return Err(Error::Other(String::from("No existing previous track."))); | ||||
|         } | ||||
| 
 | ||||
|         self.set_track(current_track) | ||||
|     } | ||||
| 
 | ||||
|     pub fn has_next(&self) -> bool { | ||||
|         if let Some(generator) = &*self.track_generator.borrow() { | ||||
|             generator.has_next() | ||||
|         } else if let Some(current_track) = self.current_track.get() { | ||||
|             let playlist = self.playlist.borrow(); | ||||
|             current_track + 1 < playlist.len() | ||||
|         } else { | ||||
|             false | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     pub fn next(&self) -> Result<()> { | ||||
|         let current_track = self.current_track.get(); | ||||
|         let generator = self.track_generator.borrow(); | ||||
| 
 | ||||
|         if let Some(current_track) = current_track { | ||||
|             if current_track + 1 >= self.playlist.borrow().len() { | ||||
|                 if let Some(generator) = &*generator { | ||||
|                     let items = generator.next(); | ||||
|                     if !items.is_empty() { | ||||
|                         self.add_items(items)?; | ||||
|                     } else { | ||||
|                         return Err(Error::Other(String::from( | ||||
|                             "Track generator failed to generate next track.", | ||||
|                         ))); | ||||
|                     } | ||||
|                 } else { | ||||
|                     return Err(Error::Other(String::from("No existing next track."))); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             self.set_track(current_track + 1)?; | ||||
| 
 | ||||
|             Ok(()) | ||||
|         } else if let Some(generator) = &*generator { | ||||
|             let items = generator.next(); | ||||
|             if !items.is_empty() { | ||||
|                 self.add_items(items)?; | ||||
|             } else { | ||||
|                 return Err(Error::Other(String::from( | ||||
|                     "Track generator failed to generate next track.", | ||||
|                 ))); | ||||
|             } | ||||
| 
 | ||||
|             Ok(()) | ||||
|         } else { | ||||
|             Err(Error::Other(String::from("No existing next track."))) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     pub fn set_track(&self, current_track: usize) -> Result<()> { | ||||
|         let track = &self.playlist.borrow()[current_track]; | ||||
| 
 | ||||
|         let path = self | ||||
|             .music_library_path | ||||
|             .join(track.path.clone()) | ||||
|             .into_os_string() | ||||
|             .into_string() | ||||
|             .unwrap(); | ||||
| 
 | ||||
|         let uri = glib::filename_to_uri(&path, None) | ||||
|             .map_err(|_| Error::Other(format!("Failed to create URI from path: {}", path)))?; | ||||
| 
 | ||||
|         self.player.set_uri(Some(&uri)); | ||||
| 
 | ||||
|         if self.is_playing() { | ||||
|             self.player.play(); | ||||
|         } | ||||
| 
 | ||||
|         self.current_track.set(Some(current_track)); | ||||
| 
 | ||||
|         for cb in &*self.track_cbs.borrow() { | ||||
|             cb(current_track); | ||||
|         } | ||||
| 
 | ||||
|         #[cfg(target_os = "linux")] | ||||
|         { | ||||
|             let mut parts = Vec::<String>::new(); | ||||
|             for part in &track.work_parts { | ||||
|                 parts.push(track.recording.work.parts[*part].title.clone()); | ||||
|             } | ||||
| 
 | ||||
|             let mut title = track.recording.work.get_title(); | ||||
|             if !parts.is_empty() { | ||||
|                 title = format!("{}: {}", title, parts.join(", ")); | ||||
|             } | ||||
| 
 | ||||
|             let subtitle = track.recording.get_performers(); | ||||
| 
 | ||||
|             let mut metadata = Metadata::new(); | ||||
|             metadata.artist = Some(vec![title]); | ||||
|             metadata.title = Some(subtitle); | ||||
| 
 | ||||
|             self.mpris.set_metadata(metadata); | ||||
|             self.mpris.set_can_go_previous(self.has_previous()); | ||||
|             self.mpris.set_can_go_next(self.has_next()); | ||||
|         } | ||||
| 
 | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     pub fn send_data(&self) { | ||||
|         for cb in &*self.playlist_cbs.borrow() { | ||||
|             cb(self.playlist.borrow().clone()); | ||||
|         } | ||||
| 
 | ||||
|         for cb in &*self.track_cbs.borrow() { | ||||
|             cb(self.current_track.get().unwrap()); | ||||
|         } | ||||
| 
 | ||||
|         for cb in &*self.duration_cbs.borrow() { | ||||
|             cb(self.duration.get()); | ||||
|         } | ||||
| 
 | ||||
|         for cb in &*self.playing_cbs.borrow() { | ||||
|             cb(self.is_playing()); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     pub fn clear(&self) { | ||||
|         self.player.stop(); | ||||
|         self.playing.set(false); | ||||
|         self.current_track.set(None); | ||||
|         self.playlist.replace(Vec::new()); | ||||
| 
 | ||||
|         for cb in &*self.playing_cbs.borrow() { | ||||
|             cb(false); | ||||
|         } | ||||
| 
 | ||||
|         for cb in &*self.playlist_cbs.borrow() { | ||||
|             cb(Vec::new()); | ||||
|         } | ||||
| 
 | ||||
|         #[cfg(target_os = "linux")] | ||||
|         self.mpris.set_can_play(false); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /// Generator for new tracks to be appended to the playlist.
 | ||||
| pub trait TrackGenerator { | ||||
|     /// Whether the generator will provide a next track if asked.
 | ||||
|     fn has_next(&self) -> bool; | ||||
| 
 | ||||
|     /// Provide the next track.
 | ||||
|     ///
 | ||||
|     /// This function should always return at least one track in a state where
 | ||||
|     /// `has_next()` returns `true`.
 | ||||
|     fn next(&self) -> Vec<Track>; | ||||
| } | ||||
| 
 | ||||
| /// A track generator that generates one random track per call.
 | ||||
| pub struct RandomTrackGenerator { | ||||
|     backend: Rc<Backend>, | ||||
| } | ||||
| 
 | ||||
| impl RandomTrackGenerator { | ||||
|     pub fn new(backend: Rc<Backend>) -> Self { | ||||
|         Self { backend } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl TrackGenerator for RandomTrackGenerator { | ||||
|     fn has_next(&self) -> bool { | ||||
|         true | ||||
|     } | ||||
| 
 | ||||
|     fn next(&self) -> Vec<Track> { | ||||
|         vec![self.backend.db().random_track().unwrap()] | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /// A track generator that returns the tracks of one random recording per call.
 | ||||
| pub struct RandomRecordingGenerator { | ||||
|     backend: Rc<Backend>, | ||||
| } | ||||
| 
 | ||||
| impl RandomRecordingGenerator { | ||||
|     pub fn new(backend: Rc<Backend>) -> Self { | ||||
|         Self { backend } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl TrackGenerator for RandomRecordingGenerator { | ||||
|     fn has_next(&self) -> bool { | ||||
|         true | ||||
|     } | ||||
| 
 | ||||
|     fn next(&self) -> Vec<Track> { | ||||
|         let recording = self.backend.db().random_recording().unwrap(); | ||||
|         self.backend.db().get_tracks(&recording.id).unwrap() | ||||
|     } | ||||
| } | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue