mpris_server/
server.rs

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