1use std::collections::HashMap;
3use std::fmt::{self, Debug, Display};
4use std::ops::{Deref, DerefMut};
5
6use jiff::{SignedDuration, Timestamp};
7use serde::{Deserialize, Serialize};
8use zvariant::{OwnedValue, Type, Value};
9
10use crate::types::TrackId;
11use crate::{Error, Result};
12
13const FIELD_TRACK_ID: &str = "mpris:trackid";
19const FIELD_LENGTH: &str = "mpris:length";
21const FIELD_ART_URL: &str = "mpris:artUrl";
24
25const FIELD_ALBUM: &str = "xesam:album";
29const FIELD_ALBUM_ARTIST: &str = "xesam:albumArtist";
31const FIELD_ARTIST: &str = "xesam:artist";
33const FIELD_AS_TEXT: &str = "xesam:asText";
35const FIELD_AUDIO_BPM: &str = "xesam:audioBPM";
37const FIELD_AUTO_RATING: &str = "xesam:autoRating";
40const FIELD_COMMENT: &str = "xesam:comment";
42const FIELD_COMPOSER: &str = "xesam:composer";
44const FIELD_CONTENT_CREATED: &str = "xesam:contentCreated";
46const FIELD_DISC_NUMBER: &str = "xesam:discNumber";
48const FIELD_FIRST_USED: &str = "xesam:firstUsed";
50const FIELD_GENRE: &str = "xesam:genre";
52const FIELD_LAST_USED: &str = "xesam:lastUsed";
54const FIELD_LYRICIST: &str = "xesam:lyricist";
56const FIELD_TITLE: &str = "xesam:title";
58const FIELD_TRACK_NUMBER: &str = "xesam:trackNumber";
60const FIELD_URL: &str = "xesam:url";
62const FIELD_USE_COUNT: &str = "xesam:useCount";
64const FIELD_USER_RATING: &str = "xesam:userRating";
66
67#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
68#[serde(transparent)]
69pub struct Metadata {
70 inner: HashMap<String, OwnedValue>,
71}
72
73impl Metadata {
74 pub fn new(track_id: TrackId) -> Self {
75 let mut metadata = Self {
76 inner: HashMap::new(),
77 };
78 metadata.insert(FIELD_TRACK_ID.to_string(), track_id.into());
79 metadata
80 }
81
82 pub fn album(&self) -> Option<String> {
84 self.inner
85 .get(FIELD_ALBUM)
86 .cloned()
87 .and_then(|v| v.try_into().ok())
88 }
89
90 pub fn artists(&self) -> Option<Vec<String>> {
92 self.inner
93 .get(FIELD_ARTIST)
94 .cloned()
95 .and_then(|artists| artists.try_into().ok())
96 }
97
98 pub fn lyrics(&self) -> Option<String> {
100 self.inner
101 .get(FIELD_AS_TEXT)
102 .cloned()
103 .and_then(|v| v.try_into().ok())
104 }
105
106 pub fn album_artists(&self) -> Option<Vec<String>> {
108 self.inner
109 .get(FIELD_ALBUM_ARTIST)
110 .cloned()
111 .and_then(|artists| artists.try_into().ok())
112 }
113
114 pub fn bpm(&self) -> Option<u32> {
116 self.inner
117 .get(FIELD_AUDIO_BPM)
118 .cloned()
119 .and_then(|v| try_value_into_integer(v).ok())
120 }
121
122 pub fn auto_rating(&self) -> Option<f64> {
125 self.inner
126 .get(FIELD_AUTO_RATING)
127 .cloned()
128 .and_then(|v| v.try_into().ok())
129 }
130
131 pub fn comments(&self) -> Option<Vec<String>> {
133 self.inner
134 .get(FIELD_COMMENT)
135 .cloned()
136 .and_then(|v| v.try_into().ok())
137 }
138
139 pub fn composers(&self) -> Option<Vec<String>> {
141 self.inner
142 .get(FIELD_COMPOSER)
143 .cloned()
144 .and_then(|composers| composers.try_into().ok())
145 }
146
147 pub fn created(&self) -> Option<Timestamp> {
149 self.inner
150 .get(FIELD_CONTENT_CREATED)
151 .cloned()
152 .and_then(|v| try_value_into_date(v).ok())
153 }
154
155 pub fn disc_number(&self) -> Option<u32> {
157 self.inner
158 .get(FIELD_DISC_NUMBER)
159 .cloned()
160 .and_then(|v| try_value_into_integer(v).ok())
161 }
162
163 pub fn first_played(&self) -> Option<Timestamp> {
165 self.inner
166 .get(FIELD_FIRST_USED)
167 .cloned()
168 .and_then(|v| try_value_into_date(v).ok())
169 }
170
171 pub fn genres(&self) -> Option<Vec<String>> {
173 self.inner
174 .get(FIELD_GENRE)
175 .cloned()
176 .and_then(|genres| genres.try_into().ok())
177 }
178
179 pub fn last_played(&self) -> Option<Timestamp> {
181 self.inner
182 .get(FIELD_LAST_USED)
183 .cloned()
184 .and_then(|v| try_value_into_date(v).ok())
185 }
186
187 pub fn lyricists(&self) -> Option<Vec<String>> {
189 self.inner
190 .get(FIELD_LYRICIST)
191 .cloned()
192 .and_then(|lyricists| lyricists.try_into().ok())
193 }
194
195 pub fn title(&self) -> Option<String> {
197 self.inner
198 .get(FIELD_TITLE)
199 .cloned()
200 .and_then(|v| v.try_into().ok())
201 }
202
203 pub fn track_number(&self) -> Option<u32> {
205 self.inner
206 .get(FIELD_TRACK_NUMBER)
207 .cloned()
208 .and_then(|v| try_value_into_integer(v).ok())
209 }
210
211 pub fn url(&self) -> Option<String> {
213 self.inner
214 .get(FIELD_URL)
215 .cloned()
216 .and_then(|v| v.try_into().ok())
217 }
218
219 pub fn play_count(&self) -> Option<u32> {
221 self.inner
222 .get(FIELD_USE_COUNT)
223 .cloned()
224 .and_then(|v| try_value_into_integer(v).ok())
225 }
226
227 pub fn user_rating(&self) -> Option<f64> {
229 self.inner
230 .get(FIELD_USER_RATING)
231 .cloned()
232 .and_then(|v| v.try_into().ok())
233 }
234
235 pub fn track_id(&self) -> Option<TrackId> {
237 self.inner
238 .get(FIELD_TRACK_ID)
239 .cloned()
240 .and_then(|path| TrackId::try_from(path).ok())
241 }
242
243 pub fn length(&self) -> Option<SignedDuration> {
245 self.inner
246 .get(FIELD_LENGTH)
247 .cloned()
248 .and_then(|v| i64::try_from(v).ok())
249 .map(SignedDuration::from_micros)
250 }
251
252 pub fn art_url(&self) -> Option<String> {
255 self.inner
256 .get(FIELD_ART_URL)
257 .cloned()
258 .and_then(|v| v.try_into().ok())
259 }
260}
261
262impl Deref for Metadata {
263 type Target = HashMap<String, OwnedValue>;
264
265 fn deref(&self) -> &Self::Target {
266 &self.inner
267 }
268}
269
270impl DerefMut for Metadata {
271 fn deref_mut(&mut self) -> &mut Self::Target {
272 &mut self.inner
273 }
274}
275
276impl Display for Metadata {
277 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
278 write!(f, "{{")?;
279 let mut iter = self.inner.iter().peekable();
280 while let Some((k, v)) = iter.next() {
281 write!(f, "{k}: ")?;
282 write_zvalue(f, v)?;
283 if iter.peek().is_some() {
284 write!(f, ", ")?;
285 }
286 }
287 write!(f, "}}")
288 }
289}
290
291impl From<HashMap<String, OwnedValue>> for Metadata {
292 fn from(value: HashMap<String, OwnedValue>) -> Self {
293 Self { inner: value }
294 }
295}
296
297impl Type for Metadata {
300 const SIGNATURE: &'static zvariant::Signature = &zvariant::signature!("a{sv}");
301}
302
303impl From<Metadata> for Value<'_> {
304 fn from(value: Metadata) -> Self {
305 value.inner.into()
306 }
307}
308
309impl TryFrom<OwnedValue> for Metadata {
310 type Error = zvariant::Error;
311
312 fn try_from(value: OwnedValue) -> std::result::Result<Self, Self::Error> {
313 Ok(Self {
314 inner: value.clone().try_into()?,
315 })
316 }
317}
318
319pub fn try_value_into_integer(value: OwnedValue) -> Result<u32> {
321 match *value {
331 Value::U8(v) => Ok(v as u32),
332 Value::Bool(v) => Ok(v as u32),
333 Value::I16(v) => Ok(v as u32),
334 Value::U16(v) => Ok(v as u32),
335 Value::I32(v) => Ok(v as u32),
336 Value::U32(v) => Ok(v),
337 Value::I64(v) => Ok(v as u32),
338 Value::U64(v) => Ok(v as u32),
339 _ => Err(Error::IncorrectValue {
340 wanted: "Integer",
341 actual: value,
342 }),
343 }
344}
345
346pub fn try_value_into_date(value: OwnedValue) -> Result<Timestamp> {
347 let value: String = value
348 .clone()
349 .try_into()
350 .map_err(|_| Error::IncorrectValue {
351 wanted: "Str",
352 actual: value,
353 })?;
354 let value = value
355 .parse::<Timestamp>()
356 .map_err(Error::InvalidTimestamp)?;
357 Ok(value)
358}
359
360fn write_zvalue(f: &mut fmt::Formatter<'_>, value: &Value) -> fmt::Result {
361 match value {
362 Value::U8(b) => write!(f, "{b}"),
363 Value::Bool(b) => write!(f, "{b}"),
364 Value::I16(i) => write!(f, "{i}"),
365 Value::U16(u) => write!(f, "{u}"),
366 Value::I32(i) => write!(f, "{i}"),
367 Value::U32(u) => write!(f, "{u}"),
368 Value::I64(i) => write!(f, "{i}"),
369 Value::U64(u) => write!(f, "{u}"),
370 Value::F64(d) => write!(f, "{d}"),
371 Value::Str(s) => write!(f, "\"{s}\""),
372 Value::ObjectPath(p) => write!(f, "\"{p}\""),
373 Value::Array(a) => {
374 write!(f, "[")?;
375 let mut iter = a.iter().peekable();
376 while let Some(value) = iter.next() {
377 if iter.peek().is_some() {
378 write!(f, "{value}, ")?;
379 } else {
380 write!(f, "{value}")?;
381 }
382 }
383 write!(f, "]")
384 }
385 Value::Dict(d) => {
386 write!(f, "{{")?;
387 let mut iter = d.iter().peekable();
388 while let Some((k, v)) = iter.next() {
389 if iter.peek().is_some() {
390 write!(f, "{k}: {v}, ")?;
391 } else {
392 write!(f, "{k}: {v}")?;
393 }
394 }
395 write!(f, "}}")
396 }
397 Value::Value(value) => write_zvalue(f, value),
398 _ => write!(f, "{value:?}"),
399 }
400}
401
402#[cfg(test)]
403mod test {
404 use std::collections::HashMap;
405 use std::str::FromStr;
406
407 use jiff::SignedDuration;
408 use zvariant::serialized::Context;
409 use zvariant::{
410 Array, Dict, LE, ObjectPath, OwnedValue, Signature, Value, signature, to_bytes,
411 to_bytes_for_signature,
412 };
413
414 use super::*;
415
416 #[test]
417 fn success() {
418 let mut expected: Metadata = HashMap::new().into();
419 expected.insert(
420 FIELD_TRACK_ID.to_string(),
421 ObjectPath::try_from("/hii").unwrap().into(),
422 );
423
424 let mut v = Dict::new(&Signature::Str, &Signature::Variant);
425 v.add(
426 FIELD_TRACK_ID,
427 Value::ObjectPath("/hii".try_into().unwrap()),
428 )
429 .unwrap();
430 let v = Value::Dict(v);
431 let ov = OwnedValue::try_from(v.clone()).unwrap();
432 let metadata = Metadata::try_from(ov).unwrap();
433
434 assert_eq!(metadata, expected);
435 assert_eq!(v, Value::from(expected));
436 }
437
438 #[test]
439 fn wrong_dbus_type() {
440 let v = Value::U64(5);
441 let ov = OwnedValue::try_from(v).unwrap();
442 let err = Metadata::try_from(ov).unwrap_err();
443
444 assert_eq!(zvariant::Error::IncorrectType, err);
445 }
446
447 #[test]
448 fn new() {
449 let op = TrackId::try_from("/hii").unwrap();
450 let metadata = Metadata::new(op);
451 assert_eq!(
452 metadata.track_id(),
453 Some(TrackId::try_from("/hii").unwrap())
454 );
455 }
456
457 #[test]
458 fn get_track_id() {
459 let metadata = new_metadata(FIELD_TRACK_ID, ObjectPath::try_from("/hii").unwrap());
460 assert_eq!(
461 metadata.track_id(),
462 Some(TrackId::try_from("/hii").unwrap())
463 );
464 }
465
466 #[test]
467 fn get_length() {
468 let metadata = new_metadata(FIELD_LENGTH, Value::I64(42 * 1000 * 1000));
469 assert_eq!(metadata.length(), Some(SignedDuration::from_secs(42)));
470 }
471
472 #[test]
473 fn get_art_url() {
474 let metadata = new_metadata(FIELD_ART_URL, Value::Str("https://my/cool/art".into()));
475 assert_eq!(metadata.art_url(), Some("https://my/cool/art".to_string()));
476 }
477
478 #[test]
479 fn get_album() {
480 let metadata = new_metadata(FIELD_ALBUM, Value::Str("The Album".into()));
481 assert_eq!(metadata.album(), Some("The Album".to_string()));
482 }
483
484 #[test]
485 fn get_album_artist() {
486 let mut arr = Array::new(&Signature::Str);
487 arr.append(Value::Str("Foo Artist".into())).unwrap();
488 arr.append(Value::Str("Bar Artist".into())).unwrap();
489 let metadata = new_metadata(FIELD_ALBUM_ARTIST, arr);
490 assert_eq!(
491 metadata.album_artists(),
492 Some(vec!["Foo Artist".to_string(), "Bar Artist".to_string()])
493 );
494 }
495
496 #[test]
497 fn get_artist() {
498 let mut arr = Array::new(&Signature::Str);
499 arr.append(Value::Str("Foo Artist".into())).unwrap();
500 arr.append(Value::Str("Bar Artist".into())).unwrap();
501 let metadata = new_metadata(FIELD_ARTIST, arr);
502 assert_eq!(
503 metadata.artists(),
504 Some(vec!["Foo Artist".to_string(), "Bar Artist".to_string()])
505 );
506 }
507
508 #[test]
509 fn get_lyrics() {
510 let metadata = new_metadata(
511 FIELD_AS_TEXT,
512 Value::Str("If I can live through this, I can do anything".into()),
513 );
514 assert_eq!(
515 metadata.lyrics(),
516 Some("If I can live through this, I can do anything".to_string())
517 );
518 }
519
520 #[test]
521 fn get_bpm() {
522 let metadata = new_metadata(FIELD_AUDIO_BPM, Value::I32(120));
523 assert_eq!(metadata.bpm(), Some(120));
524 }
525
526 #[test]
527 fn get_auto_rating() {
528 let metadata = new_metadata(FIELD_AUTO_RATING, Value::F64(0.7));
529 assert_eq!(metadata.auto_rating(), Some(0.7));
530 }
531
532 #[test]
533 fn get_comments() {
534 let mut arr = Array::new(&Signature::Str);
535 arr.append(Value::Str("Some comment".into())).unwrap();
536 arr.append(Value::Str("Another comment".into())).unwrap();
537 let metadata = new_metadata(FIELD_COMMENT, arr);
538 assert_eq!(
539 metadata.comments(),
540 Some(vec![
541 "Some comment".to_string(),
542 "Another comment".to_string()
543 ])
544 );
545 }
546
547 #[test]
548 fn get_composers() {
549 let mut arr = Array::new(&Signature::Str);
550 arr.append(Value::Str("Chopin".into())).unwrap();
551 arr.append(Value::Str("Franchomme".into())).unwrap();
552 let metadata = new_metadata(FIELD_COMPOSER, arr);
553 assert_eq!(
554 metadata.composers(),
555 Some(vec!["Chopin".to_string(), "Franchomme".to_string()])
556 );
557 }
558
559 #[test]
560 fn get_created() {
561 let metadata = new_metadata(
562 FIELD_CONTENT_CREATED,
563 Value::Str("2007-04-29T13:56+01:00".into()),
564 );
565 assert_eq!(
566 metadata.created(),
567 Some(Timestamp::from_str("2007-04-29T13:56+01:00").unwrap())
568 );
569 }
570
571 #[test]
572 fn get_disc_number() {
573 let metadata = new_metadata(FIELD_DISC_NUMBER, Value::I32(2));
574 assert_eq!(metadata.disc_number(), Some(2));
575 }
576
577 #[test]
578 fn get_first_played() {
579 let metadata = new_metadata(
580 FIELD_FIRST_USED,
581 Value::Str("2007-04-29T13:56+01:00".into()),
582 );
583 assert_eq!(
584 metadata.first_played(),
585 Some(Timestamp::from_str("2007-04-29T13:56+01:00").unwrap())
586 );
587 }
588
589 #[test]
590 fn get_genres() {
591 let mut arr = Array::new(&Signature::Str);
592 arr.append(Value::Str("Pop".into())).unwrap();
593 arr.append(Value::Str("Rock".into())).unwrap();
594 let metadata = new_metadata(FIELD_GENRE, arr);
595 assert_eq!(
596 metadata.genres(),
597 Some(vec!["Pop".to_string(), "Rock".to_string()])
598 );
599 }
600
601 #[test]
602 fn get_last_played() {
603 let metadata = new_metadata(FIELD_LAST_USED, Value::Str("2007-04-29T13:56+01:00".into()));
604 assert_eq!(
605 metadata.last_played(),
606 Some(Timestamp::from_str("2007-04-29T13:56+01:00").unwrap())
607 );
608 }
609
610 #[test]
611 fn get_lyricists() {
612 let mut arr = Array::new(&Signature::Str);
613 arr.append(Value::Str("Frog in a Car".into())).unwrap();
614 arr.append(Value::Str("Potato Chip".into())).unwrap();
615 let metadata = new_metadata(FIELD_LYRICIST, arr);
616 assert_eq!(
617 metadata.lyricists(),
618 Some(vec!["Frog in a Car".to_string(), "Potato Chip".to_string()])
619 );
620 }
621
622 #[test]
623 fn get_title() {
624 let metadata = new_metadata(FIELD_TITLE, Value::Str("The Grid".into()));
625 assert_eq!(metadata.title(), Some("The Grid".to_string()));
626 }
627
628 #[test]
629 fn get_track_number() {
630 let metadata = new_metadata(FIELD_TRACK_NUMBER, Value::I32(7));
631 assert_eq!(metadata.track_number(), Some(7));
632 }
633
634 #[test]
635 fn get_url() {
636 let metadata = new_metadata(FIELD_URL, Value::Str("https://the/best/song/ever".into()));
637 assert_eq!(
638 metadata.url(),
639 Some("https://the/best/song/ever".to_string())
640 );
641 }
642
643 #[test]
644 fn get_play_count() {
645 let metadata = new_metadata(FIELD_USE_COUNT, Value::I32(123));
646 assert_eq!(metadata.play_count(), Some(123));
647 }
648
649 #[test]
650 fn get_user_rating() {
651 let metadata = new_metadata(FIELD_USER_RATING, Value::F64(0.9));
652 assert_eq!(metadata.user_rating(), Some(0.9));
653 }
654
655 #[test]
656 fn serialize() {
657 let ctxt = Context::new_dbus(LE, 0);
658 let encoded = to_bytes(ctxt, &new_metadata(FIELD_TRACK_NUMBER, 7)).unwrap();
659 let decoded: HashMap<String, OwnedValue> = encoded.deserialize().unwrap().0;
660
661 assert_eq!(
662 decoded.get(FIELD_TRACK_NUMBER).unwrap().deref(),
663 &Value::from(7)
664 );
665 }
666
667 #[test]
668 fn deserialize() {
669 let mut hashmap: HashMap<String, OwnedValue> = HashMap::new();
670 hashmap.insert(FIELD_TRACK_NUMBER.to_string(), OwnedValue::from(7i32));
671
672 let ctxt = Context::new_dbus(LE, 0);
673 let encoded = to_bytes_for_signature(ctxt, signature!("a{sv}"), &hashmap).unwrap();
674 let decoded: Metadata = encoded.deserialize().unwrap().0;
675
676 assert_eq!(
677 decoded.get(FIELD_TRACK_NUMBER).unwrap().deref(),
678 &Value::from(7)
679 );
680 }
681
682 fn new_metadata<'a, K, V>(key: K, value: V) -> Metadata
683 where
684 K: Into<String>,
685 V: Into<Value<'a>>,
686 {
687 let mut metadata: Metadata = HashMap::new().into();
688 metadata.insert(key.into(), value.into().try_into().unwrap());
689 metadata
690 }
691}