diff --git a/Cargo.lock b/Cargo.lock index 53cd97a6a076b49c42f90ac06121ca10aeab9d0b..8d1356eca2abf569994bc6055c3b8c060ab42215 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3258,6 +3258,7 @@ dependencies = [ "anyhow", "axum", "clap", + "derive_more", "futures", "hashbrown 0.15.1", "hyper", diff --git a/lektor_mpris/src/server.rs b/lektor_mpris/src/server.rs index 42e35123d30f72ed5e8a54c2b16a72cf3a654d3b..b98e5599c4caa072dcfc2600053713c0753711b6 100644 --- a/lektor_mpris/src/server.rs +++ b/lektor_mpris/src/server.rs @@ -2,18 +2,24 @@ //! `org.mpris.MediaPlayer2.TrackList` use crate::{types::*, AsMpris}; -use zbus::{connection, object_server::SignalEmitter, Connection}; +use zbus::{ + connection, + object_server::{InterfaceRef, SignalEmitter}, + Connection, +}; /// The adapter for MPRIS. #[derive(Debug)] -pub struct MPRISAdapter<T: AsMpris + Send + Sync + Clone + 'static>(Connection, T); +pub struct MPRISAdapter<T: AsMpris + Send + Sync + Clone + 'static> { + connection: Connection, + application: T, +} /// Builder used to configure and create the adapter. pub struct MPRISBuilder<T: AsMpris + Send + Sync + Clone + 'static> { application: T, identity: String, - desktop_entry: Option<String>, - is_unique: bool, + desk_entry: Option<String>, } impl<T: AsMpris + Send + Sync + Clone + 'static> MPRISAdapter<T> { @@ -22,54 +28,105 @@ impl<T: AsMpris + Send + Sync + Clone + 'static> MPRISAdapter<T> { MPRISBuilder { application, identity: identity.as_ref().into(), - desktop_entry: None, - is_unique: false, + desk_entry: None, } } + + /// Get the application that is wrapped inside the [MPRISAdapter]. + pub fn application(&self) -> &T { + &self.application + } + + /// Get the [TrackList] server object, to emit some signals with it. + async fn object_track_list(&self) -> zbus::Result<InterfaceRef<TrackList<T>>> { + self.connection + .object_server() + .interface::<&str, TrackList<T>>("org.mpris.MediaPlayer2.TrackList") + .await + } + + /// TrackAdded signal + pub async fn signal_track_added( + &self, + metadata: impl Into<TrackMetadata>, + after: ObjectPath, + ) -> zbus::Result<()> { + self.object_track_list() + .await? + .track_added(metadata.into(), after) + .await + } + + /// TrackListReplaced signal + pub async fn signal_track_list_replaced( + &self, + tracks: impl IntoIterator<Item = ObjectPath>, + current: ObjectPath, + ) -> zbus::Result<()> { + self.object_track_list() + .await? + .track_list_replaced(tracks.into_iter().collect(), current) + .await + } + + /// TrackMetadataChanged signal + pub async fn signal_track_metadata_changed( + &self, + track_id: ObjectPath, + metadata: impl Into<TrackMetadata>, + ) -> zbus::Result<()> { + self.object_track_list() + .await? + .track_metadata_changed(track_id, metadata.into()) + .await + } + + /// TrackRemoved signal + pub async fn signal_track_removed(&self, track_id: ObjectPath) -> zbus::Result<()> { + self.object_track_list() + .await? + .track_removed(track_id) + .await + } } impl<T: AsMpris + Send + Sync + Clone + 'static> MPRISBuilder<T> { /// Name of the application (Lektord, Amadeus, etc) - pub fn desktop_entry(mut self, desktop_entry: impl AsRef<str>) -> Self { - self.desktop_entry = Some(desktop_entry.as_ref().into()); - self + pub fn desktop_entry(self, desktop_entry: impl AsRef<str>) -> Self { + Self { + desk_entry: Some(desktop_entry.as_ref().into()), + ..self + } } - /// Should we be unique? - /// - Lektord should be a unique instance. - /// - It makes sens for Amadeus to not be unique, so we must call this function to make each - /// mpris server unique. - pub fn unique(mut self) -> Self { - self.is_unique = true; - self + /// Get the DBus name. We can't have multiple Lektord or Amadeus instances. + fn get_name(&self) -> String { + format!("org.mpris.MediaPlayer2.{}", self.identity) } /// Try to build the DBus server. pub async fn try_build(self) -> zbus::Result<MPRISAdapter<T>> { - let name = format!( - "org.mpris.MediaPlayer2.{}{}", - self.identity, - self.is_unique - .then_some(format!("-{}", std::process::id())) - .unwrap_or_default() - ); - let obj_main = Main { - desktop_entry: self.desktop_entry.unwrap_or_else(|| self.identity.clone()), - identity: self.identity, - }; - let obj_player = Player(self.application.clone()); - let obj_tracks = TrackList(self.application.clone()); + let name = self.get_name(); + let Self { + application, + identity, + desk_entry, + } = self; log::info!("try to create new dbus connection for: {name}"); + let connection = connection::Builder::session()? .name(name)? - .serve_at("/org/mpris/MediaPlayer2", obj_main)? - .serve_at("/org/mpris/MediaPlayer2", obj_player)? - .serve_at("/org/mpris/MediaPlayer2", obj_tracks)? + .serve_at("/org/mpris/MediaPlayer2", Main::new(identity, desk_entry))? + .serve_at("/org/mpris/MediaPlayer2", Player(application.clone()))? + .serve_at("/org/mpris/MediaPlayer2", TrackList(application.clone()))? .build() .await?; - Ok(MPRISAdapter(connection, self.application)) + Ok(MPRISAdapter { + connection, + application, + }) } } @@ -80,6 +137,15 @@ struct Main { desktop_entry: String, } +impl Main { + fn new(identity: String, desktop_entry: Option<String>) -> Self { + Self { + desktop_entry: desktop_entry.unwrap_or_else(|| identity.clone()), + identity, + } + } +} + #[zbus::interface(name = "org.mpris.MediaPlayer2")] impl Main { /// Quit method @@ -153,57 +219,64 @@ impl Main { impl<T: AsMpris + Send + Sync + Clone + 'static> Player<T> { /// Next method #[zbus(name = "Next")] - fn next(&self) { - self.0.play_next() + async fn next(&self) { + self.0.play_next().await } /// Pause method #[zbus(name = "Pause")] - fn pause(&self) { - self.0.set_playback_status(PlaybackStatus::Paused.into()) + async fn pause(&self) { + self.0 + .set_playback_status(PlaybackStatus::Paused.into()) + .await } /// Play method #[zbus(name = "Play")] - fn play(&self) { - self.0.set_playback_status(PlaybackStatus::Playing.into()) + async fn play(&self) { + self.0 + .set_playback_status(PlaybackStatus::Playing.into()) + .await } /// OpenUri method #[zbus(name = "OpenUri")] - fn open_uri(&self, uri: &str) { - self.0.play_file(uri) + async fn open_uri(&self, uri: &str) { + self.0.play_file(uri).await } /// Toggle play pause status. #[zbus(name = "PlayPause")] - fn play_pause(&self) { - self.0.toggle_playback_status() + async fn play_pause(&self) { + self.0.toggle_playback_status().await } /// Previous method #[zbus(name = "Previous")] - fn previous(&self) { - self.0.play_previous() + async fn previous(&self) { + self.0.play_previous().await } /// Seek method #[zbus(name = "Seek")] - fn seek(&self, offset: TimeMicroSec) { - let position = self.0.position().into() + offset; - self.0.seek(None, position.into()); + async fn seek(&self, offset: TimeMicroSec) { + self.0 + .seek(None, (self.0.position().await.into() + offset).into()) + .await; } /// SetPosition method #[zbus(name = "SetPosition")] - fn set_position(&self, track_id: ObjectPath, position: TimeMicroSec) { - self.0.seek(Some(track_id.into()), position.into()); + async fn set_position(&self, track_id: ObjectPath, position: TimeMicroSec) { + self.0.seek(Some(track_id.into()), position.into()).await; } /// Stop method #[zbus(name = "Stop")] - fn stop(&self) { - log::warn!("ignore stop command") + async fn stop(&self) { + self.0 + .set_playback_status(PlaybackStatus::Stopped.into()) + .await } /// CanControl property @@ -240,51 +313,74 @@ impl<T: AsMpris + Send + Sync + Clone + 'static> Player<T> { log::warn!("ignore loop status {loop_status:?}") } - #[rustfmt::skip] #[zbus(property, name = "MaximumRate")] fn maximum_rate(&self) -> f64 { 1.0 } - #[rustfmt::skip] #[zbus(property, name = "MinimumRate")] fn minimum_rate(&self) -> f64 { 1.0 } - #[rustfmt::skip] #[zbus(property, name = "MaximumRate")] fn set_maximum_rate(&self, rate: f64) { log::warn!("ignore rate {rate}") } - #[rustfmt::skip] #[zbus(property, name = "MinimumRate")] fn set_minimum_rate(&self, rate: f64) { log::warn!("ignore rate {rate}") } - #[rustfmt::skip] #[zbus(property, name = "Rate" )] fn rate(&self) -> f64 { 1.0 } - #[rustfmt::skip] #[zbus(property, name = "Rate" )] fn set_rate(&self, rate: f64) { log::warn!("ignore rate {rate}") } + #[zbus(property, name = "MaximumRate")] + fn maximum_rate(&self) -> f64 { + 1.0 + } + + #[zbus(property, name = "MinimumRate")] + fn minimum_rate(&self) -> f64 { + 1.0 + } + + #[zbus(property, name = "MaximumRate")] + fn set_maximum_rate(&self, rate: f64) { + log::warn!("ignore rate {rate}") + } + + #[zbus(property, name = "MinimumRate")] + fn set_minimum_rate(&self, rate: f64) { + log::warn!("ignore rate {rate}") + } + + #[zbus(property, name = "Rate")] + fn rate(&self) -> f64 { + 1.0 + } + + #[zbus(property, name = "Rate")] + fn set_rate(&self, rate: f64) { + log::warn!("ignore rate {rate}") + } /// Shuffle property #[zbus(property, name = "Shuffle")] fn shuffle(&self) -> bool { - self.0.shuffle() + false } #[zbus(property, name = "Shuffle")] fn set_shuffle(&self, shuffle: bool) { - self.0.set_shuffle(shuffle); + log::error!("ignore shuffle request to: {shuffle}") } /// Volume property #[zbus(property, name = "Volume")] - fn volume(&self) -> f64 { - self.0.volume() + async fn volume(&self) -> f64 { + self.0.volume().await } #[zbus(property, name = "Volume")] - fn set_volume(&self, volume: f64) { - self.0.set_volume(volume) + async fn set_volume(&self, volume: f64) { + self.0.set_volume(volume).await } /// PlaybackStatus property #[zbus(property, name = "PlaybackStatus")] - fn playback_status(&self) -> PlaybackStatus { - self.0.get_playback_status().into() + async fn playback_status(&self) -> PlaybackStatus { + self.0.get_playback_status().await.into() } /// Position property #[zbus(property, name = "Position")] - fn position(&self) -> TimeMicroSec { - self.0.position().into() + async fn position(&self) -> TimeMicroSec { + self.0.position().await.into() } /// Metadata property. If there is a current kara playing, there must be at /// least a `mpris:trackid` value of type `o` which is the object path of /// the current kara id. #[zbus(property, name = "Metadata")] - fn metadata(&self) -> TrackMetadata { - self.0.get_current_metadata().into() + async fn metadata(&self) -> TrackMetadata { + self.0.get_current_metadata().await.into() } } @@ -292,16 +388,16 @@ impl<T: AsMpris + Send + Sync + Clone + 'static> Player<T> { impl<T: AsMpris + Send + Sync + Clone + 'static> TrackList<T> { /// AddTrack method #[zbus(name = "AddTrack")] - fn add_track(&self, uri: &str, after: ObjectPath, set_as_current: bool) { - self.0.add_track(uri, after.into(), set_as_current) + async fn add_track(&self, uri: &str, after: ObjectPath, set_as_current: bool) { + self.0.add_track(uri, after.into(), set_as_current).await } /// GetTracksMetadata method #[zbus(name = "GetTracksMetadata")] - fn get_tracks_metadata(&self, tracks_ids: Vec<ObjectPath>) -> Vec<TrackMetadata> { - let tracks = tracks_ids.into_iter().map(From::from).collect(); + async fn get_tracks_metadata(&self, tracks_ids: Vec<ObjectPath>) -> Vec<TrackMetadata> { self.0 - .get_tracks_metadata(tracks) + .get_tracks_metadata(tracks_ids.into_iter().map(From::from).collect()) + .await .into_iter() .map(Into::into) .collect() @@ -309,14 +405,14 @@ impl<T: AsMpris + Send + Sync + Clone + 'static> TrackList<T> { /// GoTo method #[zbus(name = "GoTo")] - fn go_to(&self, track_id: ObjectPath) { - self.0.go_to(track_id.into()) + async fn go_to(&self, track_id: ObjectPath) { + self.0.go_to(track_id.into()).await } /// RemoveTrack method #[zbus(name = "RemoveTrack")] - fn remove_track(&self, track_id: ObjectPath) { - self.0.remove_track(track_id.into()) + async fn remove_track(&self, track_id: ObjectPath) { + self.0.remove_track(track_id.into()).await } /// CanEditTracks property @@ -327,9 +423,10 @@ impl<T: AsMpris + Send + Sync + Clone + 'static> TrackList<T> { /// Tracks property #[zbus(property, name = "Tracks")] - fn tracks(&self) -> Vec<ObjectPath> { + async fn tracks(&self) -> Vec<ObjectPath> { self.0 .get_queue_content() + .await .into_iter() .map(Into::into) .collect() diff --git a/lektor_mpris/src/traits.rs b/lektor_mpris/src/traits.rs index fadd07ceea90208fabd1f79f682988f085d9aea0..5103ac0a96b82fba3a044f22a00296ee5603a73a 100644 --- a/lektor_mpris/src/traits.rs +++ b/lektor_mpris/src/traits.rs @@ -1,41 +1,49 @@ -//! TODO: See how we can do for the signals... We may need to fire them at some point. - use crate::types::*; +use std::future::Future; /// An application must implement the [AsMpris] trait to be able to be wrapped by the /// [crate::server::MPRISAdapter]. -pub trait AsMpris { - type Status: From<PlaybackStatus> + Into<PlaybackStatus>; - type Time: From<TimeMicroSec> + Into<TimeMicroSec>; - type TrackMdt: Into<TrackMetadata>; - type Path: From<ObjectPath> + Into<ObjectPath>; - - fn set_playback_status(&self, status: Self::Status); - fn get_playback_status(&self) -> Self::Status; - fn toggle_playback_status(&self); - - fn play_next(&self); - fn play_previous(&self); - - fn seek(&self, file: Option<Self::Path>, position: Self::Time); - fn go_to(&self, track_id: Self::Path); - fn position(&self) -> Self::Time; - - fn shuffle(&self) -> bool; - fn set_shuffle(&self, shuffle: bool); - - fn volume(&self) -> f64 { - 100.0 +pub trait AsMpris: Send { + type Status: From<PlaybackStatus> + Into<PlaybackStatus> + Send + Copy; + type Time: From<TimeMicroSec> + Into<TimeMicroSec> + Send + Copy; + type TrackMdt: Into<TrackMetadata> + Send; + type Path: From<ObjectPath> + Into<ObjectPath> + Send; + + fn get_playback_status(&self) -> impl Future<Output = Self::Status> + Send; + fn set_playback_status(&self, status: Self::Status) -> impl Future<Output = ()> + Send; + fn toggle_playback_status(&self) -> impl Future<Output = ()> + Send; + + fn play_next(&self) -> impl Future<Output = ()> + Send; + fn play_previous(&self) -> impl Future<Output = ()> + Send; + + fn seek( + &self, + file: Option<Self::Path>, + position: Self::Time, + ) -> impl Future<Output = ()> + Send; + fn go_to(&self, track_id: Self::Path) -> impl Future<Output = ()> + Send; + fn position(&self) -> impl Future<Output = Self::Time> + Send; + + fn volume(&self) -> impl Future<Output = f64> + Send { + async { 100.0 } } - fn set_volume(&self, volume: f64) { - log::warn!("ignore volume {volume}"); + fn set_volume(&self, volume: f64) -> impl Future<Output = ()> + Send { + async move { log::warn!("ignore volume {volume}") } } - fn get_current_metadata(&self) -> Self::TrackMdt; - fn get_tracks_metadata(&self, tracks_ids: Vec<Self::Path>) -> Vec<Self::TrackMdt>; - fn get_queue_content(&self) -> Vec<Self::Path>; - - fn add_track(&self, uri: &str, after: Self::Path, set_as_current: bool); - fn remove_track(&self, track_id: Self::Path); - fn play_file(&self, uri: &str); + fn get_current_metadata(&self) -> impl Future<Output = Self::TrackMdt> + Send; + fn get_tracks_metadata( + &self, + tracks_ids: Vec<Self::Path>, + ) -> impl Future<Output = Vec<Self::TrackMdt>> + Send; + fn get_queue_content(&self) -> impl Future<Output = Vec<Self::Path>> + Send; + + fn add_track( + &self, + uri: &str, + after: Self::Path, + set_as_current: bool, + ) -> impl Future<Output = ()> + Send; + fn remove_track(&self, track_id: Self::Path) -> impl Future<Output = ()> + Send; + fn play_file(&self, uri: &str) -> impl Future<Output = ()> + Send; } diff --git a/lektor_mpris/src/types.rs b/lektor_mpris/src/types.rs index cf268909bdb7120fbcf52e9e0b3277d273a427ff..5c60bb5b3821df96ad6f2bcdf407951d1a96e084 100644 --- a/lektor_mpris/src/types.rs +++ b/lektor_mpris/src/types.rs @@ -38,7 +38,7 @@ impl std::ops::Add for TimeMicroSec { } /// The metadata of a track. We will need to convert a kara from nkdb into this type. -#[derive(Debug, Serialize, Deserialize, ZType, Clone, PartialEq, Eq)] +#[derive(Debug, Serialize, Deserialize, ZType, Clone, PartialEq, Eq, Default)] #[zvariant(signature = "a{sv}")] pub struct TrackMetadata(HashMap<String, String>); @@ -119,6 +119,7 @@ impl From<ZValue<'_>> for PlaybackStatus { impl From<ZValue<'_>> for ObjectPath { fn from(value: ZValue<'_>) -> Self { + log::error!("we may want to replace the lektor by amadeus here..."); let value: ZObjectPath = value.try_into().unwrap_or_default(); let value = value.trim_start_matches('/'); match value.split('/').collect::<Vec<_>>()[..] { @@ -138,6 +139,7 @@ impl From<ZValue<'_>> for ObjectPath { impl From<ObjectPath> for ZValue<'_> { fn from(value: ObjectPath) -> Self { + log::error!("we may want to replace the lektor by amadeus here..."); use ObjectPath::*; let path = match value { None => "/org/lektor/MPRIS/None".try_into(), diff --git a/lektor_nkdb/src/database/epoch.rs b/lektor_nkdb/src/database/epoch.rs index 27ddba1952df02035043c7e470d4e786fd61e6a2..30787020fa0cec62437febe9c962d74c668a3ebf 100644 --- a/lektor_nkdb/src/database/epoch.rs +++ b/lektor_nkdb/src/database/epoch.rs @@ -1,9 +1,9 @@ //! An epoch is a complete representation of the database at a point in time. It should be valid //! and sufficient to load the database and update it (well, appart from the files...) -use hashbrown::hash_map; - use crate::*; +use hashbrown::hash_map; +use std::{ffi::OsStr, path::Path}; /// The epoch contains all available karas at a certain point in time. It can be submitted and /// available to all readers of the database or unsubmited and only one writter can edit it. @@ -35,10 +35,11 @@ impl Epoch { /// Get the list of [Kara] corresponding to the [KId]. If we failed to find one kara we log the /// error and continue (silent fail). - pub fn get_karas_by_kid(&self, ids: impl IntoIterator<Item = KId>) -> Vec<&Kara> { - ids.into_iter() - .flat_map(|id| self.get_kara_by_kid(id)) - .collect() + pub fn get_karas_by_kid( + &self, + ids: impl IntoIterator<Item = KId>, + ) -> impl Iterator<Item = &Kara> { + ids.into_iter().flat_map(|id| self.get_kara_by_kid(id)) } /// Get a [Kara] by its local id [KId]. Should be more efficient that the [get_kara_by_u64] @@ -48,6 +49,29 @@ impl Epoch { self.0.get(&id) } + /// Like [Self::get_kara_by_kid], but we try to find it by its [uri](url::Url). This is usefull + /// only when handling things from MPRIS because it's the only way to request a kara by its + /// file path in lektord. + pub fn get_kara_by_uri(&self, uri: &url::Url) -> Option<&Kara> { + self.get_kara_by_kid(match uri.scheme() { + "file" => { + let path = Path::new(uri.path()); + (path.extension()? == OsStr::new("mkv")) + .then(|| path.file_name()?.to_str()?.parse::<KId>().ok()) + .flatten()? + } + + "id" => (uri.path().parse::<KId>()) + .map_err(|err| log::error!("invalid kara id in uri: {err}")) + .ok()?, + + scheme => { + log::error!("unsuported uri scheme: {scheme}"); + return None; + } + }) + } + /// Get access to the karas in the epoch. pub fn data(&self) -> &EpochData { &self.0 diff --git a/lektor_nkdb/src/lib.rs b/lektor_nkdb/src/lib.rs index 73d585f9bcbb38ec835b1805d629d1c8085779c7..a4d0df06ef9e16f97986bd634c5aeae385136214 100644 --- a/lektor_nkdb/src/lib.rs +++ b/lektor_nkdb/src/lib.rs @@ -21,10 +21,11 @@ pub use crate::{ pub use kurisu_api::v2::{SongOrigin, SongType, SONGORIGIN_LENGTH, SONGTYPE_LENGTH}; use crate::database::{epoch::EpochData, pool::Pool}; -use anyhow::Context as _; +use anyhow::{anyhow, Context as _}; use hashbrown::HashMap; use lektor_utils::pushvec::*; use playlists::Playlists; +use std::fmt; mod database; mod id; @@ -91,6 +92,11 @@ impl<Storage: DatabaseStorage> Database<Storage> { ) } + /// Get the last epoch from the database. + pub async fn last_epoch(&self) -> Option<&Epoch> { + self.epochs.last().await + } + /// Get a [Kara] by its [KId] representation in the last epoch. Should be more efficient than /// the [Self::get_kara_by_id] because we dirrectly use the hash thing and we don't iterate /// over all the items in the [HashMap]. @@ -102,9 +108,20 @@ impl<Storage: DatabaseStorage> Database<Storage> { .with_context(|| format!("no kara {id}")) } - /// Get the last epoch from the database. - pub async fn last_epoch(&self) -> Option<&Epoch> { - self.epochs.last().await + /// Like [Self::get_kara_by_kid], but we try to find it by its [uri](url::Url). This is usefull + /// only when handling things from MPRIS because it's the only way to request a kara by its + /// file path in lektord. + pub async fn get_kara_by_uri<U>(&self, uri: U) -> anyhow::Result<&Kara> + where + U: TryInto<url::Url>, + <U as TryInto<url::Url>>::Error: fmt::Display, + { + let uri = uri.try_into().map_err(|err| anyhow!("{err}"))?; + self.last_epoch() + .await + .context("empty epoch")? + .get_kara_by_uri(&uri) + .with_context(|| format!("no kara matched uri `{uri}`")) } /// Get the path to a kara, try to get the absolute path, so here we append the relative path diff --git a/lektord/Cargo.toml b/lektord/Cargo.toml index a4e54f9cb1445f790f6d8d835a58f790405c8b3e..56ab4d1eac69e3e866603abffd526e425039056d 100644 --- a/lektord/Cargo.toml +++ b/lektord/Cargo.toml @@ -12,10 +12,11 @@ license.workspace = true serde.workspace = true serde_json.workspace = true -log.workspace = true -rand.workspace = true -anyhow.workspace = true -hashbrown.workspace = true +log.workspace = true +rand.workspace = true +anyhow.workspace = true +hashbrown.workspace = true +derive_more.workspace = true futures.workspace = true tokio.workspace = true diff --git a/lektord/src/app/mod.rs b/lektord/src/app/mod.rs index d020f169bc1aa131ecd7e5cfb243c53848137add..47897747779d524df7c308cb053f0f2327ea64ca 100644 --- a/lektord/src/app/mod.rs +++ b/lektord/src/app/mod.rs @@ -9,7 +9,7 @@ mod routes; mod search_adaptors; use crate::LektorConfig; -use anyhow::{Context, Result}; +use anyhow::Context as _; use axum::{ extract::Request, http::{response, StatusCode}, @@ -18,8 +18,8 @@ use axum::{ routing::{delete, get, post, put}, }; use lektor_mpris::MPRISAdapter; -use lektor_nkdb::{Database, DatabaseDiskStorage, Epoch}; -use lektor_payloads::{Epochs, LektorUser}; +use lektor_nkdb::{Database, DatabaseDiskStorage, Epoch, KId}; +use lektor_payloads::{Epochs, LektorUser, PlayState}; use lektor_repo::Repo; use lektor_utils::config::LektorDatabaseConfig; use play_state::StoredPlayState; @@ -31,7 +31,7 @@ use tokio::sync::{oneshot::Sender, RwLock}; /// the tokio test framework. pub async fn app( config: LektorConfig, -) -> Result<(axum::Router, tokio::sync::oneshot::Receiver<()>)> { +) -> anyhow::Result<(axum::Router, tokio::sync::oneshot::Receiver<()>)> { /// Try to declare the routes in a more legible way without having an ugly formating. macro_rules! router { ( @@ -186,7 +186,7 @@ pub(crate) type LektorStatePtr = Arc<LektorState>; impl LektorState { /// Create a new server state from the configuration file. - pub async fn new(config: LektorConfig, shutdown: Sender<()>) -> Result<LektorStatePtr> { + pub async fn new(config: LektorConfig, shutdown: Sender<()>) -> anyhow::Result<LektorStatePtr> { let LektorConfig { database: LektorDatabaseConfig { folder, .. }, player, @@ -215,8 +215,7 @@ impl LektorState { .desktop_entry("Lektord") .try_build() .await - .with_context(|| "failed to build mpris server, run with no mpris") - .map_err(|err| log::error!("{err}")) + .map_err(|err| log::error!("can't build mpris server, run with one: {err}")) .ok(); } crate::c_wrapper::init_player_module(ptr.clone(), player)?; @@ -245,13 +244,59 @@ impl LektorState { } } + /// Toggle the playstate. + fn toggle_play_state(&self) -> anyhow::Result<()> { + crate::c_wrapper::player_toggle_pause() + } + + /// Set the playstate. + async fn set_play_state(&self, to: PlayState) -> anyhow::Result<()> { + if to == PlayState::Stop { + return crate::c_wrapper::player_stop(); + } + + match self.queue().read(|_, state| state).await { + PlayState::Stop if to == PlayState::Play => self.play_next().await?, + PlayState::Stop => log::error!("can't change state from Stop to {to:?}"), + _ => crate::c_wrapper::player_set_paused(to)?, + }; + + Ok(()) + } + + /// Play from a certain kara if it is present in the queue. If the kara is present multiple + /// times, we play from the first found position. + async fn play_from_queue_kid(&self, id: KId) -> anyhow::Result<()> { + (self.queue()) + .write(|content, _| { + let idx = (content.get(..).into_iter().enumerate()) + .find_map(|(idx, (_, kid))| (kid == id).then_some(idx))?; + content.play_from_position(idx)?; + Some(()) + }) + .await + .with_context(|| format!("can't find kara with id {id} in the queue"))?; + crate::c_wrapper::player_load_file(self.database.get_kara_uri(id)?.to_string(), id)?; + Ok(()) + } + + /// Play from a certain position in the queue. + async fn play_from_queue_pos(&self, position: usize) -> anyhow::Result<()> { + let id = (self.queue()) + .write(|content, _| content.play_from_position(position)) + .await + .with_context(|| format!("position {position} doesn't exists in queue"))?; + crate::c_wrapper::player_load_file(self.database.get_kara_uri(id)?.to_string(), id)?; + Ok(()) + } + /// Get a handle to read or write the queue, alongside the playstate. pub(crate) fn queue(&self) -> QueueHandle { QueueHandle::new(&self.queue, &self.playstate) } /// Play the next kara in the queue. - pub(crate) async fn play_next(&self) -> Result<()> { + pub(crate) async fn play_next(&self) -> anyhow::Result<()> { let id = self .queue() .write(|queue, _| queue.next().context("no next kara to play")) @@ -260,7 +305,7 @@ impl LektorState { } /// Play the previous kara in the queue. - pub(crate) async fn play_previous(&self) -> Result<()> { + pub(crate) async fn play_previous(&self) -> anyhow::Result<()> { let id = self .queue() .write(|queue, _| queue.previous().context("no next kara to play")) @@ -272,6 +317,12 @@ impl LektorState { #[derive(Debug, Clone)] pub struct LektorStateWeakPtr(Weak<LektorState>); +impl LektorStateWeakPtr { + fn upgrade(&self) -> Option<LektorStatePtr> { + Weak::upgrade(&self.0) + } +} + impl From<&Arc<LektorState>> for LektorStateWeakPtr { fn from(value: &Arc<LektorState>) -> Self { Self(Arc::downgrade(value)) diff --git a/lektord/src/app/mpris.rs b/lektord/src/app/mpris.rs index b34e9deae6ef71d0da091525b2418fc7198430bc..046b684d8e2f60d542d8934c770e527515743960 100644 --- a/lektord/src/app/mpris.rs +++ b/lektord/src/app/mpris.rs @@ -1,97 +1,221 @@ use crate::LektorStateWeakPtr; +use derive_more::Display; use lektor_mpris::AsMpris; -use lektor_nkdb::Kara; -use std::sync::Arc; +use lektor_nkdb::{KId, Kara}; +use std::{ + ops::{self, Bound}, + sync::Arc, +}; /// The play state, convertible to the thing liked by MPRIS. +#[derive(Debug, Default, Clone, Copy)] pub struct PlayState(lektor_payloads::PlayState); /// A thing used to convert a [Kara] into track metadatas for MPRIS. -pub struct TrackMdt(Arc<Kara>); +pub struct TrackMdt<'a>(&'a Kara); /// An object used to designate karas in the database/queue by their path. It can also be used to /// specify other things as MPRIS like them. -pub enum ObjPath {} +#[derive(Debug, Default, Clone, Copy)] +pub enum ObjPath { + #[default] + None, + Id(KId), + Position(usize), +} /// Wrapper around the time for MPRIS. +#[derive(Debug, Default, Clone, Copy, Display)] +#[display("{_0}")] pub struct Time(u64); impl AsMpris for LektorStateWeakPtr { type Status = PlayState; type Time = Time; - type TrackMdt = TrackMdt; + type TrackMdt = lektor_mpris::types::TrackMetadata; type Path = ObjPath; - fn set_playback_status(&self, _status: Self::Status) { - todo!() - } - - fn get_playback_status(&self) -> Self::Status { - todo!() - } - - fn toggle_playback_status(&self) { - todo!() + async fn set_playback_status(&self, PlayState(status): Self::Status) { + if let Some(state) = self.upgrade() { + _ = (state.set_play_state(status)) + .await + .map_err(|err| log::error!("{err}")); + } } - fn play_next(&self) { - todo!() + async fn get_playback_status(&self) -> Self::Status { + if let Some(state) = self.upgrade() { + state.queue().read(|_, status| status).await.into() + } else { + Default::default() + } } - fn play_previous(&self) { - todo!() + async fn toggle_playback_status(&self) { + if let Some(state) = self.upgrade() { + _ = state + .toggle_play_state() + .map_err(|err| log::error!("{err}")); + } } - fn seek(&self, _file: Option<Self::Path>, _position: Self::Time) { - todo!() + async fn play_next(&self) { + if let Some(state) = self.upgrade() { + _ = state.play_next().await.map_err(|err| log::error!("{err}")); + } } - fn go_to(&self, _track_id: Self::Path) { - todo!() + async fn play_previous(&self) { + if let Some(state) = self.upgrade() { + _ = (state.play_previous()) + .await + .map_err(|err| log::error!("{err}")); + } } - fn position(&self) -> Self::Time { - todo!() + async fn seek(&self, file: Option<Self::Path>, position: Self::Time) { + log::error!("ignore the seeking thing for now, don't go to time: {position}"); + if let Some(track_id) = file { + self.go_to(track_id).await; + } } - fn shuffle(&self) -> bool { - todo!() + async fn go_to(&self, track_id: Self::Path) { + let Some(state) = self.upgrade() else { return }; + _ = match track_id { + ObjPath::None => Ok(()), + ObjPath::Id(kid) => state.play_from_queue_kid(kid).await, + ObjPath::Position(pos) => state.play_from_queue_pos(pos).await, + } + .map_err(|err| log::error!("{err}")); } - fn set_shuffle(&self, _shuffle: bool) { - todo!() + async fn position(&self) -> Self::Time { + let seconds = crate::c_wrapper::player_get_elapsed() + .map_err(|err| log::error!("failed to get state: {err}")) + .unwrap_or_default() + .max(0) as u64; + Time(seconds) } - fn get_current_metadata(&self) -> Self::TrackMdt { - todo!() + async fn get_current_metadata(&self) -> Self::TrackMdt { + let Some(state) = self.upgrade() else { + return Default::default(); + }; + let Some(id) = state.queue().read(|content, _| content.current()).await else { + return Default::default(); + }; + let Some(epoch) = state.database.last_epoch().await else { + return Default::default(); + }; + epoch + .get_kara_by_kid(id) + .map(|kara| TrackMdt(kara).into()) + .unwrap_or_default() } - fn get_tracks_metadata(&self, _tracks_ids: Vec<Self::Path>) -> Vec<Self::TrackMdt> { - todo!() + async fn get_tracks_metadata(&self, tracks_ids: Vec<Self::Path>) -> Vec<Self::TrackMdt> { + let Some(state) = self.upgrade() else { + return vec![]; + }; + let Some(epoch) = state.database.last_epoch().await else { + return vec![]; + }; + + let mut ret = Vec::with_capacity(tracks_ids.len()); + for path in tracks_ids { + let kid = match path { + ObjPath::None => continue, + ObjPath::Id(kid) => kid, + ObjPath::Position(pos) => match state + .queue() + .read(move |content, _| { + content.get((ops::Bound::Included(pos), ops::Bound::Included(pos))) + }) + .await + .pop() + { + Some((_, id)) => id, + None => continue, + }, + }; + if let Some(kara) = epoch.get_kara_by_kid(kid) { + ret.push(TrackMdt(kara).into()); + } + } + ret } - fn get_queue_content(&self) -> Vec<Self::Path> { - todo!() + async fn get_queue_content(&self) -> Vec<Self::Path> { + let Some(state) = self.upgrade() else { + return vec![]; + }; + (state.queue()) + .read(|content, _| content.get(..)) + .await + .into_iter() + .map(|(_, kid)| ObjPath::Id(kid)) + .collect() } - fn add_track(&self, _uri: &str, _after: Self::Path, _set_as_current: bool) { - todo!() + async fn add_track(&self, uri: &str, after: Self::Path, set_as_current: bool) { + let Some(state) = self.upgrade() else { return }; + + let id = match state.database.get_kara_by_uri(uri).await { + Ok(kara) => kara.id, + Err(err) => return log::error!("{err}"), + }; + + let after = match after { + ObjPath::None => None, + ObjPath::Position(pos) => Some(pos), + ObjPath::Id(kid) => state.queue().read(|content, _| content.index_of(kid)).await, + }; + + state + .queue() + .write(|content, _| content.insert_after(after, id)) + .await; + + _ = match (after, set_as_current) { + (None, true) => state.play_next().await, + (Some(after), true) => state.play_from_queue_pos(after).await, + _ => anyhow::Ok(()), + } + .map_err(|err| log::error!("{err}")); } - fn remove_track(&self, _track_id: Self::Path) { - todo!() + async fn remove_track(&self, track_id: Self::Path) { + let Some(state) = self.upgrade() else { return }; + match track_id { + ObjPath::None => {} + ObjPath::Id(kid) => { + state + .queue() + .write(|content, _| content.delete_all(kid)) + .await + } + ObjPath::Position(pos) => { + let range = (Bound::Included(pos), Bound::Included(pos)); + state + .queue() + .write(|content, _| content.delete(range)) + .await + } + } } - fn play_file(&self, _uri: &str) { - todo!() - } + async fn play_file(&self, uri: &str) { + let Some(state) = self.upgrade() else { return }; - fn volume(&self) -> f64 { - 100.0 - } + let id = match state.database.get_kara_by_uri(uri).await { + Ok(kara) => kara.id, + Err(err) => return log::error!("{err}"), + }; - fn set_volume(&self, volume: f64) { - log::warn!("ignore volume {volume}"); + if let Err(err) = state.play_from_queue_kid(id).await { + log::error!("{err}") + } } } @@ -140,34 +264,43 @@ impl From<Time> for lektor_mpris::types::TimeMicroSec { } } -impl From<TrackMdt> for lektor_mpris::types::TrackMetadata { - fn from(TrackMdt(kara): TrackMdt) -> Self { - let mut kara_makers: Vec<_> = kara.kara_makers.iter().cloned().collect(); +impl From<TrackMdt<'_>> for lektor_mpris::types::TrackMetadata { + fn from(TrackMdt(kara): TrackMdt<'_>) -> Self { + let mut kara_makers: Vec<_> = kara.kara_makers.iter().map(Arc::as_ref).collect(); kara_makers.sort(); - let kara_makers = kara_makers.join(", "); - let mut language: Vec<_> = kara.language.iter().cloned().collect(); + let mut language: Vec<_> = kara.language.iter().map(Arc::as_ref).collect(); language.sort(); - let language = language.join(", "); lektor_mpris::types::TrackMetadata::from_iter([ ("Song Title".to_string(), kara.song_title.clone()), ("Song Source".to_string(), kara.song_source.clone()), ("Sont Type".to_string(), kara.song_type.to_string()), - ("Kara Makers".to_string(), kara_makers), - ("Languages".to_string(), language), + ("Song Origin".to_string(), kara.song_origin.to_string()), + ("Kara Makers".to_string(), kara_makers.join(", ")), + ("Languages".to_string(), language.join(", ")), ]) } } impl From<lektor_mpris::types::ObjectPath> for ObjPath { - fn from(_value: lektor_mpris::types::ObjectPath) -> Self { - todo!() + fn from(value: lektor_mpris::types::ObjectPath) -> Self { + match value { + lektor_mpris::types::ObjectPath::Id(id) => Self::Id(KId::from_u64(id)), + lektor_mpris::types::ObjectPath::Position(pos) => Self::Position(pos), + lektor_mpris::types::ObjectPath::None | lektor_mpris::types::ObjectPath::NoTrack => { + Default::default() + } + } } } impl From<ObjPath> for lektor_mpris::types::ObjectPath { - fn from(_value: ObjPath) -> Self { - todo!() + fn from(value: ObjPath) -> Self { + match value { + ObjPath::None => lektor_mpris::types::ObjectPath::NoTrack, + ObjPath::Id(kid) => lektor_mpris::types::ObjectPath::Id(kid.into()), + ObjPath::Position(pos) => lektor_mpris::types::ObjectPath::Position(pos), + } } } diff --git a/lektord/src/app/queue.rs b/lektord/src/app/queue.rs index a368cba3d0b7e150be5113402f59e92bd788c58b..4edf12f609108c49d685bf6fddf6921b40630907 100644 --- a/lektord/src/app/queue.rs +++ b/lektord/src/app/queue.rs @@ -78,10 +78,7 @@ impl QueueContent { std::ops::Bound::Unbounded => self.count(), }, ); - self.queue - .iter() - .enumerate() - .rev() + (self.queue.iter().enumerate().rev()) .flat_map(|(idx, lvl)| { let ret = (start <= lvl.len()) .then(|| (Priority::from(idx + 1), start..=(lvl.len() - 1).min(end))); @@ -237,6 +234,43 @@ impl QueueContent { self.insert_slice(prio, &kara) } + /// Insert a kara after a certain position. + pub fn insert_after(&mut self, maybe_after: Option<usize>, kid: KId) { + match maybe_after { + Some(mut pos) => { + let insert_in = (self.queue.iter().enumerate().rev()).find_map( + |(level_index, level)| match pos < level.len() { + true => Some((level_index, pos + 1)), + false => { + pos -= level.len(); + None + } + }, + ); + + match insert_in { + Some((level, position)) => self.queue[level].insert(position, kid), + None => self.queue[Priority::Add.index()].push(kid), + } + } + + None => { + let level = (self.queue.iter().enumerate().rev()) + .find_map(|(idx, level)| (!level.is_empty()).then_some(idx)) + .unwrap_or_default(); + self.queue[level].insert(0, kid); + } + } + } + + /// Get the index of the first occurance of a kara by its [KId]. + pub fn index_of(&self, kid: KId) -> Option<usize> { + self.get(..) + .into_iter() + .enumerate() + .find_map(|(idx, (_, id))| (id == kid).then_some(idx)) + } + /// Get the next kara to play from the queue and insert the old-playing one into the history. /// The returned kara is the new current one. pub fn next(&mut self) -> Option<KId> { diff --git a/lektord/src/app/routes.rs b/lektord/src/app/routes.rs index d71f918f0ff79c2afd357a8ff80c684471403b2c..17e538b87e538b2fac0a308cfcf67a10fd29ca14 100644 --- a/lektord/src/app/routes.rs +++ b/lektord/src/app/routes.rs @@ -75,12 +75,7 @@ pub(crate) async fn play_from_queue_pos( State(state): State<LektorStatePtr>, Path(position): Path<usize>, ) -> Result<(), LektordError> { - let id = state - .queue() - .write(|content, _| content.play_from_position(position)) - .await - .with_context(|| format!("position {position} doesn't exists in queue"))?; - crate::c_wrapper::player_load_file(state.database.get_kara_uri(id)?.to_string(), id)?; + state.play_from_queue_pos(position).await?; Ok(()) } @@ -90,29 +85,17 @@ pub(crate) async fn set_play_state( State(state): State<LektorStatePtr>, Json(to): Json<PlayState>, ) -> Result<(), LektordError> { - if to == PlayState::Stop { - c_wrapper::player_stop()? - } else if state - .queue() - .write(|_, state| match state.get() { - PlayState::Stop if to == PlayState::Play => Ok(true), - stop @ PlayState::Stop => { - log::trace!("can't change playback from {stop:?} to {to:?}"); - Ok(false) - } - _ => c_wrapper::player_set_paused(to).map(|_| false), - }) - .await? - { - state.play_next().await? - } + state.set_play_state(to).await?; Ok(()) } /// Toggle the play state of the server, Be aware of race conditions... #[axum::debug_handler(state = LektorStatePtr)] -pub(crate) async fn toggle_play_state() -> Result<(), LektordError> { - Ok(crate::c_wrapper::player_toggle_pause()?) +pub(crate) async fn toggle_play_state( + State(state): State<LektorStatePtr>, +) -> Result<(), LektordError> { + state.toggle_play_state()?; + Ok(()) } /// Get all the informations about a kara by its id. Returns the kara as a json object to avoid