mpris_server/
server.rs

1use std::{collections::HashMap, fmt, sync::Arc};
2
3use serde::Serialize;
4use zbus::{
5    fdo,
6    names::BusName,
7    zvariant::{DynamicType, ObjectPath, Value},
8    Connection, ConnectionBuilder, Interface, Result, SignalContext,
9};
10
11use crate::{
12    playlist::MaybePlaylist, LoopStatus, Metadata, PlaybackRate, PlaybackStatus, PlayerInterface,
13    Playlist, PlaylistId, PlaylistOrdering, PlaylistsInterface, PlaylistsProperty, PlaylistsSignal,
14    Property, RootInterface, Signal, Time, TrackId, TrackListInterface, TrackListProperty,
15    TrackListSignal, Uri, Volume,
16};
17
18const OBJECT_PATH: ObjectPath<'static> =
19    ObjectPath::from_static_str_unchecked("/org/mpris/MediaPlayer2");
20
21struct RawRootInterface<T> {
22    imp: Arc<T>,
23}
24
25#[zbus::interface(name = "org.mpris.MediaPlayer2")]
26impl<T> RawRootInterface<T>
27where
28    T: RootInterface + 'static,
29{
30    async fn raise(&self) -> fdo::Result<()> {
31        self.imp.raise().await
32    }
33
34    async fn quit(&self) -> fdo::Result<()> {
35        self.imp.quit().await
36    }
37
38    #[zbus(property)]
39    async fn can_quit(&self) -> fdo::Result<bool> {
40        self.imp.can_quit().await
41    }
42
43    #[zbus(property)]
44    async fn fullscreen(&self) -> fdo::Result<bool> {
45        self.imp.fullscreen().await
46    }
47
48    #[zbus(property)]
49    async fn set_fullscreen(&self, fullscreen: bool) -> Result<()> {
50        self.imp.set_fullscreen(fullscreen).await
51    }
52
53    #[zbus(property)]
54    async fn can_set_fullscreen(&self) -> fdo::Result<bool> {
55        self.imp.can_set_fullscreen().await
56    }
57
58    #[zbus(property)]
59    async fn can_raise(&self) -> fdo::Result<bool> {
60        self.imp.can_raise().await
61    }
62
63    #[zbus(property)]
64    async fn has_track_list(&self) -> fdo::Result<bool> {
65        self.imp.has_track_list().await
66    }
67
68    #[zbus(property)]
69    async fn identity(&self) -> fdo::Result<String> {
70        self.imp.identity().await
71    }
72
73    #[zbus(property)]
74    async fn desktop_entry(&self) -> fdo::Result<String> {
75        self.imp.desktop_entry().await
76    }
77
78    #[zbus(property)]
79    async fn supported_uri_schemes(&self) -> fdo::Result<Vec<String>> {
80        self.imp.supported_uri_schemes().await
81    }
82
83    #[zbus(property)]
84    async fn supported_mime_types(&self) -> fdo::Result<Vec<String>> {
85        self.imp.supported_mime_types().await
86    }
87}
88
89struct RawPlayerInterface<T> {
90    imp: Arc<T>,
91}
92
93#[zbus::interface(name = "org.mpris.MediaPlayer2.Player")]
94impl<T> RawPlayerInterface<T>
95where
96    T: PlayerInterface + 'static,
97{
98    async fn next(&self) -> fdo::Result<()> {
99        self.imp.next().await
100    }
101
102    async fn previous(&self) -> fdo::Result<()> {
103        self.imp.previous().await
104    }
105
106    async fn pause(&self) -> fdo::Result<()> {
107        self.imp.pause().await
108    }
109
110    async fn play_pause(&self) -> fdo::Result<()> {
111        self.imp.play_pause().await
112    }
113
114    async fn stop(&self) -> fdo::Result<()> {
115        self.imp.stop().await
116    }
117
118    async fn play(&self) -> fdo::Result<()> {
119        self.imp.play().await
120    }
121
122    async fn seek(&self, offset: Time) -> fdo::Result<()> {
123        self.imp.seek(offset).await
124    }
125
126    async fn set_position(&self, track_id: TrackId, position: Time) -> fdo::Result<()> {
127        self.imp.set_position(track_id, position).await
128    }
129
130    async fn open_uri(&self, uri: String) -> fdo::Result<()> {
131        self.imp.open_uri(uri).await
132    }
133
134    #[zbus(signal)]
135    async fn seeked(ctxt: &SignalContext<'_>, position: Time) -> Result<()>;
136
137    #[zbus(property)]
138    async fn playback_status(&self) -> fdo::Result<PlaybackStatus> {
139        self.imp.playback_status().await
140    }
141
142    #[zbus(property)]
143    async fn loop_status(&self) -> fdo::Result<LoopStatus> {
144        self.imp.loop_status().await
145    }
146
147    #[zbus(property)]
148    async fn set_loop_status(&self, loop_status: LoopStatus) -> Result<()> {
149        self.imp.set_loop_status(loop_status).await
150    }
151
152    #[zbus(property)]
153    async fn rate(&self) -> fdo::Result<PlaybackRate> {
154        self.imp.rate().await
155    }
156
157    #[zbus(property)]
158    async fn set_rate(&self, rate: PlaybackRate) -> Result<()> {
159        self.imp.set_rate(rate).await
160    }
161
162    #[zbus(property)]
163    async fn shuffle(&self) -> fdo::Result<bool> {
164        self.imp.shuffle().await
165    }
166
167    #[zbus(property)]
168    async fn set_shuffle(&self, shuffle: bool) -> Result<()> {
169        self.imp.set_shuffle(shuffle).await
170    }
171
172    #[zbus(property)]
173    async fn metadata(&self) -> fdo::Result<Metadata> {
174        self.imp.metadata().await
175    }
176
177    #[zbus(property)]
178    async fn volume(&self) -> fdo::Result<Volume> {
179        self.imp.volume().await
180    }
181
182    #[zbus(property)]
183    async fn set_volume(&self, volume: Volume) -> Result<()> {
184        self.imp.set_volume(volume).await
185    }
186
187    #[zbus(property(emits_changed_signal = "false"))]
188    async fn position(&self) -> fdo::Result<Time> {
189        self.imp.position().await
190    }
191
192    #[zbus(property)]
193    async fn minimum_rate(&self) -> fdo::Result<PlaybackRate> {
194        self.imp.minimum_rate().await
195    }
196
197    #[zbus(property)]
198    async fn maximum_rate(&self) -> fdo::Result<PlaybackRate> {
199        self.imp.maximum_rate().await
200    }
201
202    #[zbus(property)]
203    async fn can_go_next(&self) -> fdo::Result<bool> {
204        self.imp.can_go_next().await
205    }
206
207    #[zbus(property)]
208    async fn can_go_previous(&self) -> fdo::Result<bool> {
209        self.imp.can_go_previous().await
210    }
211
212    #[zbus(property)]
213    async fn can_play(&self) -> fdo::Result<bool> {
214        self.imp.can_play().await
215    }
216
217    #[zbus(property)]
218    async fn can_pause(&self) -> fdo::Result<bool> {
219        self.imp.can_pause().await
220    }
221
222    #[zbus(property)]
223    async fn can_seek(&self) -> fdo::Result<bool> {
224        self.imp.can_seek().await
225    }
226
227    // FIXME This should have been "const", but it is "false" in the spec.
228    #[zbus(property(emits_changed_signal = "false"))]
229    async fn can_control(&self) -> fdo::Result<bool> {
230        self.imp.can_control().await
231    }
232}
233
234struct RawTrackListInterface<T> {
235    imp: Arc<T>,
236}
237
238#[zbus::interface(name = "org.mpris.MediaPlayer2.TrackList")]
239impl<T> RawTrackListInterface<T>
240where
241    T: TrackListInterface + 'static,
242{
243    async fn get_tracks_metadata(&self, track_ids: Vec<TrackId>) -> fdo::Result<Vec<Metadata>> {
244        self.imp.get_tracks_metadata(track_ids).await
245    }
246
247    async fn add_track(
248        &self,
249        uri: Uri,
250        after_track: TrackId,
251        set_as_current: bool,
252    ) -> fdo::Result<()> {
253        self.imp.add_track(uri, after_track, set_as_current).await
254    }
255
256    async fn remove_track(&self, track_id: TrackId) -> fdo::Result<()> {
257        self.imp.remove_track(track_id).await
258    }
259
260    async fn go_to(&self, track_id: TrackId) -> fdo::Result<()> {
261        self.imp.go_to(track_id).await
262    }
263
264    #[zbus(signal)]
265    async fn track_list_replaced(
266        ctxt: &SignalContext<'_>,
267        tracks: Vec<TrackId>,
268        current_track: TrackId,
269    ) -> Result<()>;
270
271    #[zbus(signal)]
272    async fn track_added(
273        ctxt: &SignalContext<'_>,
274        metadata: Metadata,
275        after_track: TrackId,
276    ) -> Result<()>;
277
278    #[zbus(signal)]
279    async fn track_removed(ctxt: &SignalContext<'_>, track_id: TrackId) -> Result<()>;
280
281    #[zbus(signal)]
282    async fn track_metadata_changed(
283        ctxt: &SignalContext<'_>,
284        track_id: TrackId,
285        metadata: Metadata,
286    ) -> Result<()>;
287
288    #[zbus(property(emits_changed_signal = "invalidates"))]
289    async fn tracks(&self) -> fdo::Result<Vec<TrackId>> {
290        self.imp.tracks().await
291    }
292
293    #[zbus(property)]
294    async fn can_edit_tracks(&self) -> fdo::Result<bool> {
295        self.imp.can_edit_tracks().await
296    }
297}
298
299struct RawPlaylistsInterface<T> {
300    imp: Arc<T>,
301}
302
303#[zbus::interface(name = "org.mpris.MediaPlayer2.Playlists")]
304impl<T> RawPlaylistsInterface<T>
305where
306    T: PlaylistsInterface + 'static,
307{
308    async fn activate_playlist(&self, playlist_id: PlaylistId) -> fdo::Result<()> {
309        self.imp.activate_playlist(playlist_id).await
310    }
311
312    async fn get_playlists(
313        &self,
314        index: u32,
315        max_count: u32,
316        order: PlaylistOrdering,
317        reverse_order: bool,
318    ) -> fdo::Result<Vec<Playlist>> {
319        self.imp
320            .get_playlists(index, max_count, order, reverse_order)
321            .await
322    }
323
324    #[zbus(signal)]
325    async fn playlist_changed(ctxt: &SignalContext<'_>, playlist: Playlist) -> Result<()>;
326
327    #[zbus(property)]
328    async fn playlist_count(&self) -> fdo::Result<u32> {
329        self.imp.playlist_count().await
330    }
331
332    #[zbus(property)]
333    async fn orderings(&self) -> fdo::Result<Vec<PlaylistOrdering>> {
334        self.imp.orderings().await
335    }
336
337    #[zbus(property)]
338    async fn active_playlist(&self) -> fdo::Result<MaybePlaylist> {
339        self.imp.active_playlist().await.map(MaybePlaylist::from)
340    }
341}
342
343/// Thin wrapper around [`zbus::Connection`] that calls to `T`'s implementation
344/// of [`RootInterface`], [`PlayerInterface`], [`TrackListInterface`], and
345/// [`PlaylistsInterface`] to implement `org.mpris.MediaPlayer2` and its
346/// sub-interfaces.
347///
348/// When implementing using [`Server`], it is important to note that properties
349/// changed signals are *not* emitted automatically; they must be emitted
350/// manually using [`Server::properties_changed`],
351/// [`Server::track_list_properties_changed`], or
352/// [`Server::playlists_properties_changed`], when they changed internally.
353pub struct Server<T> {
354    connection: Connection,
355    imp: Arc<T>,
356}
357
358impl<T> fmt::Debug for Server<T> {
359    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
360        f.debug_struct("Server").finish()
361    }
362}
363
364macro_rules! insert_property {
365    ($item:ident, $property_type:ident => $($map:ident, $property:ident);*) => {
366        match $item {
367            $(
368                $property_type::$property(val) => {
369                    $map.insert(stringify!($property), Value::new(val));
370                }
371            )*
372        }
373    };
374}
375
376impl<T> Server<T>
377where
378    T: PlayerInterface + 'static,
379{
380    /// Creates a new [`Server`] with the given bus name suffix and
381    /// implementation, `imp`, which must implement [`RootInterface`] and
382    /// [`PlayerInterface`].
383    ///
384    /// The resulting bus name will be
385    /// `org.mpris.MediaPlayer2.<bus_name_suffix>`, where
386    /// `<bus_name_suffix>`must be a unique identifier, such as one based on a
387    /// UNIX process id. For example, this could be:
388    ///
389    /// * `org.mpris.MediaPlayer2.vlc.instance7389`
390    ///
391    /// **Note:** According to the [D-Bus specification], the unique
392    /// identifier "must only contain  the ASCII characters
393    /// `[A-Z][a-z][0-9]_-`" and "must not begin with a digit".
394    ///
395    /// [D-Bus specification]: https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-names-bus
396    pub async fn new(bus_name_suffix: &str, imp: T) -> Result<Self> {
397        Self::new_inner(bus_name_suffix, imp, |builder, _| Ok(builder)).await
398    }
399
400    /// Returns a reference to the underlying implementation.
401    #[inline]
402    pub fn imp(&self) -> &T {
403        &self.imp
404    }
405
406    /// Returns a reference to the inner [`Connection`].
407    ///
408    /// If you needed to call this, consider filing an issue.
409    #[cfg(feature = "unstable")]
410    #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))]
411    #[inline]
412    pub fn connection(&self) -> &Connection {
413        &self.connection
414    }
415
416    /// Emits the given signal.
417    pub async fn emit(&self, signal: Signal) -> Result<()> {
418        match signal {
419            Signal::Seeked { position } => {
420                self.emit_inner::<RawPlayerInterface<T>>("Seeked", &(position,))
421                    .await?;
422            }
423        }
424
425        Ok(())
426    }
427
428    /// Emits the `PropertiesChanged` signal for the given properties.
429    ///
430    /// This categorizes the property in the `changed` or `invalidated`
431    /// properties as defined by the spec.
432    ///
433    /// [`Server::track_list_properties_changed`] or
434    /// [`Server::playlists_properties_changed`] are used
435    /// to emit `PropertiesChanged` for the `TrackList` or `Playlists`
436    /// interfaces respectively.
437    pub async fn properties_changed(
438        &self,
439        properties: impl IntoIterator<Item = Property>,
440    ) -> Result<()> {
441        let mut root_changed = HashMap::new();
442        let mut player_changed = HashMap::new();
443
444        for property in properties.into_iter() {
445            insert_property!(
446                property, Property =>
447                root_changed, CanQuit;
448                root_changed, Fullscreen;
449                root_changed, CanSetFullscreen;
450                root_changed, CanRaise;
451                root_changed, HasTrackList;
452                root_changed, Identity;
453                root_changed, DesktopEntry;
454                root_changed, SupportedUriSchemes;
455                root_changed, SupportedMimeTypes;
456                player_changed, PlaybackStatus;
457                player_changed, LoopStatus;
458                player_changed, Rate;
459                player_changed, Shuffle;
460                player_changed, Metadata;
461                player_changed, Volume;
462                player_changed, MinimumRate;
463                player_changed, MaximumRate;
464                player_changed, CanGoNext;
465                player_changed, CanGoPrevious;
466                player_changed, CanPlay;
467                player_changed, CanPause;
468                player_changed, CanSeek
469            );
470        }
471
472        if !root_changed.is_empty() {
473            self.properties_changed_inner::<RawRootInterface<T>>(root_changed, &[])
474                .await?;
475        }
476
477        if !player_changed.is_empty() {
478            self.properties_changed_inner::<RawPlayerInterface<T>>(player_changed, &[])
479                .await?;
480        }
481
482        Ok(())
483    }
484
485    async fn new_inner(
486        bus_name_suffix: &str,
487        imp: T,
488        builder_ext_func: impl FnOnce(ConnectionBuilder<'_>, Arc<T>) -> Result<ConnectionBuilder<'_>>
489            + Send
490            + Sync
491            + 'static,
492    ) -> Result<Self> {
493        let imp = Arc::new(imp);
494
495        let connection_builder = ConnectionBuilder::session()?
496            .name(format!("org.mpris.MediaPlayer2.{}", bus_name_suffix))?
497            .serve_at(
498                OBJECT_PATH,
499                RawRootInterface {
500                    imp: Arc::clone(&imp),
501                },
502            )?
503            .serve_at(
504                OBJECT_PATH,
505                RawPlayerInterface {
506                    imp: Arc::clone(&imp),
507                },
508            )?;
509        let connection = builder_ext_func(connection_builder, Arc::clone(&imp))?
510            .build()
511            .await?;
512
513        Ok(Self { connection, imp })
514    }
515
516    async fn properties_changed_inner<I>(
517        &self,
518        changed_properties: HashMap<&str, Value<'_>>,
519        invalidated_properties: &[&str],
520    ) -> Result<()>
521    where
522        I: Interface,
523    {
524        self.emit_inner::<fdo::Properties>(
525            "PropertiesChanged",
526            &(I::name(), changed_properties, invalidated_properties),
527        )
528        .await
529    }
530
531    async fn emit_inner<I>(
532        &self,
533        signal_name: &str,
534        body: &(impl Serialize + DynamicType),
535    ) -> Result<()>
536    where
537        I: Interface,
538    {
539        self.connection
540            .emit_signal(
541                None::<BusName<'_>>,
542                OBJECT_PATH,
543                I::name(),
544                signal_name,
545                body,
546            )
547            .await
548    }
549}
550
551impl<T> Server<T>
552where
553    T: TrackListInterface + 'static,
554{
555    /// Creates a new [`Server`] with the given bus name suffix and
556    /// implementation, which must implement [`TrackListInterface`] in addition
557    /// to [`RootInterface`] and [`PlayerInterface`].
558    ///
559    /// See also [`Server::new`].
560    pub async fn new_with_track_list(bus_name_suffix: &str, imp: T) -> Result<Self> {
561        Self::new_inner(bus_name_suffix, imp, |builder, imp| {
562            builder.serve_at(OBJECT_PATH, RawTrackListInterface { imp })
563        })
564        .await
565    }
566
567    /// Emits the given signal on the `TrackList` interface.
568    pub async fn track_list_emit(&self, signal: TrackListSignal) -> Result<()> {
569        match signal {
570            TrackListSignal::TrackListReplaced {
571                tracks,
572                current_track,
573            } => {
574                self.emit_inner::<RawTrackListInterface<T>>(
575                    "TrackListReplaced",
576                    &(tracks, current_track),
577                )
578                .await?;
579            }
580            TrackListSignal::TrackAdded {
581                metadata,
582                after_track,
583            } => {
584                self.emit_inner::<RawTrackListInterface<T>>("TrackAdded", &(metadata, after_track))
585                    .await?;
586            }
587            TrackListSignal::TrackRemoved { track_id } => {
588                self.emit_inner::<RawTrackListInterface<T>>("TrackRemoved", &(track_id,))
589                    .await?;
590            }
591            TrackListSignal::TrackMetadataChanged { track_id, metadata } => {
592                self.emit_inner::<RawTrackListInterface<T>>(
593                    "TrackMetadataChanged",
594                    &(track_id, metadata),
595                )
596                .await?;
597            }
598        }
599
600        Ok(())
601    }
602
603    /// Emits the `PropertiesChanged` signal for the given properties.
604    ///
605    /// This categorizes the property in the `changed` or `invalidated`
606    /// properties as defined by the spec.
607    pub async fn track_list_properties_changed(
608        &self,
609        properties: impl IntoIterator<Item = TrackListProperty>,
610    ) -> Result<()> {
611        let mut changed = HashMap::new();
612        let mut invalidated = Vec::new();
613
614        for property in properties.into_iter() {
615            match property {
616                TrackListProperty::Tracks => {
617                    invalidated.push("Tracks");
618                }
619                TrackListProperty::CanEditTracks(can_edit_tracks) => {
620                    changed.insert("CanEditTracks", Value::new(can_edit_tracks));
621                }
622            }
623        }
624
625        if !changed.is_empty() || !invalidated.is_empty() {
626            self.properties_changed_inner::<RawTrackListInterface<T>>(changed, &invalidated)
627                .await?;
628        }
629
630        Ok(())
631    }
632}
633
634impl<T> Server<T>
635where
636    T: PlaylistsInterface + 'static,
637{
638    /// Creates a new [`Server`] with the given bus name suffix and
639    /// implementation, which must implement [`PlaylistsInterface`] in addition
640    /// to [`RootInterface`] and [`PlayerInterface`].
641    ///
642    /// See also [`Server::new`].
643    pub async fn new_with_playlists(bus_name_suffix: &str, imp: T) -> Result<Self> {
644        Self::new_inner(bus_name_suffix, imp, |builder, imp| {
645            builder.serve_at(OBJECT_PATH, RawPlaylistsInterface { imp })
646        })
647        .await
648    }
649
650    /// Emits the given signal on the `Playlists` interface.
651    pub async fn playlists_emit(&self, signal: PlaylistsSignal) -> Result<()> {
652        match signal {
653            PlaylistsSignal::PlaylistChanged { playlist } => {
654                self.emit_inner::<RawPlaylistsInterface<T>>("PlaylistChanged", &(playlist,))
655                    .await?;
656            }
657        }
658
659        Ok(())
660    }
661
662    /// Emits the `PropertiesChanged` signal for the given properties.
663    ///
664    /// This categorizes the property in the `changed` or `invalidated`
665    /// properties as defined by the spec.
666    pub async fn playlists_properties_changed(
667        &self,
668        properties: impl IntoIterator<Item = PlaylistsProperty>,
669    ) -> Result<()> {
670        let mut changed = HashMap::new();
671
672        for property in properties.into_iter() {
673            match property {
674                PlaylistsProperty::PlaylistCount(playlist_count) => {
675                    changed.insert("PlaylistCount", Value::new(playlist_count));
676                }
677                PlaylistsProperty::Orderings(orderings) => {
678                    changed.insert("Orderings", Value::new(orderings));
679                }
680                PlaylistsProperty::ActivePlaylist(active_playlist) => {
681                    changed.insert(
682                        "ActivePlaylist",
683                        Value::new(MaybePlaylist::from(active_playlist)),
684                    );
685                }
686            }
687        }
688
689        if !changed.is_empty() {
690            self.properties_changed_inner::<RawPlaylistsInterface<T>>(changed, &[])
691                .await?;
692        }
693
694        Ok(())
695    }
696}
697
698impl<T> Server<T>
699where
700    T: TrackListInterface + PlaylistsInterface + 'static,
701{
702    /// Creates a new [`Server`] with the given bus name suffix and
703    /// implementation, which must implement [`TrackListInterface`] and
704    /// [`PlaylistsInterface`] in addition to [`RootInterface`] and
705    /// [`PlayerInterface`].
706    ///
707    /// See also [`Server::new`].
708    pub async fn new_with_all(bus_name_suffix: &str, imp: T) -> Result<Self> {
709        Self::new_inner(bus_name_suffix, imp, |builder, imp| {
710            builder
711                .serve_at(
712                    OBJECT_PATH,
713                    RawTrackListInterface {
714                        imp: Arc::clone(&imp),
715                    },
716                )?
717                .serve_at(OBJECT_PATH, RawPlaylistsInterface { imp })
718        })
719        .await
720    }
721}
722
723#[cfg(test)]
724mod tests {
725    use static_assertions::assert_impl_all;
726
727    use super::Server;
728
729    #[allow(dead_code)]
730    pub struct TestPlayer;
731
732    assert_impl_all!(Server<TestPlayer>: Send, Sync, Unpin);
733}