Skip to main content

rustfm_scrobble_proxy/
models.rs

1macro_rules! impl_collection {
2    (
3        $(#[$meta:meta])+
4        struct $name:ident($item:ty);
5    ) => {
6        $(#[$meta])+
7        pub struct $name(::std::vec::Vec<$item>);
8
9        impl $name {
10            #[doc = concat!("Creates a new, empty ", stringify!($name))]
11            pub fn new() -> Self {
12                Self(::std::vec::Vec::new())
13            }
14
15            #[doc = concat!("Returns true if the ", stringify!($name), " contains no ", stringify!($item), "s")]
16            pub fn is_empty(&self) -> bool {
17                self.0.is_empty()
18            }
19
20            #[doc = concat!("Returns the number of ", stringify!($item), "s in the ", stringify!($name))]
21            pub fn len(&self) -> usize {
22                self.0.len()
23            }
24
25            #[doc = concat!("Returns an iterator over the ", stringify!($name))]
26            pub fn iter<'a>(&'a self) -> ::std::slice::Iter<'a, $item> {
27                self.into_iter()
28            }
29        }
30
31        impl ::std::iter::FromIterator<$item> for $name {
32            fn from_iter<I: ::std::iter::IntoIterator<Item=$item>>(iter: I) -> Self {
33                Self(::std::vec::Vec::from_iter(iter))
34            }
35        }
36
37        impl ::std::convert::From<Vec<$item>> for $name {
38            fn from(value: ::std::vec::Vec<$item>) -> Self {
39                Self(value)
40            }
41        }
42
43        impl ::std::iter::IntoIterator for $name {
44            type Item = $item;
45            type IntoIter = ::std::vec::IntoIter<$item>;
46
47            fn into_iter(self) -> Self::IntoIter {
48                self.0.into_iter()
49            }
50        }
51
52        impl<'a> ::std::iter::IntoIterator for &'a $name {
53            type Item = &'a $item;
54            type IntoIter = ::std::slice::Iter<'a, $item>;
55
56            fn into_iter(self) -> Self::IntoIter {
57                self.0.iter()
58            }
59        }
60
61        impl ::std::iter::Extend<$item> for $name {
62            fn extend<I: ::std::iter::IntoIterator<Item=$item>>(&mut self, iter: I) {
63                self.0.extend(iter)
64            }
65        }
66    };
67}
68
69pub mod responses {
70
71    use std::fmt;
72
73    use serde::Deserialize;
74    use serde_json as json;
75
76    #[derive(Deserialize, Debug)]
77    pub struct AuthResponse {
78        pub session: SessionResponse,
79    }
80
81    /// Response to an Authentication request.
82    ///
83    /// Contains a Session Key and the username of the authenticated Last.fm user and a subscriber ID.
84    /// Only the Session Key is used internally by the crate; the other values are exposed as they may have some value
85    /// for clients.
86    ///
87    /// [Authentication API Requests Documentation](https://www.last.fm/api/authspec)
88    #[derive(Deserialize, Debug, Clone)]
89    pub struct SessionResponse {
90        pub key: String,
91        pub subscriber: i64,
92        pub name: String,
93    }
94
95    #[derive(Deserialize)]
96    pub struct NowPlayingResponseWrapper {
97        pub nowplaying: NowPlayingResponse,
98    }
99
100    /// Response to a Now Playing request.
101    ///
102    /// Represents a response to a Now Playing API request. This type can often be ignored by clients. All of the
103    /// fields are [`CorrectableString`] types, which can be used to see if Last.fm applied any metadata correction
104    /// to your artist, song or album.
105    ///
106    /// [Now Playing Request API Documentation](https://www.last.fm/api/show/track.updateNowPlaying)
107    #[derive(Deserialize, Debug)]
108    pub struct NowPlayingResponse {
109        pub artist: CorrectableString,
110        pub album: CorrectableString,
111        #[serde(rename = "albumArtist")]
112        pub album_artist: CorrectableString,
113        pub track: CorrectableString,
114    }
115
116    #[derive(Deserialize)]
117    pub struct ScrobbleResponseWrapper {
118        pub scrobbles: SingleScrobble,
119    }
120
121    #[derive(Deserialize)]
122    pub struct SingleScrobble {
123        pub scrobble: ScrobbleResponse,
124    }
125
126    /// Response to a Scrobble request
127    ///
128    /// Represents a response to a Scrobble API request. Contains the results of the Scrobble call, including any
129    /// metadata corrections the Last.fm API made to the arist/track/album submitted.
130    ///
131    /// [Scrobble Request API Documentation](https://www.last.fm/api/show/track.scrobble)
132    #[derive(Deserialize, Debug)]
133    pub struct ScrobbleResponse {
134        pub artist: CorrectableString,
135        pub album: CorrectableString,
136        #[serde(rename = "albumArtist")]
137        pub album_artist: CorrectableString,
138        pub track: CorrectableString,
139        pub timestamp: String,
140    }
141
142    impl_collection! {
143        #[derive(Debug, Deserialize)]
144        struct ScrobbleList(ScrobbleResponse);
145    }
146
147    /// Response to a Batch Scrobble request
148    ///
149    /// Represents a response to a batched Scrobble request. Contains the results of the Scrobble call, including
150    /// any metadata corrections the Last.fm API made to the arist/track/album submitted.
151    ///
152    /// [Scrobble Request API Documentation](https://www.last.fm/api/show/track.scrobble)
153    #[derive(Debug)]
154    pub struct BatchScrobbleResponse {
155        pub scrobbles: ScrobbleList,
156    }
157
158    #[derive(Deserialize, Debug)]
159    pub struct BatchScrobbleResponseWrapper {
160        pub scrobbles: BatchScrobbles,
161    }
162
163    #[derive(Deserialize, Debug)]
164    pub struct BatchScrobbles {
165        #[serde(deserialize_with = "BatchScrobbles::deserialize_response_scrobbles")]
166        #[serde(rename = "scrobble")]
167        pub scrobbles: ScrobbleList,
168    }
169
170    impl BatchScrobbles {
171        fn deserialize_response_scrobbles<'de, D>(de: D) -> Result<ScrobbleList, D::Error>
172        where
173            D: serde::Deserializer<'de>,
174        {
175            let deser_result: json::Value = serde::Deserialize::deserialize(de)?;
176            let scrobbles = match deser_result {
177                obj @ json::Value::Object(_) => {
178                    let scrobble: ScrobbleResponse =
179                        serde_json::from_value(obj).expect("Parsing scrobble failed");
180                    ScrobbleList::from(vec![scrobble])
181                }
182                arr @ json::Value::Array(_) => {
183                    let scrobbles: ScrobbleList =
184                        serde_json::from_value(arr).expect("Parsing scrobble list failed");
185                    scrobbles
186                }
187                _ => ScrobbleList::from(vec![]),
188            };
189            Ok(scrobbles)
190        }
191    }
192
193    /// Represents a string that can be marked as 'corrected' by the Last.fm API.
194    ///
195    /// All Scrobble/NowPlaying responses have their fields as `CorrectableString`'s. The API will sometimes change
196    /// the artist/song name/album name data that you have submitted. For example - it is common for Bjork to be turned
197    /// into Björk by the API; the modified artist field would be marked `corrected = true`, `text = "Björk".
198    ///
199    /// Most clients can ignore these corrections, but the information is exposed for clients that require it.
200    ///
201    /// [Meta-Data Correction Documentation](https://www.last.fm/api/scrobbling#meta-data-corrections)
202    #[derive(Deserialize, Debug)]
203    pub struct CorrectableString {
204        #[serde(deserialize_with = "CorrectableString::deserialize_corrected_field")]
205        pub corrected: bool,
206        #[serde(rename = "#text", default)]
207        pub text: String,
208    }
209
210    impl CorrectableString {
211        fn deserialize_corrected_field<'de, D>(de: D) -> Result<bool, D::Error>
212        where
213            D: serde::Deserializer<'de>,
214        {
215            let deser_result: json::Value = serde::Deserialize::deserialize(de)?;
216            match deser_result {
217                json::Value::String(ref s) if &*s == "1" => Ok(true),
218                json::Value::String(ref s) if &*s == "0" => Ok(false),
219                _ => Err(serde::de::Error::custom("Unexpected value")),
220            }
221        }
222    }
223
224    impl fmt::Display for CorrectableString {
225        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
226            write!(f, "{}", self.text)
227        }
228    }
229}
230
231pub mod metadata {
232
233    use std::collections::HashMap;
234
235    /// Repesents a single music track played at a point in time. In the Last.fm universe, this is known as a
236    /// "scrobble".
237    ///
238    /// Takes an artist, track and optional album name. Can hold a timestamp indicating when the track was listened to.
239    /// `Scrobble` objects are submitted via [`Scrobbler::now_playing`], [`Scrobbler::scrobble`] and batches of
240    /// Scrobbles are sent via [`Scrobbler::scrobble_batch`].
241    ///
242    /// [`Scrobbler::now_playing`]: struct.Scrobbler.html#method.now_playing
243    /// [`Scrobbler::scrobble`]: struct.Scrobbler.html#method.scrobble
244    /// [`Scrobbler::scrobble_batch`]: struct.Scrobbler.html#method.scrobble_batch
245    #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
246    pub struct Scrobble {
247        artist: String,
248        track: String,
249        album: Option<String>,
250
251        timestamp: Option<u64>,
252    }
253
254    impl_collection! {
255        /// A batch of Scrobbles to be submitted to Last.fm together.
256        #[derive(Clone, Debug)]
257        struct ScrobbleBatch(Scrobble);
258    }
259
260    impl Scrobble {
261        /// Constructs a new Scrobble instance, representing a single playthrough of a music track. `Scrobble`s are
262        /// submitted to Last.fm via an instance of [`Scrobbler`]. A new `Scrobble` requires an artist name and song/track
263        /// name, and an optional album name.
264        ///
265        /// # Example
266        /// ```ignore
267        /// let scrobble = Scrobble::new("Example Artist", "Example Track", Some("Example Album"));
268        /// ```
269        ///
270        /// [`Scrobbler`]: struct.Scrobbler.html
271        pub fn new(artist: &str, track: &str, album: Option<&str>) -> Self {
272            Self {
273                artist: artist.to_owned(),
274                track: track.to_owned(),
275                album: album.map(ToOwned::to_owned),
276                timestamp: None,
277            }
278        }
279
280        /// Sets the timestamp (date/time of play) of a Scrobble. Used in a builder-style pattern, typically after
281        /// [`Scrobble::new`].
282        ///
283        /// # Example
284        /// ```ignore
285        /// let mut scrobble = Scrobble::new(...).with_timestamp(12345);
286        /// ```
287        ///
288        /// # Note on Timestamps
289        /// Scrobbles without timestamps are automatically assigned a timestamp of the current time when
290        /// submitted via [`Scrobbler::scrobble`] or [`Scrobbler::scrobble_batch`]. Timestamps only need to be
291        /// explicitly set when you are submitting a Scrobble at a point in the past, or in the future.
292        ///
293        /// [`Scrobble::new`]: struct.Scrobble.html#method.new
294        /// [`Scrobbler::scrobble`]: struct.Scrobbler.html#method.scrobble
295        /// [`Scrobbler::scrobble_batch`]: struct.Scrobbler.html#method.scrobble_batch
296        pub fn with_timestamp(&mut self, timestamp: u64) -> &mut Self {
297            self.timestamp = Some(timestamp);
298            self
299        }
300
301        /// Converts the Scrobble metadata (track name, artist & album name) into a `HashMap`. Map keys are
302        /// `"track"` and `"artist"`. If the album is set, it will be present in the map under key `"album"`.
303        /// If a timestamp is set, it will be present in the map under key `"timestamp"`.
304        ///
305        /// # Example
306        /// ```ignore
307        /// let scrobble = Scrobble::new("Example Artist", ...);
308        /// let scrobble_map = scrobble.as_map();
309        /// assert_eq!(scrobble_map.get("artist"), "Example Artist");
310        /// ```
311        pub fn as_map(&self) -> HashMap<String, String> {
312            let mut params = HashMap::new();
313            params.insert("track".to_string(), self.track.clone());
314            params.insert("artist".to_string(), self.artist.clone());
315
316            if let Some(ref album) = self.album {
317                params.insert("album".to_string(), album.clone());
318            }
319
320            if let Some(timestamp) = self.timestamp {
321                params.insert("timestamp".to_string(), timestamp.to_string());
322            }
323
324            params
325        }
326
327        /// Returns the `Scrobble`'s artist name
328        pub fn artist(&self) -> &str {
329            &self.artist
330        }
331
332        /// Returns the `Scrobble`'s track name
333        pub fn track(&self) -> &str {
334            &self.track
335        }
336
337        /// Returns the `Scrobble`'s album name
338        pub fn album(&self) -> Option<&str> {
339            self.album.as_deref()
340        }
341    }
342
343    /// Converts from tuple of `&str`s in the form `(artist, track, album)`
344    ///
345    /// Designed to make it easier to cooperate with other track info types.
346    impl From<&(&str, &str, &str)> for Scrobble {
347        fn from((artist, track, album): &(&str, &str, &str)) -> Self {
348            Scrobble::new(artist, track, Some(album))
349        }
350    }
351
352    /// Converts from tuple of `String`s in the form `(artist, track, album)`
353    ///
354    /// Designed to make it easier to cooperate with other track info types.
355    impl From<&(String, String, String)> for Scrobble {
356        fn from((artist, track, album): &(String, String, String)) -> Self {
357            Scrobble::new(artist, track, Some(album))
358        }
359    }
360
361    /// Converts from vector of `&str` tuples, in the form `(artist, track, album)`.
362    ///
363    /// Designed to make it easier to cooperate with other track info types.
364    impl From<Vec<(&str, &str, &str)>> for ScrobbleBatch {
365        fn from(collection: Vec<(&str, &str, &str)>) -> Self {
366            let scrobbles: Vec<Scrobble> = collection.iter().map(Scrobble::from).collect();
367
368            ScrobbleBatch::from(scrobbles)
369        }
370    }
371
372    /// Converts from vector of `String` tuples, in the form `(artist, track, album)`.
373    ///
374    /// Designed to make it easier to cooperate with other track info types.
375    impl From<Vec<(String, String, String)>> for ScrobbleBatch {
376        fn from(collection: Vec<(String, String, String)>) -> Self {
377            let scrobbles: Vec<Scrobble> = collection.iter().map(Scrobble::from).collect();
378
379            ScrobbleBatch::from(scrobbles)
380        }
381    }
382
383    #[cfg(test)]
384    mod tests {
385        use super::*;
386
387        #[test]
388        fn make_scrobble() {
389            let mut scrobble = Scrobble::new(
390                "foo floyd and the fruit flies",
391                "old bananas",
392                Some("old bananas"),
393            );
394            scrobble.with_timestamp(1337);
395            assert_eq!(scrobble.artist(), "foo floyd and the fruit flies");
396            assert_eq!(scrobble.track(), "old bananas");
397            assert_eq!(scrobble.album(), Some("old bananas"));
398            assert_eq!(scrobble.timestamp, Some(1337));
399        }
400
401        #[test]
402        fn make_scrobble_check_map() {
403            let scrobble = Scrobble::new(
404                "foo floyd and the fruit flies",
405                "old bananas",
406                Some("old bananas"),
407            );
408
409            let params = scrobble.as_map();
410            assert_eq!(params["artist"], "foo floyd and the fruit flies");
411            assert_eq!(params["track"], "old bananas");
412            assert_eq!(params["album"], "old bananas");
413        }
414    }
415}