mirror of
https://github.com/johrpan/musicus.git
synced 2025-10-26 19:57:25 +01:00
Add user preferences for random playback
This commit is contained in:
parent
3ae5727f39
commit
5c64bdef7e
14 changed files with 402 additions and 156 deletions
|
|
@ -1,7 +1,11 @@
|
|||
use gio::traits::SettingsExt;
|
||||
use log::warn;
|
||||
use musicus_database::Database;
|
||||
use std::cell::RefCell;
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
path::PathBuf,
|
||||
rc::Rc,
|
||||
};
|
||||
use tokio::sync::{broadcast, broadcast::Sender};
|
||||
|
||||
pub use musicus_database as db;
|
||||
|
|
@ -55,6 +59,12 @@ pub struct Backend {
|
|||
/// 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 {
|
||||
|
|
@ -73,6 +83,8 @@ impl Backend {
|
|||
library_updated_sender,
|
||||
database: RefCell::new(None),
|
||||
player: RefCell::new(None),
|
||||
keep_playing: Cell::new(false),
|
||||
play_full_recordings: Cell::new(true),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -83,8 +95,12 @@ impl Backend {
|
|||
|
||||
/// Initialize the backend. A state callback should already have been registered using
|
||||
/// [`set_state_cb()`] to react to the result.
|
||||
pub fn init(&self) -> Result<()> {
|
||||
self.init_library()?;
|
||||
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),
|
||||
|
|
@ -94,12 +110,68 @@ impl Backend {
|
|||
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 {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
use crate::{Backend, BackendState, Player, Result};
|
||||
use gio::prelude::*;
|
||||
use glib::clone;
|
||||
use log::warn;
|
||||
use musicus_database::Database;
|
||||
use std::path::PathBuf;
|
||||
|
|
@ -8,7 +7,7 @@ use std::rc::Rc;
|
|||
|
||||
impl Backend {
|
||||
/// Initialize the music library if it is set in the settings.
|
||||
pub(super) fn init_library(&self) -> Result<()> {
|
||||
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()))?;
|
||||
|
|
@ -18,7 +17,7 @@ impl Backend {
|
|||
}
|
||||
|
||||
/// Set the path to the music library folder and connect to the database.
|
||||
pub fn set_music_library_path(&self, path: PathBuf) -> Result<()> {
|
||||
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())
|
||||
|
|
@ -34,7 +33,7 @@ impl Backend {
|
|||
}
|
||||
|
||||
/// Set the path to the music library folder and and connect to the database.
|
||||
pub fn set_music_library_path_priv(&self, path: PathBuf) -> Result<()> {
|
||||
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()));
|
||||
|
|
@ -46,14 +45,10 @@ impl Backend {
|
|||
self.database.replace(Some(Rc::clone(&database)));
|
||||
|
||||
let player = Player::new(path);
|
||||
|
||||
// Keep adding random tracks in case the playlist ends.
|
||||
player.set_generate_next_track_cb(clone!(@weak database => @default-panic, move || {
|
||||
database.random_track().unwrap()
|
||||
}));
|
||||
|
||||
self.player.replace(Some(player));
|
||||
|
||||
Rc::clone(&self).update_track_generator();
|
||||
|
||||
self.set_state(BackendState::Ready);
|
||||
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{Error, Result};
|
||||
use crate::{Backend, Error, Result};
|
||||
use glib::clone;
|
||||
use gstreamer_player::prelude::*;
|
||||
use musicus_database::Track;
|
||||
|
|
@ -17,7 +17,7 @@ pub struct Player {
|
|||
current_track: Cell<Option<usize>>,
|
||||
playing: Cell<bool>,
|
||||
duration: Cell<u64>,
|
||||
generate_next_track_cb: RefCell<Option<Box<dyn Fn() -> Track>>>,
|
||||
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)>>>,
|
||||
|
|
@ -45,7 +45,7 @@ impl Player {
|
|||
current_track: Cell::new(None),
|
||||
playing: Cell::new(false),
|
||||
duration: Cell::new(0),
|
||||
generate_next_track_cb: RefCell::new(None),
|
||||
track_generator: RefCell::new(None),
|
||||
playlist_cbs: RefCell::new(Vec::new()),
|
||||
track_cbs: RefCell::new(Vec::new()),
|
||||
duration_cbs: RefCell::new(Vec::new()),
|
||||
|
|
@ -146,8 +146,11 @@ impl Player {
|
|||
result
|
||||
}
|
||||
|
||||
pub fn set_generate_next_track_cb<F: Fn() -> Track + 'static>(&self, cb: F) {
|
||||
self.generate_next_track_cb.replace(Some(Box::new(cb)));
|
||||
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) {
|
||||
|
|
@ -190,12 +193,17 @@ impl Player {
|
|||
self.playing.get()
|
||||
}
|
||||
|
||||
pub fn add_item(&self, item: Track) -> Result<()> {
|
||||
/// 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.push(item);
|
||||
playlist.append(&mut items);
|
||||
|
||||
was_empty
|
||||
};
|
||||
|
|
@ -282,8 +290,8 @@ impl Player {
|
|||
}
|
||||
|
||||
pub fn has_next(&self) -> bool {
|
||||
if self.generate_next_track_cb.borrow().is_some() {
|
||||
true
|
||||
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()
|
||||
|
|
@ -294,13 +302,19 @@ impl Player {
|
|||
|
||||
pub fn next(&self) -> Result<()> {
|
||||
let current_track = self.current_track.get();
|
||||
let cb = self.generate_next_track_cb.borrow();
|
||||
let generator = self.track_generator.borrow();
|
||||
|
||||
if let Some(current_track) = current_track {
|
||||
if current_track + 1 >= self.playlist.borrow().len() {
|
||||
if let Some(cb) = &*cb {
|
||||
let new_track = cb();
|
||||
self.add_item(new_track)?;
|
||||
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.")));
|
||||
}
|
||||
|
|
@ -309,9 +323,15 @@ impl Player {
|
|||
self.set_track(current_track + 1)?;
|
||||
|
||||
Ok(())
|
||||
} else if let Some(cb) = &*cb {
|
||||
let new_track = cb();
|
||||
self.add_item(new_track)?;
|
||||
} 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 {
|
||||
|
|
@ -406,3 +426,58 @@ impl Player {
|
|||
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