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
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