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>>,
/// 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 >,
/// 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 >>,
/// The player handling playlist and playback. This can be assumed to exist, when the state is
/// set to BackendState::Ready.
player: RefCell >>,
/// Whether to keep playing random tracks after the playlist ends.
keep_playing: Cell,
/// Whether to choose full recordings for random playback.
play_full_recordings: Cell,
}
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(&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) -> 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, 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, 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) {
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::);
}
}
}
}
impl Default for Backend {
fn default() -> Self {
Self::new()
}
}