// SPDX-License-Identifier: MPL-2.0 use crate::error::{Error, Result}; use std::{ collections::HashMap, fmt, ops::{Deref, DerefMut}, }; use time::{Duration, OffsetDateTime}; use zbus::zvariant::{OwnedObjectPath, Value as ZValue}; #[derive(Debug, Clone, PartialEq)] pub struct Metadata { inner: HashMap, } impl Metadata { /// `xesam:album`: The track artist(s). pub fn album(&self) -> Option { self.inner .get("xesam:album") .cloned() .and_then(|v| v.try_into_string().ok()) } /// `xesam:artist`: The track artist(s). pub fn artists(&self) -> Option> { self.inner .get("xesam:artist") .cloned() .and_then(|artists| artists.try_into_array().ok()) .map(|artists| { artists .into_iter() .filter_map(|v| v.try_into_string().ok()) .collect() }) } /// `xesam:asText`: The track lyrics. pub fn lyrics(&self) -> Option { self.inner .get("xesam:asText") .cloned() .and_then(|v| v.try_into_string().ok()) } /// `xesam:albumArtist`: The album artist(s). pub fn album_artists(&self) -> Option> { self.inner .get("xesam:albumArtist") .cloned() .and_then(|artists| artists.try_into_array().ok()) .map(|artists| { artists .into_iter() .filter_map(|v| v.try_into_string().ok()) .collect() }) } /// `xesam:audioBPM`: The speed of the music, in beats per minute. pub fn bpm(&self) -> Option { self.inner .get("xesam:audioBPM") .cloned() .and_then(|v| v.try_into_uint().ok()) } /// `xesam:autoRating`: An automatically-generated rating, based on things such as how often it has been played. /// This should be in the range 0.0 to 1.0. pub fn auto_rating(&self) -> Option { self.inner .get("xesam:autoRating") .cloned() .and_then(|v| v.try_into_double().ok()) } /// `xesam:composer`: The composer(s) of the track. pub fn composer(&self) -> Option> { self.inner .get("xesam:composer") .cloned() .and_then(|artists| artists.try_into_array().ok()) .map(|artists| { artists .into_iter() .filter_map(|v| v.try_into_string().ok()) .collect() }) } /// `xesam:contentCreated`: When the track was created. Usually only the year component will be useful. pub fn created(&self) -> Option { self.inner .get("xesam:contentCreated") .cloned() .and_then(|v| v.try_into_date().ok()) } /// `xesam:discNumber`: The disc number on the album that this track is from. pub fn disc_number(&self) -> Option { self.inner .get("xesam:discNumber") .cloned() .and_then(|v| v.try_into_uint().ok()) } /// `xesam:firstUsed`: When the track was first played. pub fn first_played(&self) -> Option { self.inner .get("xesam:firstUsed") .cloned() .and_then(|v| v.try_into_date().ok()) } /// `xesam:genre`: The genre(s) of the track. pub fn genre(&self) -> Option> { self.inner .get("xesam:genre") .cloned() .and_then(|artists| artists.try_into_array().ok()) .map(|artists| { artists .into_iter() .filter_map(|v| v.try_into_string().ok()) .collect() }) } /// `xesam:lastUsed`: When the track was last played. pub fn last_played(&self) -> Option { self.inner .get("xesam:lastUsed") .cloned() .and_then(|v| v.try_into_date().ok()) } /// `xesam:lyricist`: The lyricist(s) of the track. pub fn lyricist(&self) -> Option> { self.inner .get("xesam:lyricist") .cloned() .and_then(|artists| artists.try_into_array().ok()) .map(|artists| { artists .into_iter() .filter_map(|v| v.try_into_string().ok()) .collect() }) } /// `xesam:title`: The track title. pub fn title(&self) -> Option { self.inner .get("xesam:title") .cloned() .and_then(|v| v.try_into_string().ok()) } /// `xesam:trackNumber`: The track number on the album that this track is from. pub fn track_number(&self) -> Option { self.inner .get("xesam:trackNumber") .cloned() .and_then(|v| v.try_into_uint().ok()) } /// `xesam:url`: The location of the media file. pub fn url(&self) -> Option { self.inner .get("xesam:url") .cloned() .and_then(|v| v.try_into_string().ok()) } /// `xesam:useCount`: The number of times the track has been played. pub fn use_count(&self) -> Option { self.inner .get("xesam:useCount") .cloned() .and_then(|v| v.try_into_uint().ok()) } /// `xesam:userRating`: The user's rating of the track. pub fn user_rating(&self) -> Option { self.inner .get("xesam:userRating") .cloned() .and_then(|v| v.try_into_double().ok()) } /// `mpris:trackid`: D-Bus path: A unique identity for this track within the context of an MPRIS object (eg: tracklist). pub fn track_id(&self) -> Option { self.inner .get("mpris:trackid") .cloned() .and_then(|v| v.try_into_string().ok()) .and_then(|path| OwnedObjectPath::try_from(path).ok()) } /// `mpris:length`: The length of the track in microseconds. pub fn length(&self) -> Option { self.inner .get("mpris:length") .cloned() .and_then(|v| match &v { MetadataValue::Int(i) => Some(*i), MetadataValue::UInt(u) => Some(*u as i64), MetadataValue::Str(s) => s.parse().ok(), _ => None, }) .map(Duration::microseconds) } /// `mpris:artUrl`: The location of an image representing the track or album. /// Clients should not assume this will continue to exist when the media player stops giving out the URL. pub fn art_url(&self) -> Option { self.inner .get("mpris:artUrl") .cloned() .and_then(|v| v.try_into_string().ok()) } } impl Deref for Metadata { type Target = HashMap; fn deref(&self) -> &Self::Target { &self.inner } } impl DerefMut for Metadata { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.inner } } impl fmt::Display for Metadata { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{{")?; let mut iter = self.inner.iter().peekable(); while let Some((k, v)) = iter.next() { if iter.peek().is_some() { write!(f, "{}: {}, ", k, v)?; } else { write!(f, "{}: {}", k, v)?; } } write!(f, "}}") } } impl<'a, V: Into>> From> for Metadata { fn from(map: HashMap) -> Self { Self { inner: map .into_iter() .map(|(k, v)| (k, MetadataValue::from(&v.into()))) .collect(), } } } #[derive(Clone, PartialEq)] pub enum MetadataValue { Str(String), Double(f64), Int(i64), UInt(u64), Bool(bool), Array(Vec), Dict(HashMap), __Unsupported, } impl MetadataValue { fn variant(&self) -> &'static str { match self { MetadataValue::Str(_) => "Str", MetadataValue::Double(_) => "Double", MetadataValue::Int(_) => "Int", MetadataValue::UInt(_) => "UInt", MetadataValue::Bool(_) => "Bool", MetadataValue::Array(_) => "Array", MetadataValue::Dict(_) => "Dict", MetadataValue::__Unsupported => "Unsupported", } } /// Tries to extract a string from the variant, /// returning an error if the variant is not a string. pub fn try_into_string(self) -> Result { match self { MetadataValue::Str(s) => Ok(s), _ => Err(Error::IncorrectVariant { wanted: "Str", actual: self.variant(), }), } } /// Tries to extract a string from the variant, /// panicking if the variant is not a string. pub fn into_string(self) -> String { self.try_into_string() .unwrap_or_else(|err| panic!("{}", err)) } /// Tries to extract a date/time from the variant, /// returning an error if the variant is not a date/time. pub fn try_into_date(self) -> Result { let variant = self.variant(); match self { MetadataValue::Str(s) => { OffsetDateTime::parse(&s, &time::format_description::well_known::Rfc3339).map_err( |_| Error::IncorrectVariant { wanted: "String (DateTime)", actual: variant, }, ) } _ => Err(Error::IncorrectVariant { wanted: "String (DateTime)", actual: variant, }), } } /// Tries to extract a date/time from the variant, /// panicking if the variant is not a date/time. pub fn into_date(self) -> OffsetDateTime { self.try_into_date().unwrap_or_else(|err| panic!("{}", err)) } /// Tries to extract a double from the variant, /// returning an error if the variant is not a double. pub fn try_into_double(self) -> Result { match self { MetadataValue::Double(d) => Ok(d), _ => Err(Error::IncorrectVariant { wanted: "Double", actual: self.variant(), }), } } /// Tries to extract a double from the variant, /// panicking if the variant is not a double. pub fn into_double(self) -> f64 { self.try_into_double() .unwrap_or_else(|err| panic!("{}", err)) } /// Tries to extract an integer from the variant, /// returning an error if the variant is not an integer. pub fn try_into_int(self) -> Result { match self { MetadataValue::Int(i) => Ok(i), _ => Err(Error::IncorrectVariant { wanted: "Int", actual: self.variant(), }), } } /// Tries to extract an integer from the variant, /// panicking if the variant is not an integer. pub fn into_int(self) -> i64 { self.try_into_int().unwrap_or_else(|err| panic!("{}", err)) } /// Tries to extract an unsigned integer from the variant, /// returning an error if the variant is not an unsigned integer. pub fn try_into_uint(self) -> Result { match self { MetadataValue::UInt(u) => Ok(u), _ => Err(Error::IncorrectVariant { wanted: "UInt", actual: self.variant(), }), } } /// Tries to extract an unsigned integer from the variant, /// panicking if the variant is not an unsigned integer. pub fn into_uint(self) -> u64 { self.try_into_uint().unwrap_or_else(|err| panic!("{}", err)) } /// Tries to extract a boolean from the variant, /// returning an error if the variant is not a boolean. pub fn try_into_bool(self) -> Result { match self { MetadataValue::Bool(b) => Ok(b), _ => Err(Error::IncorrectVariant { wanted: "Bool", actual: self.variant(), }), } } /// Tries to extract a boolean from the variant, /// panicking if the variant is not a boolean. pub fn into_bool(self) -> bool { self.try_into_bool().unwrap_or_else(|err| panic!("{}", err)) } /// Tries to extract an array from the variant, /// returning an error if the variant is not an array. pub fn try_into_array(self) -> Result> { match self { MetadataValue::Array(a) => Ok(a), _ => Err(Error::IncorrectVariant { wanted: "Array", actual: self.variant(), }), } } /// Tries to extract an array from the variant, /// panicking if the variant is not an array. pub fn into_array(self) -> Vec { self.try_into_array() .unwrap_or_else(|err| panic!("{}", err)) } /// Tries to extract a dictionary from the variant, /// returning an error if the variant is not a dictionary. /// The dictionary is returned as a map from string keys to values. pub fn try_into_dict(self) -> Result> { match self { MetadataValue::Dict(d) => Ok(d), _ => Err(Error::IncorrectVariant { wanted: "Dict", actual: self.variant(), }), } } /// Tries to extract a dictionary from the variant, /// panicking if the variant is not a dictionary. /// The dictionary is returned as a map from string keys to values. pub fn into_dict(self) -> HashMap { self.try_into_dict().unwrap_or_else(|err| panic!("{}", err)) } } impl<'a> From<&ZValue<'a>> for MetadataValue { fn from(value: &ZValue) -> Self { match value { ZValue::U8(u) => Self::UInt(*u as u64), ZValue::Bool(b) => Self::Bool(*b), ZValue::I16(i) => Self::Int(*i as i64), ZValue::U16(u) => Self::UInt(*u as u64), ZValue::I32(i) => Self::Int(*i as i64), ZValue::U32(u) => Self::UInt(*u as u64), ZValue::I64(i) => Self::Int(*i), ZValue::U64(u) => Self::UInt(*u), ZValue::F64(f) => Self::Double(*f), ZValue::Str(s) => Self::Str(s.to_string()), ZValue::ObjectPath(path) => Self::Str(path.to_string()), ZValue::Array(a) => Self::Array(a.iter().map(|v| v.into()).collect()), ZValue::Dict(d) => Self::Dict( HashMap::::try_from(d.to_owned()) .unwrap() .into_iter() .map(|(k, v)| (k, (&v).into())) .collect(), ), ZValue::Value(value) => Self::from(&**value), _ => Self::__Unsupported, } } } impl fmt::Debug for MetadataValue { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::__Unsupported => write!(f, "__Unsupported"), Self::Int(i) => write!(f, "{}", i), Self::UInt(u) => write!(f, "{}", u), Self::Double(d) => write!(f, "{}", d), Self::Str(s) => write!(f, "{}", s), Self::Bool(b) => write!(f, "{}", b), Self::Array(a) => write!(f, "{:?}", a), Self::Dict(d) => { let mut debug_struct = f.debug_struct("Dict"); for (k, v) in d { debug_struct.field(k, &v); } debug_struct.finish() } } } } impl fmt::Display for MetadataValue { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::__Unsupported => write!(f, "__Unsupported"), Self::Int(i) => write!(f, "{}", i), Self::UInt(u) => write!(f, "{}", u), Self::Double(d) => write!(f, "{}", d), Self::Str(s) => write!(f, "\"{}\"", s), Self::Bool(b) => write!(f, "{}", b), Self::Array(a) => { write!(f, "[")?; let mut iter = a.iter().peekable(); while let Some(value) = iter.next() { if iter.peek().is_some() { write!(f, "{}, ", value)?; } else { write!(f, "{}", value)?; } } write!(f, "]") } Self::Dict(d) => { write!(f, "{{")?; let mut iter = d.iter().peekable(); while let Some((k, v)) = iter.next() { if iter.peek().is_some() { write!(f, "{}: {}, ", k, v)?; } else { write!(f, "{}: {}", k, v)?; } } write!(f, "}}") } } } }