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