mpris_server/
metadata.rs

1use std::{collections::HashMap, fmt};
2
3use serde::Serialize;
4use zbus::zvariant::{Error, Result, Type, Value};
5
6use crate::{Time, TrackId, Uri};
7
8/// Combined date and time.
9///
10/// This should be sent as strings in ISO 8601 extended
11/// format (eg: 2007-04-29T14:35:51). If the timezone is known (eg: for
12/// xesam:lastPlayed), the internet profile format of ISO 8601, as specified in
13/// RFC 3339, should be used (eg: 2007-04-29T14:35:51+02:00).
14///
15/// For example: "2007-04-29T13:56+01:00" for 29th April 2007, four
16/// minutes to 2pm, in a time zone 1 hour ahead of UTC.
17pub type DateTime = String;
18
19/// A mapping from metadata attribute names to values.
20///
21/// The [`mpris:trackid`] attribute must always be present.
22///
23/// If the length of the track is known, it should be provided in the metadata
24/// property with the [`mpris:length`] key.
25///
26/// If there is an image associated with the track, a URL for it may be provided
27/// using the [`mpris:artUrl`] key.
28///
29/// [`mpris:trackid`]: Metadata::set_trackid
30/// [`mpris:length`]: Metadata::set_length
31/// [`mpris:artUrl`]: Metadata::set_art_url
32#[derive(PartialEq, Serialize, Type)]
33#[serde(transparent)]
34#[zvariant(signature = "a{sv}")]
35#[doc(alias = "Metadata_Map")]
36pub struct Metadata(HashMap<String, Value<'static>>);
37
38impl Clone for Metadata {
39    fn clone(&self) -> Self {
40        // TODO Make this more efficient
41        Self(
42            self.0
43                .iter()
44                .map(|(k, v)| (k.clone(), v.try_clone().expect("metadata contained an fd")))
45                .collect::<HashMap<_, _>>(),
46        )
47    }
48}
49
50impl fmt::Debug for Metadata {
51    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52        fmt::Debug::fmt(&self.0, f)
53    }
54}
55
56impl Default for Metadata {
57    fn default() -> Self {
58        Self::new()
59    }
60}
61
62impl Metadata {
63    /// Creates an empty [`Metadata`].
64    pub fn new() -> Self {
65        Self(HashMap::new())
66    }
67
68    /// Creates a new builder-pattern struct instance to construct [`Metadata`].
69    pub fn builder() -> MetadataBuilder {
70        MetadataBuilder { m: Metadata::new() }
71    }
72
73    /// Returns the value corresponding to the key and convert it to `V`.
74    pub fn get<'v, V>(&'v self, key: &str) -> Option<Result<&'v V>>
75    where
76        &'v V: TryFrom<&'v Value<'v>>,
77        <&'v V as TryFrom<&'v Value<'v>>>::Error: Into<Error>,
78    {
79        self.get_value(key).map(|v| v.downcast_ref())
80    }
81
82    /// Returns a reference to the value corresponding to the key.
83    pub fn get_value(&self, key: &str) -> Option<&Value<'_>> {
84        self.0.get(key)
85    }
86
87    /// Replaces the value for the given key and returns the previous value, if
88    /// any.
89    ///
90    /// The entry is removed if the given value is `None`.
91    pub fn set(
92        &mut self,
93        key: &str,
94        value: Option<impl Into<Value<'static>>>,
95    ) -> Option<Value<'static>> {
96        self.set_value(key, value.map(|value| value.into()))
97    }
98
99    /// Replaces the value for the given key and returns the previous value, if
100    /// any.
101    ///
102    /// The entry is removed if the given value is `None`.
103    ///
104    /// This behaves like [`Metadata::set`], but this takes a [`enum@Value`]
105    /// instead of a generic type.
106    pub fn set_value(
107        &mut self,
108        key: &str,
109        value: Option<Value<'static>>,
110    ) -> Option<Value<'static>> {
111        if let Some(value) = value {
112            self.0.insert(key.into(), value)
113        } else {
114            self.0.remove(key)
115        }
116    }
117
118    /// A unique identity for this track within the context of an
119    /// MPRIS object (eg: tracklist).
120    ///
121    /// This contains a D-Bus path that uniquely identifies the track
122    /// within the scope of the playlist. There may or may not be an actual
123    /// D-Bus object at that path; this specification says nothing about
124    /// what interfaces such an object may implement.
125    pub fn trackid(&self) -> Option<TrackId> {
126        self.get_value("mpris:trackid")?.downcast_ref().ok()
127    }
128
129    /// Sets a unique identity for this track within the context of an
130    /// MPRIS object (eg: tracklist).
131    ///
132    /// This contains a D-Bus path that uniquely identifies the track
133    /// within the scope of the playlist. There may or may not be an actual
134    /// D-Bus object at that path; this specification says nothing about
135    /// what interfaces such an object may implement.
136    pub fn set_trackid(&mut self, trackid: Option<impl Into<TrackId>>) {
137        self.set("mpris:trackid", trackid.map(|trackid| trackid.into()));
138    }
139
140    /// The duration of the track.
141    pub fn length(&self) -> Option<Time> {
142        self.get_value("mpris:length")?.downcast_ref().ok()
143    }
144
145    /// Sets the duration of the track.
146    pub fn set_length(&mut self, length: Option<Time>) {
147        self.set("mpris:length", length);
148    }
149
150    /// The location of an image representing the track or album.
151    ///
152    /// Clients should not assume this will continue to exist when
153    /// the media player stops giving out the URL.
154    pub fn art_url(&self) -> Option<Uri> {
155        self.get_value("mpris:artUrl")?.downcast_ref().ok()
156    }
157
158    /// Sets the location of an image representing the track or album.
159    ///
160    /// Clients should not assume this will continue to exist when
161    /// the media player stops giving out the URL.
162    pub fn set_art_url(&mut self, art_url: Option<impl Into<Uri>>) {
163        self.set("mpris:artUrl", art_url.map(|art_url| art_url.into()));
164    }
165
166    /// The album name.
167    pub fn album(&self) -> Option<&str> {
168        self.get_value("xesam:album")?.downcast_ref().ok()
169    }
170
171    /// Sets the album name.
172    pub fn set_album(&mut self, album: Option<impl Into<String>>) {
173        self.set("xesam:album", album.map(|album| album.into()));
174    }
175
176    /// The album artist(s).
177    pub fn album_artist(&self) -> Option<Vec<String>> {
178        self.get_value("xesam:albumArtist")?
179            .try_clone()
180            .ok()
181            .and_then(|v| v.downcast().ok())
182    }
183
184    /// Sets the album artist(s).
185    pub fn set_album_artist(
186        &mut self,
187        album_artist: Option<impl IntoIterator<Item = impl Into<String>>>,
188    ) {
189        self.set(
190            "xesam:albumArtist",
191            album_artist.map(|album_artist| {
192                album_artist
193                    .into_iter()
194                    .map(|i| i.into())
195                    .collect::<Vec<_>>()
196            }),
197        );
198    }
199
200    /// The track artist(s).
201    pub fn artist(&self) -> Option<Vec<String>> {
202        self.get_value("xesam:artist")?
203            .try_clone()
204            .ok()
205            .and_then(|v| v.downcast().ok())
206    }
207
208    /// Sets the track artist(s).
209    pub fn set_artist(&mut self, artist: Option<impl IntoIterator<Item = impl Into<String>>>) {
210        self.set(
211            "xesam:artist",
212            artist.map(|artist| artist.into_iter().map(|i| i.into()).collect::<Vec<_>>()),
213        );
214    }
215
216    /// The track lyrics.
217    pub fn lyrics(&self) -> Option<&str> {
218        self.get_value("xesam:asText")?.downcast_ref().ok()
219    }
220
221    /// Sets the track lyrics.
222    pub fn set_lyrics(&mut self, lyrics: Option<impl Into<String>>) {
223        self.set("xesam:asText", lyrics.map(|lyrics| lyrics.into()));
224    }
225
226    /// The speed of the music, in beats per minute.
227    pub fn audio_bpm(&self) -> Option<i32> {
228        self.get_value("xesam:audioBPM")?.downcast_ref().ok()
229    }
230
231    /// Sets the speed of the music, in beats per minute.
232    pub fn set_audio_bpm(&mut self, audio_bpm: Option<i32>) {
233        self.set("xesam:audioBPM", audio_bpm);
234    }
235
236    /// An automatically-generated rating, based on things such
237    /// as how often it has been played. This should be in the
238    /// range 0.0 to 1.0.
239    pub fn auto_rating(&self) -> Option<f64> {
240        self.get_value("xesam:autoRating")?.downcast_ref().ok()
241    }
242
243    /// Sets an automatically-generated rating, based on things such
244    /// as how often it has been played. This should be in the
245    /// range 0.0 to 1.0.
246    pub fn set_auto_rating(&mut self, auto_rating: Option<f64>) {
247        self.set("xesam:autoRating", auto_rating);
248    }
249
250    /// A (list of) freeform comment(s).
251    pub fn comment(&self) -> Option<Vec<String>> {
252        self.get_value("xesam:comment")?
253            .try_clone()
254            .ok()
255            .and_then(|v| v.downcast().ok())
256    }
257
258    /// Sets a (list of) freeform comment(s).
259    pub fn set_comment(&mut self, comment: Option<impl IntoIterator<Item = impl Into<String>>>) {
260        self.set(
261            "xesam:comment",
262            comment.map(|comment| comment.into_iter().map(|i| i.into()).collect::<Vec<_>>()),
263        );
264    }
265
266    /// The composer(s) of the track.
267    pub fn composer(&self) -> Option<Vec<String>> {
268        self.get_value("xesam:composer")?
269            .try_clone()
270            .ok()
271            .and_then(|v| v.downcast().ok())
272    }
273
274    /// Sets the composer(s) of the track.
275    pub fn set_composer(&mut self, composer: Option<impl IntoIterator<Item = impl Into<String>>>) {
276        self.set(
277            "xesam:composer",
278            composer.map(|composer| composer.into_iter().map(|i| i.into()).collect::<Vec<_>>()),
279        );
280    }
281
282    /// When the track was created. Usually only the year component
283    /// will be useful.
284    pub fn content_created(&self) -> Option<DateTime> {
285        self.get_value("xesam:contentCreated")?.downcast_ref().ok()
286    }
287
288    /// Sets when the track was created. Usually only the year component
289    /// will be useful.
290    pub fn set_content_created(&mut self, content_created: Option<impl Into<DateTime>>) {
291        self.set(
292            "xesam:contentCreated",
293            content_created.map(|content_created| content_created.into()),
294        );
295    }
296
297    /// The disc number on the album that this track is from.
298    pub fn disc_number(&self) -> Option<i32> {
299        self.get_value("xesam:discNumber")?.downcast_ref().ok()
300    }
301
302    /// Sets the disc number on the album that this track is from.
303    pub fn set_disc_number(&mut self, disc_number: Option<i32>) {
304        self.set("xesam:discNumber", disc_number);
305    }
306
307    /// When the track was first played.
308    pub fn first_used(&self) -> Option<DateTime> {
309        self.get_value("xesam:firstUsed")?.downcast_ref().ok()
310    }
311
312    /// Sets when the track was first played.
313    pub fn set_first_used(&mut self, first_used: Option<impl Into<DateTime>>) {
314        self.set(
315            "xesam:firstUsed",
316            first_used.map(|first_used| first_used.into()),
317        );
318    }
319
320    /// The genre(s) of the track.
321    pub fn genre(&self) -> Option<Vec<String>> {
322        self.get_value("xesam:genre")?
323            .try_clone()
324            .ok()
325            .and_then(|v| v.downcast().ok())
326    }
327
328    /// Sets the genre(s) of the track.
329    pub fn set_genre(&mut self, genre: Option<impl IntoIterator<Item = impl Into<String>>>) {
330        self.set(
331            "xesam:genre",
332            genre.map(|genre| genre.into_iter().map(|i| i.into()).collect::<Vec<_>>()),
333        );
334    }
335
336    /// When the track was last played.
337    pub fn last_used(&self) -> Option<DateTime> {
338        self.get_value("xesam:lastUsed")?.downcast_ref().ok()
339    }
340
341    /// Sets when the track was last played.
342    pub fn set_last_used(&mut self, last_used: Option<impl Into<DateTime>>) {
343        self.set(
344            "xesam:lastUsed",
345            last_used.map(|last_used| last_used.into()),
346        );
347    }
348
349    /// The lyricist(s) of the track.
350    pub fn lyricist(&self) -> Option<Vec<String>> {
351        self.get_value("xesam:lyricist")?
352            .try_clone()
353            .ok()
354            .and_then(|v| v.downcast().ok())
355    }
356
357    /// Sets the lyricist(s) of the track.
358    pub fn set_lyricist(&mut self, lyricist: Option<impl IntoIterator<Item = impl Into<String>>>) {
359        self.set(
360            "xesam:lyricist",
361            lyricist.map(|lyricist| lyricist.into_iter().map(|i| i.into()).collect::<Vec<_>>()),
362        );
363    }
364
365    /// The track title.
366    pub fn title(&self) -> Option<&str> {
367        self.get_value("xesam:title")?.downcast_ref().ok()
368    }
369
370    /// Sets the track title.
371    pub fn set_title(&mut self, title: Option<impl Into<String>>) {
372        self.set("xesam:title", title.map(|title| title.into()));
373    }
374
375    /// The track number on the album disc.
376    pub fn track_number(&self) -> Option<i32> {
377        self.get_value("xesam:trackNumber")?.downcast_ref().ok()
378    }
379
380    /// Sets the track number on the album disc.
381    pub fn set_track_number(&mut self, track_number: Option<i32>) {
382        self.set("xesam:trackNumber", track_number);
383    }
384
385    /// The location of the media file.
386    pub fn url(&self) -> Option<Uri> {
387        self.get_value("xesam:url")?.downcast_ref().ok()
388    }
389
390    /// Sets the location of the media file.
391    pub fn set_url(&mut self, url: Option<impl Into<Uri>>) {
392        self.set("xesam:url", url.map(|url| url.into()));
393    }
394
395    /// The number of times the track has been played.
396    pub fn use_count(&self) -> Option<i32> {
397        self.get_value("xesam:useCount")?.downcast_ref().ok()
398    }
399
400    /// Sets the number of times the track has been played.
401    pub fn set_use_count(&mut self, use_count: Option<i32>) {
402        self.set("xesam:useCount", use_count);
403    }
404
405    /// A user-specified rating. This should be in the range 0.0 to 1.0.
406    pub fn user_rating(&self) -> Option<f64> {
407        self.get_value("xesam:userRating")?.downcast_ref().ok()
408    }
409
410    /// Sets a user-specified rating. This should be in the range 0.0 to 1.0.
411    pub fn set_user_rating(&mut self, user_rating: Option<f64>) {
412        self.set("xesam:userRating", user_rating);
413    }
414}
415
416/// A builder used to create [`Metadata`].
417#[derive(Debug, Default, Clone)]
418#[must_use = "must call `build()` to finish building the metadata"]
419pub struct MetadataBuilder {
420    m: Metadata,
421}
422
423impl MetadataBuilder {
424    /// Sets a value for the given key.
425    pub fn other(mut self, key: &str, value: impl Into<Value<'static>>) -> Self {
426        self.m.set(key, Some(value));
427        self
428    }
429
430    /// Sets a unique identity for this track within the context of an
431    /// MPRIS object (eg: tracklist).
432    ///
433    /// This contains a D-Bus path that uniquely identifies the track
434    /// within the scope of the playlist. There may or may not be an actual
435    /// D-Bus object at that path; this specification says nothing about
436    /// what interfaces such an object may implement.
437    pub fn trackid(mut self, trackid: impl Into<TrackId>) -> Self {
438        self.m.set_trackid(Some(trackid));
439        self
440    }
441
442    /// Sets the duration of the track.
443    pub fn length(mut self, length: Time) -> Self {
444        self.m.set_length(Some(length));
445        self
446    }
447
448    /// Sets the location of an image representing the track or album.
449    ///
450    /// Clients should not assume this will continue to exist when
451    /// the media player stops giving out the URL.
452    pub fn art_url(mut self, art_url: impl Into<Uri>) -> Self {
453        self.m.set_art_url(Some(art_url));
454        self
455    }
456
457    /// Sets the album name.
458    pub fn album(mut self, album: impl Into<String>) -> Self {
459        self.m.set_album(Some(album));
460        self
461    }
462
463    /// Sets the album artist(s).
464    pub fn album_artist(
465        mut self,
466        album_artist: impl IntoIterator<Item = impl Into<String>>,
467    ) -> Self {
468        self.m.set_album_artist(Some(album_artist));
469        self
470    }
471
472    /// Sets the track artist(s).
473    pub fn artist(mut self, artist: impl IntoIterator<Item = impl Into<String>>) -> Self {
474        self.m.set_artist(Some(artist));
475        self
476    }
477
478    /// Sets the track lyrics.
479    pub fn lyrics(mut self, lyrics: impl Into<String>) -> Self {
480        self.m.set_lyrics(Some(lyrics));
481        self
482    }
483
484    /// Sets the speed of the music, in beats per minute.
485    pub fn audio_bpm(mut self, audio_bpm: i32) -> Self {
486        self.m.set_audio_bpm(Some(audio_bpm));
487        self
488    }
489
490    /// Sets an automatically-generated rating, based on things such
491    /// as how often it has been played. This should be in the
492    /// range 0.0 to 1.0.
493    pub fn auto_rating(mut self, auto_rating: f64) -> Self {
494        self.m.set_auto_rating(Some(auto_rating));
495        self
496    }
497
498    /// Sets a (list of) freeform comment(s).
499    pub fn comment(mut self, comment: impl IntoIterator<Item = impl Into<String>>) -> Self {
500        self.m.set_comment(Some(comment));
501        self
502    }
503
504    /// Sets the composer(s) of the track.
505    pub fn composer(mut self, composer: impl IntoIterator<Item = impl Into<String>>) -> Self {
506        self.m.set_composer(Some(composer));
507        self
508    }
509
510    /// Sets when the track was created. Usually only the year component
511    /// will be useful.
512    pub fn content_created(mut self, content_created: impl Into<DateTime>) -> Self {
513        self.m.set_content_created(Some(content_created));
514        self
515    }
516
517    /// Sets the disc number on the album that this track is from.
518    pub fn disc_number(mut self, disc_number: i32) -> Self {
519        self.m.set_disc_number(Some(disc_number));
520        self
521    }
522
523    /// Sets when the track was first played.
524    pub fn first_used(mut self, first_used: impl Into<DateTime>) -> Self {
525        self.m.set_first_used(Some(first_used));
526        self
527    }
528
529    /// Sets the genre(s) of the track.
530    pub fn genre(mut self, genre: impl IntoIterator<Item = impl Into<String>>) -> Self {
531        self.m.set_genre(Some(genre));
532        self
533    }
534
535    /// Sets when the track was last played.
536    pub fn last_used(mut self, last_used: impl Into<DateTime>) -> Self {
537        self.m.set_last_used(Some(last_used));
538        self
539    }
540
541    /// Sets the lyricist(s) of the track.
542    pub fn lyricist(mut self, lyricist: impl IntoIterator<Item = impl Into<String>>) -> Self {
543        self.m.set_lyricist(Some(lyricist));
544        self
545    }
546
547    /// Sets the track title.
548    pub fn title(mut self, title: impl Into<String>) -> Self {
549        self.m.set_title(Some(title));
550        self
551    }
552
553    /// Sets the track number on the album disc.
554    pub fn track_number(mut self, track_number: i32) -> Self {
555        self.m.set_track_number(Some(track_number));
556        self
557    }
558
559    /// Sets the location of the media file.
560    pub fn url(mut self, url: impl Into<Uri>) -> Self {
561        self.m.set_url(Some(url));
562        self
563    }
564
565    /// Sets the number of times the track has been played.
566    pub fn use_count(mut self, use_count: i32) -> Self {
567        self.m.set_use_count(Some(use_count));
568        self
569    }
570
571    /// Sets a user-specified rating. This should be in the range 0.0 to 1.0.
572    pub fn user_rating(mut self, user_rating: f64) -> Self {
573        self.m.set_user_rating(Some(user_rating));
574        self
575    }
576
577    /// Creates [`Metadata`] from the builder.
578    #[must_use = "building metadata is usually expensive and is not expected to have side effects"]
579    pub fn build(self) -> Metadata {
580        self.m
581    }
582}
583
584impl From<Metadata> for Value<'_> {
585    fn from(metainfo: Metadata) -> Self {
586        Value::new(metainfo.0)
587    }
588}
589
590#[cfg(test)]
591mod tests {
592    use zbus::zvariant::Str;
593
594    use super::*;
595
596    #[test]
597    fn clone() {
598        let original = Metadata::builder().trackid(TrackId::NO_TRACK).build();
599        assert_eq!(original, original.clone());
600    }
601
602    #[test]
603    fn builder_and_getter() {
604        let m = Metadata::builder()
605            .other("other", "value")
606            .trackid(TrackId::try_from("/io/github/seadve/Player/Track123").unwrap())
607            .length(Time::from_millis(2))
608            .art_url("file:///tmp/cover.jpg")
609            .album("The Album")
610            .album_artist(vec!["The Album Artist".to_string()])
611            .artist(vec!["The Artist".to_string()])
612            .lyrics("The lyrics")
613            .audio_bpm(120)
614            .auto_rating(0.5)
615            .comment(vec!["The comment".to_string()])
616            .composer(vec!["The Composer".to_string()])
617            .content_created("2021-01-01T00:00:00".to_string())
618            .disc_number(3)
619            .first_used("2021-01-01T00:00:00".to_string())
620            .genre(vec!["The Genre".to_string()])
621            .last_used("2021-01-01T00:00:00".to_string())
622            .lyricist(vec!["The Lyricist".to_string()])
623            .title("The Title")
624            .track_number(2)
625            .url("file:///tmp/track.mp3")
626            .use_count(1)
627            .user_rating(0.5)
628            .build();
629
630        assert_eq!(
631            m.get::<Str<'_>>("other"),
632            Some(Ok(&Str::from_static("value")))
633        );
634        assert_eq!(
635            m.trackid(),
636            Some(TrackId::try_from("/io/github/seadve/Player/Track123").unwrap())
637        );
638        assert_eq!(m.length(), Some(Time::from_millis(2)));
639        assert_eq!(m.art_url(), Some("file:///tmp/cover.jpg".into()));
640        assert_eq!(m.album(), Some("The Album"));
641        assert_eq!(m.album_artist(), Some(vec!["The Album Artist".to_string()]));
642        assert_eq!(m.artist(), Some(vec!["The Artist".to_string()]));
643        assert_eq!(m.lyrics(), Some("The lyrics"));
644        assert_eq!(m.audio_bpm(), Some(120));
645        assert_eq!(m.auto_rating(), Some(0.5));
646        assert_eq!(m.comment(), Some(vec!["The comment".to_string()]));
647        assert_eq!(m.composer(), Some(vec!["The Composer".to_string()]));
648        assert_eq!(m.content_created(), Some("2021-01-01T00:00:00".to_string()));
649        assert_eq!(m.disc_number(), Some(3));
650        assert_eq!(m.first_used(), Some("2021-01-01T00:00:00".to_string()));
651        assert_eq!(m.genre(), Some(vec!["The Genre".to_string()]));
652        assert_eq!(m.last_used(), Some("2021-01-01T00:00:00".to_string()));
653        assert_eq!(m.lyricist(), Some(vec!["The Lyricist".to_string()]));
654        assert_eq!(m.title(), Some("The Title"));
655        assert_eq!(m.track_number(), Some(2));
656        assert_eq!(m.url(), Some("file:///tmp/track.mp3".into()));
657        assert_eq!(m.use_count(), Some(1));
658        assert_eq!(m.user_rating(), Some(0.5));
659    }
660}