From 2d23b4cccf4e5b2eb944b5d5e7fb26d1d73a8323 Mon Sep 17 00:00:00 2001 From: Koen Bolhuis Date: Sun, 10 Jan 2021 17:59:15 +0100 Subject: [PATCH] Implement more endpoints - users/{user_list}/recent-listens - user/{user_name}/playing-now - user/{user_name}/listens - latest-import --- src/lib.rs | 169 +++++++++++++++++++++++++++++++---------- src/models/request.rs | 9 ++- src/models/response.rs | 94 +++++++++++++++++++++++ 3 files changed, 231 insertions(+), 41 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 517ddaf..91584f5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,9 +3,9 @@ use std::fmt; use std::io; -use serde::{Serialize, Deserialize}; use serde::de::DeserializeOwned; -use thiserror::Error; +use serde::{Deserialize, Serialize}; + use ureq::Agent; pub mod models; @@ -19,11 +19,11 @@ enum Endpoint<'a> { SubmitListens, ValidateToken, DeleteListen, - //UsersRecentListens(&'a [&'a str]), + UsersRecentListens(&'a [&'a str]), UserListenCount(&'a str), - // UserPlayingNow(&'a str), - // UserListens(&'a str), - // LatestImport, + UserPlayingNow(&'a str), + UserListens(&'a str), + LatestImport, // StatsSitewideArtists, // StatsUserListeningActivity(&'a str), // StatsUserDailyActivity(&'a str), @@ -40,14 +40,14 @@ impl<'a> fmt::Display for Endpoint<'a> { Self::SubmitListens => "submit-listens", Self::ValidateToken => "validate-token", Self::DeleteListen => "delete-listen", - // Self::UsersRecentListens(users) => { - // let users = users.join(","); - // return write!(f, "users/{}/recent-listens", users); - // } + Self::UsersRecentListens(users) => { + // TODO: url-encode usernames with commas + return write!(f, "users/{}/recent-listens", users.join(",")); + } Self::UserListenCount(user) => return write!(f, "user/{}/listen-count", user), - // Self::UserPlayingNow(user) => return write!(f, "user/{}/playing-now", user), - // Self::UserListens(user) => return write!(f, "user/{}/listens", user), - // Self::LatestImport => "latest-import", + Self::UserPlayingNow(user) => return write!(f, "user/{}/playing-now", user), + Self::UserListens(user) => return write!(f, "user/{}/listens", user), + Self::LatestImport => "latest-import", // Self::StatsSitewideArtists => "stats/sitewide/artists", // Self::StatsUserListeningActivity(user) => return write!(f, "stats/user/{}/listening-activity", user), // Self::StatsUserDailyActivity(user) => return write!(f, "stats/user/{}/daily-activity", user), @@ -62,7 +62,7 @@ impl<'a> fmt::Display for Endpoint<'a> { } /// Represents errors that can occor while interacting with the API. -#[derive(Error, Debug)] +#[derive(Debug, thiserror::Error)] pub enum Error { /// The API returned a non-200 status code. #[error("API error ({code}): {error}")] @@ -99,12 +99,10 @@ impl From for Error { impl From for Error { fn from(error: ureq::Error) -> Self { match error { - ureq::Error::Status(_code, response) => { - match response.into_json::() { - Ok(api_error) => api_error.into(), - Err(err) => Error::ResponseJson(err), - } - } + ureq::Error::Status(_code, response) => match response.into_json::() { + Ok(api_error) => api_error.into(), + Err(err) => Error::ResponseJson(err), + }, ureq::Error::Transport(_) => Error::Http(error), } } @@ -126,14 +124,12 @@ impl Client { } } - fn get(&mut self, endpoint: Endpoint, query: &[(&str, &str)]) -> Result { + fn get(&mut self, endpoint: Endpoint) -> Result { let endpoint = format!("{}{}", API_ROOT_URL, endpoint); - let mut request = self.agent.get(&endpoint); - for &(param, value) in query.iter() { - request = request.query(param, value); - } - request.call()? + self.agent + .get(&endpoint) + .call()? .into_json() .map_err(Error::ResponseJson) } @@ -147,7 +143,8 @@ impl Client { let endpoint = format!("{}{}", API_ROOT_URL, endpoint); - self.agent.post(&endpoint) + self.agent + .post(&endpoint) .set("Authorization", &format!("Token {}", token)) .send_json(data)? .into_json() @@ -155,32 +152,124 @@ impl Client { } /// Endpoint: `submit-listens` - pub fn submit_listens(&mut self, token: &str, data: SubmitListens) -> Result { + pub fn submit_listens( + &mut self, + token: &str, + data: SubmitListens, + ) -> Result { self.post(Endpoint::SubmitListens, token, data) } /// Endpoint: `validate-token` pub fn validate_token(&mut self, token: &str) -> Result { - self.get(Endpoint::ValidateToken, &[("token", token)]) + let endpoint = format!("{}{}", API_ROOT_URL, Endpoint::ValidateToken); + + self.agent + .get(&endpoint) + .query("token", token) + .call()? + .into_json() + .map_err(Error::ResponseJson) } /// Endpoint: `delete-listen` - pub fn delete_listen(&mut self, token: &str, data: DeleteListen) -> Result { + pub fn delete_listen( + &mut self, + token: &str, + data: DeleteListen, + ) -> Result { self.post(Endpoint::DeleteListen, token, data) } - /// Endpoint: `user/{user_name}/listen-count` - pub fn user_listen_count(&mut self, user: &str) -> Result { - self.get(Endpoint::UserListenCount(user), &[]) + /// Endpoint: `users/{user_list}/recent-listens + pub fn users_recent_listens( + &mut self, + user_list: &[&str], + ) -> Result { + self.get(Endpoint::UsersRecentListens(user_list)) } - /// Endpoint: `status/get-dump-info` - pub fn status_get_dump_info(&mut self, id: Option) -> Result { - if let Some(id) = id { - let id = id.to_string(); - self.get(Endpoint::StatusGetDumpInfo, &[("id", &id)]) - } else { - self.get(Endpoint::StatusGetDumpInfo, &[]) + /// Endpoint: `user/{user_name}/listen-count` + pub fn user_listen_count(&mut self, user_name: &str) -> Result { + self.get(Endpoint::UserListenCount(user_name)) + } + + // UserPlayingNow(&'a str), + /// Endpoint: `user/{user_name}/playing-now` + pub fn user_playing_now(&mut self, user_name: &str) -> Result { + self.get(Endpoint::UserPlayingNow(user_name)) + } + + pub fn user_listens( + &mut self, + user_name: &str, + min_ts: Option, + max_ts: Option, + count: Option, + time_range: Option + ) -> Result { + let endpoint = format!("{}{}", API_ROOT_URL, Endpoint::UserListens(user_name)); + + let mut request = self.agent.get(&endpoint); + + if let Some(min_ts) = min_ts { + request = request.query("min_ts", &min_ts.to_string()); } + if let Some(max_ts) = max_ts { + request = request.query("max_ts", &max_ts.to_string()); + } + if let Some(count) = count { + request = request.query("count", &count.to_string()); + } + if let Some(time_range) = time_range { + request = request.query("time_range", &time_range.to_string()); + } + + request.call()?.into_json().map_err(Error::ResponseJson) + } + + /// Endpoint: `latest-import` (GET) + pub fn get_latest_import(&mut self, user_name: &str) -> Result { + let endpoint = format!("{}{}", API_ROOT_URL, Endpoint::LatestImport); + + self.agent + .get(&endpoint) + .query("user_name", user_name) + .call()? + .into_json() + .map_err(Error::ResponseJson) + } + + /// Endpoint: `latest-import` (POST) + pub fn update_latest_import( + &mut self, + token: &str, + data: UpdateLatestImport, + ) -> Result { + self.post(Endpoint::LatestImport, token, data) + } + + // StatsSitewideArtists, + // StatsUserListeningActivity(&'a str), + // StatsUserDailyActivity(&'a str), + // StatsUserRecordings(&'a str), + // StatsUserArtistMap(&'a str), + // StatsUserReleases(&'a str), + // StatsUserArtists(&'a str), + + /// Endpoint: `status/get-dump-info` + pub fn status_get_dump_info( + &mut self, + id: Option, + ) -> Result { + let endpoint = format!("{}{}", API_ROOT_URL, Endpoint::StatusGetDumpInfo); + + let mut request = self.agent.get(&endpoint); + + if let Some(id) = id { + request = request.query("id", &id.to_string()); + } + + request.call()?.into_json().map_err(Error::ResponseJson) } } diff --git a/src/models/request.rs b/src/models/request.rs index 3677c4f..f6ac20e 100644 --- a/src/models/request.rs +++ b/src/models/request.rs @@ -20,7 +20,7 @@ pub enum ListenType { #[derive(Debug, Serialize)] pub struct Payload<'a> { pub listened_at: i64, - pub track_metadata: TrackMetadata<'a> + pub track_metadata: TrackMetadata<'a>, } #[derive(Debug, Serialize)] @@ -38,3 +38,10 @@ pub struct DeleteListen<'a> { pub listened_at: i64, pub recording_msid: &'a str, } + +// --------- latest-import (POST) + +#[derive(Debug, Serialize)] +pub struct UpdateLatestImport { + pub ts: i64, +} diff --git a/src/models/response.rs b/src/models/response.rs index 22781bf..775e955 100644 --- a/src/models/response.rs +++ b/src/models/response.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use serde::Deserialize; #[derive(Debug, Deserialize)] @@ -33,6 +35,37 @@ pub struct DeleteListenResponse { pub api_response: ApiResponse, } +// --------- users/{user_list}/recent-listens + +#[derive(Debug, Deserialize)] +pub struct UsersRecentListensResponse { + pub payload: UsersRecentListensPayload, +} + +#[derive(Debug, Deserialize)] +pub struct UsersRecentListensPayload { + pub count: u64, + pub listens: Vec, + pub user_list: String, +} + +#[derive(Debug, Deserialize)] +pub struct UsersRecentListensListen { + pub user_name: String, + pub inserted_at: String, + pub listened_at: i64, + pub recording_msid: String, + pub track_metadata: UsersRecentListensTrackMetadata, +} + +#[derive(Debug, Deserialize)] +pub struct UsersRecentListensTrackMetadata { + pub artist_name: String, + pub track_name: String, + pub release_name: Option, + pub additional_info: HashMap, +} + // --------- user/{user_name}/listen-count #[derive(Debug, Deserialize)] @@ -43,6 +76,67 @@ pub struct UserListenCountResponse { pub count: u64, } +// -------- user/{user_name}/playing-now + +#[derive(Debug, Deserialize)] +pub struct UserPlayingNowResponse { + pub payload: UserPlayingNowPayload, +} + +#[derive(Debug, Deserialize)] +pub struct UserPlayingNowPayload { + pub count: u8, + pub user_id: String, + pub listens: Vec<()>, +} + +// -------- user/{user_name}/listens + +#[derive(Debug, Deserialize)] +pub struct UserListensResponse { + pub payload: UserListensPayload, +} + +#[derive(Debug, Deserialize)] +pub struct UserListensPayload { + pub count: u64, + pub latest_listen_ts: i64, + pub user_id: String, + pub listens: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct UserListensListen { + pub user_name: String, + pub inserted_at: String, + pub listened_at: i64, + pub recording_msid: String, + pub track_metadata: UserListensTrackMetadata, +} + +#[derive(Debug, Deserialize)] +pub struct UserListensTrackMetadata { + pub artist_name: String, + pub track_name: String, + pub release_name: Option, + pub additional_info: HashMap, +} + +// --------- latest-import (GET) + +#[derive(Debug, Deserialize)] +pub struct GetLatestImportResponse { + pub latest_import: i64, + pub musicbrainz_id: String, +} + +// --------- latest-import (POST) + +#[derive(Debug, Deserialize)] +pub struct UpdateLatestImportResponse { + pub status: String, +} + // --------- status/get-dump-info #[derive(Debug, Deserialize)]