mirror of
https://github.com/johrpan/musicus.git
synced 2025-10-26 19:57:25 +01:00
Move crates to toplevel directory
This commit is contained in:
parent
d16961efa8
commit
0ffe68e04f
127 changed files with 15 additions and 13 deletions
20
backend/Cargo.toml
Normal file
20
backend/Cargo.toml
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
[package]
|
||||
name = "musicus_backend"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
fragile = "1.0.0"
|
||||
futures = "0.3.6"
|
||||
futures-channel = "0.3.5"
|
||||
gio = "0.9.1"
|
||||
glib = "0.10.3"
|
||||
gstreamer = "0.16.4"
|
||||
gstreamer-player = "0.16.3"
|
||||
log = "0.4.14"
|
||||
mpris-player = "0.6.0"
|
||||
musicus_client = { version = "0.1.0", path = "../client" }
|
||||
musicus_database = { version = "0.1.0", path = "../database" }
|
||||
secret-service = "2.0.1"
|
||||
thiserror = "1.0.23"
|
||||
|
||||
25
backend/src/error.rs
Normal file
25
backend/src/error.rs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
/// An error that can happened within the backend.
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
ClientError(#[from] musicus_client::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
DatabaseError(#[from] musicus_database::Error),
|
||||
|
||||
#[error("An error happened using the SecretService.")]
|
||||
SecretServiceError(#[from] secret_service::Error),
|
||||
|
||||
#[error("A channel was canceled.")]
|
||||
ChannelError(#[from] futures_channel::oneshot::Canceled),
|
||||
|
||||
#[error("An error happened while decoding to UTF-8.")]
|
||||
Utf8Error(#[from] std::str::Utf8Error),
|
||||
|
||||
#[error("An error happened: {0}")]
|
||||
Other(&'static str),
|
||||
}
|
||||
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
163
backend/src/lib.rs
Normal file
163
backend/src/lib.rs
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
use futures::prelude::*;
|
||||
use futures_channel::mpsc;
|
||||
use gio::prelude::*;
|
||||
use log::warn;
|
||||
use musicus_client::{Client, LoginData};
|
||||
use musicus_database::DbThread;
|
||||
use std::cell::RefCell;
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
|
||||
pub use musicus_client as client;
|
||||
pub use musicus_database as db;
|
||||
|
||||
pub mod error;
|
||||
pub use error::*;
|
||||
|
||||
pub mod library;
|
||||
pub use library::*;
|
||||
|
||||
pub mod player;
|
||||
pub use player::*;
|
||||
|
||||
mod secure;
|
||||
|
||||
/// General states the application can be in.
|
||||
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 future resolving to the next state of the backend. Initially, this should be assumed to
|
||||
/// be BackendState::Loading. Changes should be awaited before calling init().
|
||||
state_stream: RefCell<mpsc::Receiver<BackendState>>,
|
||||
|
||||
/// The internal sender to publish the state via state_stream.
|
||||
state_sender: RefCell<mpsc::Sender<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 database. This can be assumed to exist, when the state is set to BackendState::Ready.
|
||||
database: RefCell<Option<Rc<DbThread>>>,
|
||||
|
||||
/// 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>>>,
|
||||
|
||||
/// A client for the Wolfgang server.
|
||||
client: Client,
|
||||
}
|
||||
|
||||
impl Backend {
|
||||
/// Create a new backend initerface. The user interface should subscribe to the state stream
|
||||
/// and call init() afterwards.
|
||||
pub fn new() -> Self {
|
||||
let (state_sender, state_stream) = mpsc::channel(1024);
|
||||
|
||||
Backend {
|
||||
state_stream: RefCell::new(state_stream),
|
||||
state_sender: RefCell::new(state_sender),
|
||||
settings: gio::Settings::new("de.johrpan.musicus"),
|
||||
music_library_path: RefCell::new(None),
|
||||
database: RefCell::new(None),
|
||||
player: RefCell::new(None),
|
||||
client: Client::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Wait for the next state change. Initially, the state should be assumed to be
|
||||
/// BackendState::Loading. Changes should be awaited before calling init().
|
||||
pub async fn next_state(&self) -> Option<BackendState> {
|
||||
self.state_stream.borrow_mut().next().await
|
||||
}
|
||||
|
||||
/// Initialize the backend updating the state accordingly.
|
||||
pub async fn init(&self) -> Result<()> {
|
||||
self.init_library().await?;
|
||||
|
||||
if let Some(url) = self.settings.get_string("server-url") {
|
||||
if !url.is_empty() {
|
||||
self.client.set_server_url(&url);
|
||||
}
|
||||
}
|
||||
|
||||
match Self::load_login_data().await {
|
||||
Ok(Some(data)) => self.client.set_login_data(Some(data)),
|
||||
Err(err) => warn!("The login data could not be loaded from SecretService. It will not \
|
||||
be available. Error message: {}", err),
|
||||
_ => (),
|
||||
}
|
||||
|
||||
if self.get_music_library_path().is_none() {
|
||||
self.set_state(BackendState::NoMusicLibrary);
|
||||
} else {
|
||||
self.set_state(BackendState::Ready);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set the URL of the Musicus server to connect to.
|
||||
pub fn set_server_url(&self, url: &str) {
|
||||
if let Err(err) = self.settings.set_string("server-url", url) {
|
||||
warn!("An error happened while trying to save the server URL to GSettings. Most \
|
||||
likely it will not be available at the next startup. Error message: {}", err);
|
||||
}
|
||||
|
||||
self.client.set_server_url(url);
|
||||
}
|
||||
|
||||
/// Get the currently set server URL.
|
||||
pub fn get_server_url(&self) -> Option<String> {
|
||||
self.client.get_server_url()
|
||||
}
|
||||
|
||||
/// Set the user credentials to use.
|
||||
pub async fn set_login_data(&self, data: Option<LoginData>) {
|
||||
if let Some(data) = &data {
|
||||
if let Err(err) = Self::store_login_data(data.clone()).await {
|
||||
warn!("An error happened while trying to store the login data using SecretService. \
|
||||
This means, that they will not be available at the next startup most likely. \
|
||||
Error message: {}", err);
|
||||
}
|
||||
} else {
|
||||
if let Err(err) = Self::delete_secrets().await {
|
||||
warn!("An error happened while trying to delete the login data from SecretService. \
|
||||
This may result in the login data being reloaded at the next startup. Error \
|
||||
message: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
self.client.set_login_data(data);
|
||||
}
|
||||
|
||||
pub fn cl(&self) -> &Client {
|
||||
&self.client
|
||||
}
|
||||
|
||||
/// Get the currently stored login credentials.
|
||||
pub fn get_login_data(&self) -> Option<LoginData> {
|
||||
self.client.get_login_data()
|
||||
}
|
||||
|
||||
/// Set the current state and notify the user interface.
|
||||
fn set_state(&self, state: BackendState) {
|
||||
self.state_sender.borrow_mut().try_send(state).unwrap();
|
||||
}
|
||||
}
|
||||
85
backend/src/library.rs
Normal file
85
backend/src/library.rs
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
use crate::{Backend, BackendState, Player, Result};
|
||||
use musicus_database::DbThread;
|
||||
use gio::prelude::*;
|
||||
use log::warn;
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
|
||||
impl Backend {
|
||||
/// Initialize the music library if it is set in the settings.
|
||||
pub(super) async fn init_library(&self) -> Result<()> {
|
||||
if let Some(path) = self.settings.get_string("music-library-path") {
|
||||
if !path.is_empty() {
|
||||
self.set_music_library_path_priv(PathBuf::from(path.to_string()))
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set the path to the music library folder and start a database thread in the background.
|
||||
pub async fn set_music_library_path(&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).await
|
||||
}
|
||||
|
||||
/// Set the path to the music library folder and start a database thread in the background.
|
||||
pub async fn set_music_library_path_priv(&self, path: PathBuf) -> Result<()> {
|
||||
self.set_state(BackendState::Loading);
|
||||
|
||||
if let Some(db) = &*self.database.borrow() {
|
||||
db.stop().await?;
|
||||
}
|
||||
|
||||
self.music_library_path.replace(Some(path.clone()));
|
||||
|
||||
let mut db_path = path.clone();
|
||||
db_path.push("musicus.db");
|
||||
|
||||
let database = DbThread::new(db_path.to_str().unwrap().to_string()).await?;
|
||||
self.database.replace(Some(Rc::new(database)));
|
||||
|
||||
let player = Player::new(path);
|
||||
self.player.replace(Some(player));
|
||||
|
||||
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 current music library database.
|
||||
pub fn get_database(&self) -> Option<Rc<DbThread>> {
|
||||
self.database.borrow().clone()
|
||||
}
|
||||
|
||||
/// Get an interface to the database and panic if there is none.
|
||||
pub fn db(&self) -> Rc<DbThread> {
|
||||
self.get_database().unwrap()
|
||||
}
|
||||
|
||||
/// Get an interface to the playback service.
|
||||
pub fn get_player(&self) -> Option<Rc<Player>> {
|
||||
self.player.borrow().clone()
|
||||
}
|
||||
|
||||
/// Notify the frontend that the library was changed.
|
||||
pub fn library_changed(&self) {
|
||||
self.set_state(BackendState::Loading);
|
||||
self.set_state(BackendState::Ready);
|
||||
}
|
||||
|
||||
/// Get an interface to the player and panic if there is none.
|
||||
pub fn pl(&self) -> Rc<Player> {
|
||||
self.get_player().unwrap()
|
||||
}
|
||||
}
|
||||
391
backend/src/player.rs
Normal file
391
backend/src/player.rs
Normal file
|
|
@ -0,0 +1,391 @@
|
|||
use crate::{Error, Result};
|
||||
use mpris_player::{Metadata, MprisPlayer, PlaybackStatus};
|
||||
use musicus_database::TrackSet;
|
||||
use glib::clone;
|
||||
use gstreamer_player::prelude::*;
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PlaylistItem {
|
||||
pub track_set: TrackSet,
|
||||
pub indices: Vec<usize>,
|
||||
}
|
||||
|
||||
pub struct Player {
|
||||
music_library_path: PathBuf,
|
||||
player: gstreamer_player::Player,
|
||||
mpris: Arc<MprisPlayer>,
|
||||
playlist: RefCell<Vec<PlaylistItem>>,
|
||||
current_item: Cell<Option<usize>>,
|
||||
current_track: Cell<Option<usize>>,
|
||||
playing: Cell<bool>,
|
||||
playlist_cbs: RefCell<Vec<Box<dyn Fn(Vec<PlaylistItem>)>>>,
|
||||
track_cbs: RefCell<Vec<Box<dyn Fn(usize, 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()>>>,
|
||||
}
|
||||
|
||||
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.get_config();
|
||||
config.set_position_update_interval(250);
|
||||
player.set_config(config).unwrap();
|
||||
player.set_video_track_enabled(false);
|
||||
|
||||
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);
|
||||
|
||||
let result = Rc::new(Self {
|
||||
music_library_path,
|
||||
player: player.clone(),
|
||||
mpris,
|
||||
playlist: RefCell::new(Vec::new()),
|
||||
current_item: Cell::new(None),
|
||||
current_track: Cell::new(None),
|
||||
playing: Cell::new(false),
|
||||
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),
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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.mseconds().unwrap());
|
||||
}
|
||||
});
|
||||
|
||||
let clone = fragile::Fragile::new(result.clone());
|
||||
player.connect_duration_changed(move |_, duration| {
|
||||
for cb in &*clone.get().duration_cbs.borrow() {
|
||||
cb(duration.mseconds().unwrap());
|
||||
}
|
||||
});
|
||||
|
||||
result.mpris.connect_play_pause(clone!(@weak result => move || {
|
||||
result.play_pause();
|
||||
}));
|
||||
|
||||
result.mpris.connect_play(clone!(@weak result => move || {
|
||||
if !result.is_playing() {
|
||||
result.play_pause();
|
||||
}
|
||||
}));
|
||||
|
||||
result.mpris.connect_pause(clone!(@weak result => move || {
|
||||
if result.is_playing() {
|
||||
result.play_pause();
|
||||
}
|
||||
}));
|
||||
|
||||
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 add_playlist_cb<F: Fn(Vec<PlaylistItem>) + 'static>(&self, cb: F) {
|
||||
self.playlist_cbs.borrow_mut().push(Box::new(cb));
|
||||
}
|
||||
|
||||
pub fn add_track_cb<F: Fn(usize, 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<PlaylistItem> {
|
||||
self.playlist.borrow().clone()
|
||||
}
|
||||
|
||||
pub fn get_current_item(&self) -> Option<usize> {
|
||||
self.current_item.get()
|
||||
}
|
||||
|
||||
pub fn get_current_track(&self) -> Option<usize> {
|
||||
self.current_track.get()
|
||||
}
|
||||
|
||||
pub fn get_duration(&self) -> gstreamer::ClockTime {
|
||||
self.player.get_duration()
|
||||
}
|
||||
|
||||
pub fn is_playing(&self) -> bool {
|
||||
self.playing.get()
|
||||
}
|
||||
|
||||
pub fn add_item(&self, item: PlaylistItem) -> Result<()> {
|
||||
if item.indices.is_empty() {
|
||||
Err(Error::Other("Tried to add an empty playlist item!"))
|
||||
} else {
|
||||
let was_empty = {
|
||||
let mut playlist = self.playlist.borrow_mut();
|
||||
let was_empty = playlist.is_empty();
|
||||
|
||||
playlist.push(item);
|
||||
|
||||
was_empty
|
||||
};
|
||||
|
||||
for cb in &*self.playlist_cbs.borrow() {
|
||||
cb(self.playlist.borrow().clone());
|
||||
}
|
||||
|
||||
if was_empty {
|
||||
self.set_track(0, 0)?;
|
||||
self.player.play();
|
||||
self.playing.set(true);
|
||||
|
||||
for cb in &*self.playing_cbs.borrow() {
|
||||
cb(true);
|
||||
}
|
||||
|
||||
self.mpris.set_can_play(true);
|
||||
self.mpris.set_playback_status(PlaybackStatus::Playing);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn play_pause(&self) {
|
||||
if self.is_playing() {
|
||||
self.player.pause();
|
||||
self.playing.set(false);
|
||||
|
||||
for cb in &*self.playing_cbs.borrow() {
|
||||
cb(false);
|
||||
}
|
||||
|
||||
self.mpris.set_playback_status(PlaybackStatus::Paused);
|
||||
} else {
|
||||
self.player.play();
|
||||
self.playing.set(true);
|
||||
|
||||
for cb in &*self.playing_cbs.borrow() {
|
||||
cb(true);
|
||||
}
|
||||
|
||||
self.mpris.set_playback_status(PlaybackStatus::Playing);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn seek(&self, ms: u64) {
|
||||
self.player.seek(gstreamer::ClockTime::from_mseconds(ms));
|
||||
}
|
||||
|
||||
pub fn has_previous(&self) -> bool {
|
||||
if let Some(current_item) = self.current_item.get() {
|
||||
if let Some(current_track) = self.current_track.get() {
|
||||
current_track > 0 || current_item > 0
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn previous(&self) -> Result<()> {
|
||||
let mut current_item = self.current_item.get()
|
||||
.ok_or(Error::Other("Player tried to access non existant current item."))?;
|
||||
|
||||
let mut current_track = self
|
||||
.current_track
|
||||
.get()
|
||||
.ok_or(Error::Other("Player tried to access non existant current track."))?;
|
||||
|
||||
let playlist = self.playlist.borrow();
|
||||
if current_track > 0 {
|
||||
current_track -= 1;
|
||||
} else if current_item > 0 {
|
||||
current_item -= 1;
|
||||
current_track = playlist[current_item].indices.len() - 1;
|
||||
} else {
|
||||
return Err(Error::Other("No existing previous track."));
|
||||
}
|
||||
|
||||
self.set_track(current_item, current_track)
|
||||
}
|
||||
|
||||
pub fn has_next(&self) -> bool {
|
||||
if let Some(current_item) = self.current_item.get() {
|
||||
if let Some(current_track) = self.current_track.get() {
|
||||
let playlist = self.playlist.borrow();
|
||||
let item = &playlist[current_item];
|
||||
|
||||
current_track + 1 < item.indices.len() || current_item + 1 < playlist.len()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next(&self) -> Result<()> {
|
||||
let mut current_item = self.current_item.get()
|
||||
.ok_or(Error::Other("Player tried to access non existant current item."))?;
|
||||
let mut current_track = self
|
||||
.current_track
|
||||
.get()
|
||||
.ok_or(Error::Other("Player tried to access non existant current track."))?;
|
||||
|
||||
let playlist = self.playlist.borrow();
|
||||
let item = &playlist[current_item];
|
||||
if current_track + 1 < item.indices.len() {
|
||||
current_track += 1;
|
||||
} else if current_item + 1 < playlist.len() {
|
||||
current_item += 1;
|
||||
current_track = 0;
|
||||
} else {
|
||||
return Err(Error::Other("No existing previous track."));
|
||||
}
|
||||
|
||||
self.set_track(current_item, current_track)
|
||||
}
|
||||
|
||||
pub fn set_track(&self, current_item: usize, current_track: usize) -> Result<()> {
|
||||
let item = &self.playlist.borrow()[current_item];
|
||||
let track = &item.track_set.tracks[current_track];
|
||||
|
||||
let uri = format!(
|
||||
"file://{}",
|
||||
self.music_library_path.join(track.path.clone()).to_str().unwrap(),
|
||||
);
|
||||
|
||||
self.player.set_uri(&uri);
|
||||
if self.is_playing() {
|
||||
self.player.play();
|
||||
}
|
||||
|
||||
self.current_item.set(Some(current_item));
|
||||
self.current_track.set(Some(current_track));
|
||||
|
||||
for cb in &*self.track_cbs.borrow() {
|
||||
cb(current_item, current_track);
|
||||
}
|
||||
|
||||
let mut parts = Vec::<String>::new();
|
||||
for part in &track.work_parts {
|
||||
parts.push(item.track_set.recording.work.parts[*part].title.clone());
|
||||
}
|
||||
|
||||
let mut title = item.track_set.recording.work.get_title();
|
||||
if !parts.is_empty() {
|
||||
title = format!("{}: {}", title, parts.join(", "));
|
||||
}
|
||||
|
||||
let subtitle = item.track_set.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_item.get().unwrap(), self.current_track.get().unwrap());
|
||||
}
|
||||
|
||||
for cb in &*self.duration_cbs.borrow() {
|
||||
cb(self.player.get_duration().mseconds().unwrap());
|
||||
}
|
||||
|
||||
for cb in &*self.playing_cbs.borrow() {
|
||||
cb(self.is_playing());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear(&self) {
|
||||
self.player.stop();
|
||||
self.playing.set(false);
|
||||
self.current_item.set(None);
|
||||
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());
|
||||
}
|
||||
|
||||
self.mpris.set_can_play(false);
|
||||
}
|
||||
}
|
||||
102
backend/src/secure.rs
Normal file
102
backend/src/secure.rs
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
use crate::{Backend, Error, Result};
|
||||
use musicus_client::LoginData;
|
||||
use futures_channel::oneshot;
|
||||
use secret_service::{Collection, EncryptionType, SecretService};
|
||||
use std::collections::HashMap;
|
||||
use std::thread;
|
||||
|
||||
impl Backend {
|
||||
/// Get the login credentials from secret storage.
|
||||
pub(super) async fn load_login_data() -> Result<Option<LoginData>> {
|
||||
let (sender, receiver) = oneshot::channel();
|
||||
thread::spawn(move || sender.send(Self::load_login_data_priv()).unwrap());
|
||||
receiver.await?
|
||||
}
|
||||
|
||||
/// Savely store the user's current login credentials.
|
||||
pub(super) async fn store_login_data(data: LoginData) -> Result<()> {
|
||||
let (sender, receiver) = oneshot::channel();
|
||||
thread::spawn(move || sender.send(Self::store_login_data_priv(data)).unwrap());
|
||||
receiver.await?
|
||||
}
|
||||
|
||||
/// Delete all stored secrets.
|
||||
pub(super) async fn delete_secrets() -> Result<()> {
|
||||
let (sender, receiver) = oneshot::channel();
|
||||
thread::spawn(move || sender.send(Self::delete_secrets_priv()).unwrap());
|
||||
receiver.await?
|
||||
}
|
||||
|
||||
/// Get the login credentials from secret storage.
|
||||
fn load_login_data_priv() -> Result<Option<LoginData>> {
|
||||
let ss = SecretService::new(EncryptionType::Dh)?;
|
||||
let collection = Self::get_collection(&ss)?;
|
||||
|
||||
let items = collection.get_all_items()?;
|
||||
|
||||
let key = "musicus-login-data";
|
||||
let item = items.iter().find(|item| item.get_label().unwrap_or_default() == key);
|
||||
|
||||
Ok(match item {
|
||||
Some(item) => {
|
||||
let username = item
|
||||
.get_attributes()?
|
||||
.get("username")
|
||||
.ok_or(Error::Other("Missing username in SecretService attributes."))?
|
||||
.to_owned();
|
||||
|
||||
let password = std::str::from_utf8(&item.get_secret()?)?.to_owned();
|
||||
|
||||
Some(LoginData { username, password })
|
||||
}
|
||||
None => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Savely store the user's current login credentials.
|
||||
fn store_login_data_priv(data: LoginData) -> Result<()> {
|
||||
let ss = SecretService::new(EncryptionType::Dh)?;
|
||||
let collection = Self::get_collection(&ss)?;
|
||||
|
||||
let key = "musicus-login-data";
|
||||
Self::delete_secrets_for_key(&collection, key)?;
|
||||
|
||||
let mut attributes = HashMap::new();
|
||||
attributes.insert("username", data.username.as_str());
|
||||
collection.create_item(key, attributes, data.password.as_bytes(), true, "text/plain")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete all stored secrets.
|
||||
fn delete_secrets_priv() -> Result<()> {
|
||||
let ss = SecretService::new(EncryptionType::Dh)?;
|
||||
let collection = Self::get_collection(&ss)?;
|
||||
|
||||
let key = "musicus-login-data";
|
||||
Self::delete_secrets_for_key(&collection, key)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete all stored secrets for the provided key.
|
||||
fn delete_secrets_for_key(collection: &Collection, key: &str) -> Result<()> {
|
||||
let items = collection.get_all_items()?;
|
||||
|
||||
for item in items {
|
||||
if item.get_label().unwrap_or_default() == key {
|
||||
item.delete()?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the default SecretService collection and unlock it.
|
||||
fn get_collection<'a>(ss: &'a SecretService) -> Result<Collection<'a>> {
|
||||
let collection = ss.get_default_collection()?;
|
||||
collection.unlock()?;
|
||||
|
||||
Ok(collection)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue