From 643404a352cc825440bf359411ce757054dbd2f8 Mon Sep 17 00:00:00 2001 From: Josh Voorkamp Date: Sat, 15 May 2021 14:04:37 +1200 Subject: [PATCH 1/2] added track.getSimilar functionality --- src/lib.rs | 1 + src/track/mod.rs | 14 ++++++ src/track/similar.rs | 102 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 117 insertions(+) create mode 100644 src/track/mod.rs create mode 100644 src/track/similar.rs diff --git a/src/lib.rs b/src/lib.rs index 1fba395..e5cfc78 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -39,6 +39,7 @@ pub mod error; pub mod macros; pub mod model; pub mod user; +pub mod track; pub mod utilities; const WS_ENDPOINT: &str = "http://ws.audioscrobbler.com/2.0/"; diff --git a/src/track/mod.rs b/src/track/mod.rs new file mode 100644 index 0000000..ee7f581 --- /dev/null +++ b/src/track/mod.rs @@ -0,0 +1,14 @@ +//! Last.fm Track API Endpoints +//! +//! Contains structs and methods related to working with the track-related endpoints +//! available through the Last.fm API + +use serde::Deserialize; + +pub mod similar; + +#[derive(Debug, Deserialize)] +pub struct Track { + #[serde(rename = "similartracks")] + pub similar_tracks: Option, +} diff --git a/src/track/similar.rs b/src/track/similar.rs new file mode 100644 index 0000000..6d08ebd --- /dev/null +++ b/src/track/similar.rs @@ -0,0 +1,102 @@ +use serde::Deserialize; +use std::marker::PhantomData; + +use crate::{ + error::{Error, LastFMError}, + model::{Attributes, Image}, + track::{Track as OriginalTrack}, + Client, RequestBuilder, +}; + +/// The main similar structure. +/// +/// This is split off into two areas: One, the attributes (used +/// for displaying various user-associated attributes), and two, +/// the similar tracks to the one provided. +/// +/// For details on the attributes available, refer to [Attributes]. For +/// details on the track information available, refer to [Track]. +#[derive(Debug, Deserialize)] +pub struct Similar { + /// A [Vec] containing similar [Track]s. + #[serde(rename = "track")] + pub tracks: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct Track { + /// The name of the given track + pub name: String, + /// The MusicBrainz ID for the given track. + pub mbid: Option, + /// The match for the given track + pub r#match: f32, + /// The Last.fm URL of the track + pub url: String, + /// Whether or not the track is streamable + pub streamable: Streamable, + /// the artist who published the given track + pub artist: Artist, + /// The cover art for the given track. Available in small, medium, + /// and large sizes. + #[serde(rename="image")] + pub images: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct Artist { + /// The name of the given artist + pub name: String, + /// The MusicBrainz ID of the given artist + pub mbid: Option, + /// The Last.fm URL for the given artist + pub url: String, +} + +/// The streamable struct. +/// +/// Available if the given track is available for streaming. +#[derive(Debug, Deserialize)] +pub struct Streamable { + pub fulltrack: String, + #[serde(rename = "#text")] + pub text: String, +} + +impl Similar { + pub async fn build_by_mbid<'a>(client: &'a mut Client, mbid: &str) -> RequestBuilder<'a, Similar> { + let url = client.build_url(vec![("method", "track.getSimilar"), ("mbid", mbid)]).await; + RequestBuilder { client, url, phantom: PhantomData } + } + + pub async fn build<'a>(client: &'a mut Client, artist: &str, track: &str) -> RequestBuilder<'a, Similar> { + let url = client.build_url(vec![("method", "track.getSimilar"), ("artist", artist), ("track", track)]).await; + RequestBuilder { client, url, phantom: PhantomData } + } +} + +impl<'a> RequestBuilder<'a, Similar> { + add_param!(with_limit, limit, usize); + + pub async fn send(&'a mut self) -> Result { + match self.client.request(&self.url).await { + Ok(response) => { + let body = response.text().await.unwrap(); + match serde_json::from_str::(&body) { + Ok(lastfm_error) => Err(Error::LastFMError(lastfm_error.into())), + Err(_) => match serde_json::from_str::(&body) { + Ok(tracks) => Ok(tracks.similar_tracks.unwrap()), + Err(e) => Err(Error::ParsingError(e)), + }, + } + } + Err(err) => Err(Error::HTTPError(err)), + } + } +} + +impl<'a> Client { + pub async fn similar_tracks_by_mbid(&'a mut self, mbid: &str) -> RequestBuilder<'a, Similar> { Similar::build_by_mbid(self, mbid).await } + + pub async fn similar_tracks(&'a mut self, artist: &str, track: &str) -> RequestBuilder<'a, Similar> { Similar::build(self, artist, track).await } +} From fb5ff5e427d3134e8983b136627b5882cde2d891 Mon Sep 17 00:00:00 2001 From: Josh Voorkamp Date: Sat, 29 May 2021 09:57:00 +1200 Subject: [PATCH 2/2] refactor structs so a track is a track, regardless of endpoint --- Cargo.toml | 1 + src/model/custom_deserialization.rs | 85 +++++++++++++++++++++ src/model/mod.rs | 114 ++++++++++++++++++++++++++++ src/track/mod.rs | 2 +- src/track/similar.rs | 46 +---------- src/user/loved_tracks.rs | 46 +---------- src/user/mod.rs | 1 + src/user/recent_tracks.rs | 48 +----------- src/user/top_artists.rs | 29 +------ 9 files changed, 211 insertions(+), 161 deletions(-) create mode 100644 src/model/custom_deserialization.rs diff --git a/Cargo.toml b/Cargo.toml index c8bc816..4e37b67 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,3 +20,4 @@ serde = { version = "1.0.125", features = ["derive"] } serde_json = "1.0.64" reqwest = { version = "0.11.2", features = ["json"] } url = "2.2.1" +void = "1.0.2" diff --git a/src/model/custom_deserialization.rs b/src/model/custom_deserialization.rs new file mode 100644 index 0000000..1247265 --- /dev/null +++ b/src/model/custom_deserialization.rs @@ -0,0 +1,85 @@ +// The following code is adapted from https://serde.rs/string-or-struct.html +// as it appears on 2021-05-29 + +// the `string_or_struct` function uses these impl to instantiate a Type +// if the input file contains a string and not a struct. + +use serde::{Deserialize, Deserializer}; +use serde::de::{self, MapAccess, Visitor}; +use std::fmt; +use std::marker::PhantomData; +use std::str::FromStr; +use void::Void; + +pub fn option_string_or_struct<'de, T, D>(deserializer: D) -> Result, D::Error> +where + T: Deserialize<'de> + FromStr, + D: Deserializer<'de>, +{ + struct OptionStringOrStruct(PhantomData T>); + + impl<'de, T> Visitor<'de> for OptionStringOrStruct> + where + T: Deserialize<'de> + FromStr, + { + type Value = Option; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("null, string or map") + } + + fn visit_str(self, value: &str) -> Result, E> + where + E: de::Error, + { + Ok(Some(FromStr::from_str(value).unwrap())) + } + + fn visit_map(self, map: M) -> Result, M::Error> + where + M: MapAccess<'de> + { + match Deserialize::deserialize(de::value::MapAccessDeserializer::new(map)) { + Ok(res) => Ok(Some(res)), + Err(err) => Err(err), + } + } + } + + deserializer.deserialize_any(OptionStringOrStruct(PhantomData)) +} + +pub fn string_or_struct<'de, T, D>(deserializer: D) -> Result +where + T: Deserialize<'de> + FromStr, + D: Deserializer<'de>, +{ + struct StringOrStruct(PhantomData T>); + + impl<'de, T> Visitor<'de> for StringOrStruct + where + T: Deserialize<'de> + FromStr, + { + type Value = T; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("string or map") + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + Ok(FromStr::from_str(value).unwrap()) + } + + fn visit_map(self, map: M) -> Result + where + M: MapAccess<'de> + { + Deserialize::deserialize(de::value::MapAccessDeserializer::new(map)) + } + } + + deserializer.deserialize_any(StringOrStruct(PhantomData)) +} diff --git a/src/model/mod.rs b/src/model/mod.rs index 83f7831..0a1d622 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -3,11 +3,16 @@ //! These are the various models the crate uses throughout the library, centralized //! in this file to ease development and remove code duplication. +use crate::model::custom_deserialization::{string_or_struct, option_string_or_struct}; use chrono::{DateTime, Utc}; use serde::Deserialize; +use std::str::FromStr; +use void::Void; use crate::utilities::deserialize_datetime_from_str; +pub mod custom_deserialization; + /// Various attributes transmitted by several API endpoints. #[derive(Debug, Deserialize)] pub struct Attributes { @@ -55,3 +60,112 @@ pub struct Image { #[serde(rename = "#text")] pub image_url: String, } + +/// Contains information about the given track +#[derive(Debug, Deserialize)] +pub struct Track { + /// The artist who published the given track. + #[serde(deserialize_with = "string_or_struct")] + pub artist: Artist, + /// Various attributes associated with the track. + #[serde(rename = "@attr")] + pub attrs: Option, + /// The MusicBrainz ID for the given track. + pub mbid: Option, + /// The name of the track. + pub name: String, + /// The album the track is associated with. + pub album: Option, + /// The last.fm URL of the track. + pub url: String, + /// Images associated with the track. + #[serde(rename = "image")] + pub images: Vec, + /// The date of when the track was scrobbled. + /// Returned when output comes from some endpoints such as loved_tracks + pub date: Option, + /// Whether or not the track is streamable + #[serde(deserialize_with = "option_string_or_struct")] + pub streamable: Option, + /// The match for the given track + /// Returned when output comes from some endpoints such as similar + pub r#match: Option, +} + +#[derive(Debug, Deserialize)] +pub struct TrackAttributes { + /// Whether or not the user's first available track is the + /// one the user is currently playing. + #[serde(rename = "nowplaying")] + pub now_playing: Option, +} + +impl FromStr for Streamable { + type Err = Void; + + fn from_str(s: &str) -> Result { + Ok(Streamable { + text: s.to_string(), + fulltrack: None, + }) + } +} + +#[derive(Debug, Deserialize)] +pub struct Album { + /// The name of the album. + #[serde(rename = "#text")] + pub name: String, +} + +#[derive(Debug, Deserialize)] +pub struct Artist { + /// The name of the artist. + #[serde(alias = "#text")] + pub name: String, + /// The last.fm URL for the given artist. + pub url: Option, + /// The MusicBrainz ID of the given artist. + pub mbid: Option, + /// Attributes associated with the artist. + #[serde(rename = "@attr")] + pub attrs: Option, + /// How many times the user has scrobbled the artist. + #[serde(rename = "playcount")] + pub scrobbles: Option, + /// The main images linked to the artist. + #[serde(rename = "image")] + pub images: Option>, +} + +impl FromStr for Artist { + type Err = Void; + + fn from_str(s: &str) -> Result { + Ok(Artist { + name: s.to_string(), + url: None, + mbid: None, + attrs: None, + scrobbles: None, + images: None, + }) + } +} + +#[derive(Debug, Deserialize)] +pub struct ArtistAttributes { + /// Where the artist is ranked in the user's profile. + pub rank: Option, +} + +/// The streamable struct. +/// +/// Available if the given track is available for streaming. +#[derive(Debug, Deserialize)] +pub struct Streamable { + pub fulltrack: Option, + #[serde(rename = "#text")] + pub text: String, +} + diff --git a/src/track/mod.rs b/src/track/mod.rs index ee7f581..d05cfbe 100644 --- a/src/track/mod.rs +++ b/src/track/mod.rs @@ -8,7 +8,7 @@ use serde::Deserialize; pub mod similar; #[derive(Debug, Deserialize)] -pub struct Track { +pub struct Endpoints { #[serde(rename = "similartracks")] pub similar_tracks: Option, } diff --git a/src/track/similar.rs b/src/track/similar.rs index 6d08ebd..a934296 100644 --- a/src/track/similar.rs +++ b/src/track/similar.rs @@ -3,8 +3,8 @@ use std::marker::PhantomData; use crate::{ error::{Error, LastFMError}, - model::{Attributes, Image}, - track::{Track as OriginalTrack}, + model::Track, + track::Endpoints, Client, RequestBuilder, }; @@ -23,46 +23,6 @@ pub struct Similar { pub tracks: Vec, } -#[derive(Debug, Deserialize)] -pub struct Track { - /// The name of the given track - pub name: String, - /// The MusicBrainz ID for the given track. - pub mbid: Option, - /// The match for the given track - pub r#match: f32, - /// The Last.fm URL of the track - pub url: String, - /// Whether or not the track is streamable - pub streamable: Streamable, - /// the artist who published the given track - pub artist: Artist, - /// The cover art for the given track. Available in small, medium, - /// and large sizes. - #[serde(rename="image")] - pub images: Vec, -} - -#[derive(Debug, Deserialize)] -pub struct Artist { - /// The name of the given artist - pub name: String, - /// The MusicBrainz ID of the given artist - pub mbid: Option, - /// The Last.fm URL for the given artist - pub url: String, -} - -/// The streamable struct. -/// -/// Available if the given track is available for streaming. -#[derive(Debug, Deserialize)] -pub struct Streamable { - pub fulltrack: String, - #[serde(rename = "#text")] - pub text: String, -} - impl Similar { pub async fn build_by_mbid<'a>(client: &'a mut Client, mbid: &str) -> RequestBuilder<'a, Similar> { let url = client.build_url(vec![("method", "track.getSimilar"), ("mbid", mbid)]).await; @@ -84,7 +44,7 @@ impl<'a> RequestBuilder<'a, Similar> { let body = response.text().await.unwrap(); match serde_json::from_str::(&body) { Ok(lastfm_error) => Err(Error::LastFMError(lastfm_error.into())), - Err(_) => match serde_json::from_str::(&body) { + Err(_) => match serde_json::from_str::(&body) { Ok(tracks) => Ok(tracks.similar_tracks.unwrap()), Err(e) => Err(Error::ParsingError(e)), }, diff --git a/src/user/loved_tracks.rs b/src/user/loved_tracks.rs index b55e071..18d07a4 100644 --- a/src/user/loved_tracks.rs +++ b/src/user/loved_tracks.rs @@ -3,8 +3,8 @@ use std::marker::PhantomData; use crate::{ error::{Error, LastFMError}, - model::{Attributes, Image, TrackDate}, - user::User, + model::Attributes, + user::{User, Track}, Client, RequestBuilder, }; @@ -28,48 +28,6 @@ pub struct LovedTracks { pub tracks: Vec, } -/// Contains information about the given track that the -/// user Loved. -#[derive(Debug, Deserialize)] -pub struct Track { - /// The artist who published the given track. - pub artist: Artist, - /// The MusicBrainz ID for the given track. - pub mbid: String, - /// The date of when the user loved the track. - pub date: Option, - /// The name of the track. - pub name: String, - /// The Last.fm URL of the track. - pub url: String, - /// The cover art for the given track. Available in small, medium, - /// and large sizes. - #[serde(rename = "image")] - pub images: Vec, - /// Whether or not the track is streamable. - pub streamable: Streamable, -} - -#[derive(Debug, Deserialize)] -pub struct Artist { - /// The Last.fm URL for the given artist. - pub url: String, - /// The name of the given artist. - pub name: String, - /// The MusicBrainz ID of the given artist. - pub mbid: String, -} - -/// The streamable struct. -/// -/// Available if the given track is available for streaming. -#[derive(Debug, Deserialize)] -pub struct Streamable { - pub fulltrack: String, - #[serde(rename = "#text")] - pub text: String, -} - impl LovedTracks { pub async fn build<'a>(client: &'a mut Client, user: &str) -> RequestBuilder<'a, LovedTracks> { let url = client.build_url(vec![("method", "user.getLovedTracks"), ("user", user)]).await; diff --git a/src/user/mod.rs b/src/user/mod.rs index a2da7b8..eec3c48 100644 --- a/src/user/mod.rs +++ b/src/user/mod.rs @@ -3,6 +3,7 @@ //! Contains structs and methods related to working with the user-related endpoints //! available through the Last.fm API. +use crate::model::{Track, Artist}; use serde::Deserialize; pub mod loved_tracks; diff --git a/src/user/recent_tracks.rs b/src/user/recent_tracks.rs index 3503e2d..4ddc4fc 100644 --- a/src/user/recent_tracks.rs +++ b/src/user/recent_tracks.rs @@ -3,8 +3,8 @@ use std::marker::PhantomData; use crate::{ error::{Error, LastFMError}, - model::{Attributes, Image, TrackDate}, - user::User, + model::Attributes, + user::{User, Track}, Client, RequestBuilder, }; @@ -26,50 +26,6 @@ pub struct RecentTracks { pub tracks: Vec, } -#[derive(Debug, Deserialize)] -pub struct Track { - /// The primary artist associated with the track. - pub artist: Artist, - /// Various attributes associated with the track. - #[serde(rename = "@attr")] - pub attrs: Option, - /// The name of the track. - pub name: String, - /// The album the track is associated with. - pub album: Album, - /// The last.fm URL of the track. - pub url: String, - /// Whether or not a track is streamable. - pub streamable: String, - /// Images associated with the track. - #[serde(rename = "image")] - pub images: Vec, - /// The date of when the track was scrobbled. - pub date: Option, -} - -#[derive(Debug, Deserialize)] -pub struct Artist { - /// The name of the artist. - #[serde(rename = "#text")] - pub name: String, -} - -#[derive(Debug, Deserialize)] -pub struct Album { - /// The name of the album. - #[serde(rename = "#text")] - pub name: String, -} - -#[derive(Debug, Deserialize)] -pub struct TrackAttributes { - /// Whether or not the user's first available track is the - /// one the user is currently playing. - #[serde(rename = "nowplaying")] - pub now_playing: String, -} - impl RecentTracks { pub async fn build<'a>(client: &'a mut Client, user: &str) -> RequestBuilder<'a, RecentTracks> { let url = client.build_url(vec![("method", "user.getRecentTracks"), ("user", user)]).await; diff --git a/src/user/top_artists.rs b/src/user/top_artists.rs index e1fe864..ef39fce 100644 --- a/src/user/top_artists.rs +++ b/src/user/top_artists.rs @@ -3,8 +3,8 @@ use std::marker::PhantomData; use crate::{ error::{Error, LastFMError}, - model::{Attributes, Image}, - user::User, + model::Attributes, + user::{User, Artist}, Client, RequestBuilder, }; @@ -25,31 +25,6 @@ pub struct TopArtists { pub attrs: Attributes, } -#[derive(Debug, Deserialize)] -pub struct Artist { - /// Attributes associated with the artist. - #[serde(rename = "@attr")] - pub attrs: ArtistAttributes, - /// The MusicBrainz ID for the artist. - pub mbid: String, - /// How many times the user has scrobbled the artist. - #[serde(rename = "playcount")] - pub scrobbles: String, - /// The name of the artist. - pub name: String, - /// The Last.fm URL for the artist. - pub url: String, - /// The main images linked to the artist. - #[serde(rename = "image")] - pub images: Vec, -} - -#[derive(Debug, Deserialize)] -pub struct ArtistAttributes { - /// Where the artist is ranked in the user's profile. - pub rank: String, -} - impl TopArtists { /// Constructs / builds the request to the user.getTopArtists API endpoint. pub async fn build<'a>(client: &'a mut Client, user: &str) -> RequestBuilder<'a, TopArtists> {