Implement auth.getSession and user.getRecentTracks
This commit is contained in:
		
							parent
							
								
									d62005c371
								
							
						
					
					
						commit
						a6b9f8f675
					
				
					 16 changed files with 3882 additions and 9 deletions
				
			
		
							
								
								
									
										2
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -1 +1 @@
 | 
			
		|||
/target
 | 
			
		||||
**/target
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										8
									
								
								.idea/.gitignore
									
										
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								.idea/.gitignore
									
										
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,8 @@
 | 
			
		|||
# Default ignored files
 | 
			
		||||
/shelf/
 | 
			
		||||
/workspace.xml
 | 
			
		||||
# Editor-based HTTP Client requests
 | 
			
		||||
/httpRequests/
 | 
			
		||||
# Datasource local storage ignored files
 | 
			
		||||
/dataSources/
 | 
			
		||||
/dataSources.local.xml
 | 
			
		||||
							
								
								
									
										13
									
								
								.idea/lastfm-vore.iml
									
										
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								.idea/lastfm-vore.iml
									
										
									
										generated
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,13 @@
 | 
			
		|||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<module type="EMPTY_MODULE" version="4">
 | 
			
		||||
  <component name="NewModuleRootManager">
 | 
			
		||||
    <content url="file://$MODULE_DIR$">
 | 
			
		||||
      <sourceFolder url="file://$MODULE_DIR$/example/src" isTestSource="false" />
 | 
			
		||||
      <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
 | 
			
		||||
      <excludeFolder url="file://$MODULE_DIR$/example/target" />
 | 
			
		||||
      <excludeFolder url="file://$MODULE_DIR$/target" />
 | 
			
		||||
    </content>
 | 
			
		||||
    <orderEntry type="inheritedJdk" />
 | 
			
		||||
    <orderEntry type="sourceFolder" forTests="false" />
 | 
			
		||||
  </component>
 | 
			
		||||
</module>
 | 
			
		||||
							
								
								
									
										8
									
								
								.idea/modules.xml
									
										
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								.idea/modules.xml
									
										
									
										generated
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,8 @@
 | 
			
		|||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<project version="4">
 | 
			
		||||
  <component name="ProjectModuleManager">
 | 
			
		||||
    <modules>
 | 
			
		||||
      <module fileurl="file://$PROJECT_DIR$/.idea/lastfm-vore.iml" filepath="$PROJECT_DIR$/.idea/lastfm-vore.iml" />
 | 
			
		||||
    </modules>
 | 
			
		||||
  </component>
 | 
			
		||||
</project>
 | 
			
		||||
							
								
								
									
										6
									
								
								.idea/vcs.xml
									
										
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.idea/vcs.xml
									
										
									
										generated
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,6 @@
 | 
			
		|||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<project version="4">
 | 
			
		||||
  <component name="VcsDirectoryMappings">
 | 
			
		||||
    <mapping directory="$PROJECT_DIR$" vcs="Git" />
 | 
			
		||||
  </component>
 | 
			
		||||
</project>
 | 
			
		||||
							
								
								
									
										1589
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										1589
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										11
									
								
								Cargo.toml
									
										
									
									
									
								
							
							
						
						
									
										11
									
								
								Cargo.toml
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1,6 +1,17 @@
 | 
			
		|||
[package]
 | 
			
		||||
name = "lastfm-vore"
 | 
			
		||||
license = "Apache-2.0"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
edition = "2024"
 | 
			
		||||
 | 
			
		||||
[dependencies]
 | 
			
		||||
http = "1.3.1"
 | 
			
		||||
 | 
			
		||||
reqwest = { version = "0.12.15", features = ["json"] }
 | 
			
		||||
 | 
			
		||||
md5 = "0.7.0"
 | 
			
		||||
 | 
			
		||||
serde = { version = "1.0.219", features = ["derive"] }
 | 
			
		||||
serde_json = "1.0.140"
 | 
			
		||||
 | 
			
		||||
thiserror = "2.0.12"
 | 
			
		||||
							
								
								
									
										1749
									
								
								example/Cargo.lock
									
										
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										1749
									
								
								example/Cargo.lock
									
										
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										12
									
								
								example/Cargo.toml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								example/Cargo.toml
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,12 @@
 | 
			
		|||
[package]
 | 
			
		||||
name = "example"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
edition = "2024"
 | 
			
		||||
 | 
			
		||||
[dependencies]
 | 
			
		||||
lastfm-vore = { path = "../" }
 | 
			
		||||
 | 
			
		||||
tokio = { version = "1.45.0", features = ["full"] }
 | 
			
		||||
axum = "0.8.4"
 | 
			
		||||
 | 
			
		||||
serde = { version = "1.0.219", features = ["derive"] }
 | 
			
		||||
							
								
								
									
										83
									
								
								example/src/main.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								example/src/main.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,83 @@
 | 
			
		|||
use std::env;
 | 
			
		||||
use std::fmt::format;
 | 
			
		||||
use axum::routing::get;
 | 
			
		||||
use axum::{Extension, Router};
 | 
			
		||||
use axum::extract::Query;
 | 
			
		||||
use axum::response::IntoResponse;
 | 
			
		||||
use lastfm_vore::{LastfmApiError, LastfmClient};
 | 
			
		||||
use serde::Deserialize;
 | 
			
		||||
use lastfm_vore::auth::get_session::GetSessionResponse;
 | 
			
		||||
 | 
			
		||||
#[derive(Clone)]
 | 
			
		||||
struct Context {
 | 
			
		||||
    lastfm: LastfmClient,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[tokio::main]
 | 
			
		||||
async fn main() {
 | 
			
		||||
    let key = env::var("API_KEY").expect("API_KEY does not exist");
 | 
			
		||||
    let secret_key = env::var("SECRET_KEY").expect("SECRET_KEY does not exist");
 | 
			
		||||
 | 
			
		||||
    let lastfm_client = LastfmClient::new(&key, &secret_key);
 | 
			
		||||
 | 
			
		||||
    let app = Router::new()
 | 
			
		||||
        .route("/callback", get(callback))
 | 
			
		||||
        .layer(Extension(Context { lastfm: lastfm_client }));
 | 
			
		||||
 | 
			
		||||
    let listener = tokio::net::TcpListener::bind("127.0.0.1:8080").await.unwrap();
 | 
			
		||||
 | 
			
		||||
    println!("wait a couple seconds and then head over to https://www.last.fm/api/auth/?api_key={}", key);
 | 
			
		||||
 | 
			
		||||
    axum::serve(listener, app).await.unwrap();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Deserialize, Debug)]
 | 
			
		||||
struct CallbackQuery {
 | 
			
		||||
    token: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn callback(
 | 
			
		||||
    Extension(ctx): Extension<Context>,
 | 
			
		||||
    Query(query): Query<CallbackQuery>,
 | 
			
		||||
) -> String {
 | 
			
		||||
    let res = lastfm_vore::auth::get_session::GetSession::send(&ctx.lastfm, query.token).await;
 | 
			
		||||
 | 
			
		||||
    match res {
 | 
			
		||||
        Ok(res) => {
 | 
			
		||||
            let res_two = lastfm_vore::user::get_recent_tracks::GetRecentTracks::send(&ctx.lastfm, res.session.name.clone(), None, None, None, None, true).await;
 | 
			
		||||
 | 
			
		||||
            match res_two {
 | 
			
		||||
                Ok(res_two) => {
 | 
			
		||||
                    let res_two = res_two.recenttracks;
 | 
			
		||||
 | 
			
		||||
                    let track = res_two.track.first().unwrap();
 | 
			
		||||
                    let artist = track.artist.clone().unwrap();
 | 
			
		||||
 | 
			
		||||
                    let mut format_string = format!("Hello {}! ", res.session.name);
 | 
			
		||||
                    
 | 
			
		||||
                    if track.is_playing() {
 | 
			
		||||
                        format_string.push_str("You're currently playing: ");
 | 
			
		||||
                    } else {
 | 
			
		||||
                        format_string.push_str("You last listened to: ");
 | 
			
		||||
                    }
 | 
			
		||||
                    
 | 
			
		||||
                    format_string.push_str(format!("{} by {} ", track.name, artist.get_artist_name()).as_str());
 | 
			
		||||
                    
 | 
			
		||||
                    if let Some(album) = track.album.clone() {
 | 
			
		||||
                        format_string.push_str(format!("on {}", album.text).as_str());
 | 
			
		||||
                    }
 | 
			
		||||
                    
 | 
			
		||||
                    format_string.push_str("!");
 | 
			
		||||
                    
 | 
			
		||||
                    format_string
 | 
			
		||||
                }
 | 
			
		||||
                Err(e) => {
 | 
			
		||||
                    e.to_string()
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            e.to_string()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										26
									
								
								src/auth/get_session.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/auth/get_session.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,26 @@
 | 
			
		|||
use serde::{Deserialize};
 | 
			
		||||
use crate::auth::Session;
 | 
			
		||||
use crate::{ApiError, LastfmApiError, LastfmClient};
 | 
			
		||||
 | 
			
		||||
#[derive(Deserialize, Clone, Debug)]
 | 
			
		||||
pub struct GetSessionResponse {
 | 
			
		||||
    pub session: Session,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub struct GetSession;
 | 
			
		||||
 | 
			
		||||
impl GetSession {
 | 
			
		||||
    pub async fn send(lastfm_client: &LastfmClient, token: String) -> Result<GetSessionResponse, LastfmApiError> {
 | 
			
		||||
        let api_url = lastfm_client.build_get_url("auth.getSession", &[("token", token.as_str())], true);
 | 
			
		||||
 | 
			
		||||
        let resp = reqwest::get(api_url.as_str())
 | 
			
		||||
            .await?
 | 
			
		||||
            .text()
 | 
			
		||||
            .await?;
 | 
			
		||||
 | 
			
		||||
        match serde_json::from_str::<GetSessionResponse>(resp.as_str()) {
 | 
			
		||||
            Ok(resp) => Ok(resp),
 | 
			
		||||
            Err(_) => Err(serde_json::from_str::<ApiError>(resp.as_str())?.into_error())
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										12
									
								
								src/auth/mod.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/auth/mod.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,12 @@
 | 
			
		|||
use crate::util::bool_from_u8;
 | 
			
		||||
use serde::Deserialize;
 | 
			
		||||
 | 
			
		||||
pub mod get_session;
 | 
			
		||||
 | 
			
		||||
#[derive(Deserialize, Clone, Debug)]
 | 
			
		||||
pub struct Session {
 | 
			
		||||
    pub name: String,
 | 
			
		||||
    pub key: String,
 | 
			
		||||
    #[serde(deserialize_with = "bool_from_u8")]
 | 
			
		||||
    pub subscriber: bool
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										151
									
								
								src/lib.rs
									
										
									
									
									
								
							
							
						
						
									
										151
									
								
								src/lib.rs
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1,14 +1,149 @@
 | 
			
		|||
pub fn add(left: u64, right: u64) -> u64 {
 | 
			
		||||
    left + right
 | 
			
		||||
use http::Uri;
 | 
			
		||||
use serde::Deserialize;
 | 
			
		||||
use thiserror::Error;
 | 
			
		||||
 | 
			
		||||
pub mod auth;
 | 
			
		||||
pub mod user;
 | 
			
		||||
mod util;
 | 
			
		||||
 | 
			
		||||
const DEFAULT_LAST_FM_API_URI: &str = "https://ws.audioscrobbler.com/2.0/";
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone)]
 | 
			
		||||
pub struct LastfmClient {
 | 
			
		||||
    api_url: String,
 | 
			
		||||
    key: String,
 | 
			
		||||
    secret: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl LastfmClient {
 | 
			
		||||
    pub fn new(key: &str, secret: &str) -> Self {
 | 
			
		||||
        LastfmClient {
 | 
			
		||||
            api_url: DEFAULT_LAST_FM_API_URI.to_string(),
 | 
			
		||||
            key: key.to_string(),
 | 
			
		||||
            secret: secret.to_string(),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    pub fn new_with_custom_api(url: &str, key: &str, secret: &str) -> Self {
 | 
			
		||||
        LastfmClient {
 | 
			
		||||
            api_url: url.parse::<Uri>().expect("Invalid API URL").to_string(),
 | 
			
		||||
            key: key.to_string(),
 | 
			
		||||
            secret: secret.to_string(),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn sign_call(&self, method: &str, params: &[(&str, &str)]) -> md5::Digest {
 | 
			
		||||
        let mut string = String::new();
 | 
			
		||||
 | 
			
		||||
        string.push_str("api_key");
 | 
			
		||||
        string.push_str(self.key.as_str());
 | 
			
		||||
 | 
			
		||||
        string.push_str("method");
 | 
			
		||||
        string.push_str(method);
 | 
			
		||||
 | 
			
		||||
        for i in params {
 | 
			
		||||
            string.push_str(&i.0);
 | 
			
		||||
            string.push_str(&i.1);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        string.push_str(self.secret.as_str());
 | 
			
		||||
 | 
			
		||||
        md5::compute(string)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn build_get_url(&self, method: &str, params: &[(&str, &str)], sign: bool) -> String {
 | 
			
		||||
        let mut api_url = format!("{}?method={}&api_key={}", self.api_url, method, self.key);
 | 
			
		||||
 | 
			
		||||
        for i in params {
 | 
			
		||||
            api_url.push_str(format!("&{}={}", i.0, i.1).as_str());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if sign {
 | 
			
		||||
            api_url.push_str(format!("&api_sig={:x}", self.sign_call(method, params)).as_str());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        api_url.push_str("&format=json");
 | 
			
		||||
 | 
			
		||||
        api_url
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Error)]
 | 
			
		||||
#[repr(u16)]
 | 
			
		||||
pub enum LastfmApiError {
 | 
			
		||||
    #[error("This service does not exist")]
 | 
			
		||||
    InvalidService = 2,
 | 
			
		||||
    #[error("No method with that name in this package")]
 | 
			
		||||
    InvalidMethod,
 | 
			
		||||
    #[error("You do not have permissions to access the service")]
 | 
			
		||||
    AuthenticationFailed,
 | 
			
		||||
    #[error("This services doesn't exist in that format")]
 | 
			
		||||
    InvalidFormat,
 | 
			
		||||
    #[error("Your request is missing a required parameter")]
 | 
			
		||||
    InvalidParameters,
 | 
			
		||||
    #[error("Invalid resource specified")]
 | 
			
		||||
    InvalidResource,
 | 
			
		||||
    #[error("Something else went wrong")]
 | 
			
		||||
    OperationFailed,
 | 
			
		||||
    #[error("Please re-authenticate")]
 | 
			
		||||
    InvalidSessionKey,
 | 
			
		||||
    #[error("You must be granted a valid key by last.fm")]
 | 
			
		||||
    InvalidAPIKey,
 | 
			
		||||
    #[error("This service is temporarily offline. Try again later.")]
 | 
			
		||||
    ServiceOffline,
 | 
			
		||||
    #[error("Invalid method signature supplied")]
 | 
			
		||||
    InvalidMethodSignature = 13,
 | 
			
		||||
    #[error("This token has not been authorized")]
 | 
			
		||||
    UnauthorizedToken,
 | 
			
		||||
    #[error("This token has expired")]
 | 
			
		||||
    ExpiredToken,
 | 
			
		||||
    #[error("Access for your account has been suspended, please contact Last.fm")]
 | 
			
		||||
    SuspendedKey = 26,
 | 
			
		||||
    #[error("Your IP has made too many requests in a short period")]
 | 
			
		||||
    RateLimitExceeded = 29,
 | 
			
		||||
    #[error("Unknown Error")]
 | 
			
		||||
    Unknown,
 | 
			
		||||
    #[error(transparent)]
 | 
			
		||||
    Reqwest(#[from] reqwest::Error),
 | 
			
		||||
    #[error(transparent)]
 | 
			
		||||
    SerdeJson(#[from] serde_json::Error),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Deserialize, Debug)]
 | 
			
		||||
struct ApiError {
 | 
			
		||||
    error: u16,
 | 
			
		||||
    message: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl ApiError {
 | 
			
		||||
    pub fn into_error(self) -> LastfmApiError {
 | 
			
		||||
        self.error.into()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<u16> for LastfmApiError {
 | 
			
		||||
    fn from(error: u16) -> Self {
 | 
			
		||||
        match error {
 | 
			
		||||
            2 => LastfmApiError::InvalidService,
 | 
			
		||||
            3 => LastfmApiError::InvalidMethod,
 | 
			
		||||
            4 => LastfmApiError::AuthenticationFailed,
 | 
			
		||||
            5 => LastfmApiError::InvalidFormat,
 | 
			
		||||
            6 => LastfmApiError::InvalidParameters,
 | 
			
		||||
            7 => LastfmApiError::InvalidResource,
 | 
			
		||||
            8 => LastfmApiError::OperationFailed,
 | 
			
		||||
            9 => LastfmApiError::InvalidSessionKey,
 | 
			
		||||
            10 => LastfmApiError::InvalidAPIKey,
 | 
			
		||||
            11 => LastfmApiError::ServiceOffline,
 | 
			
		||||
            13 => LastfmApiError::InvalidMethodSignature,
 | 
			
		||||
            14 => LastfmApiError::UnauthorizedToken,
 | 
			
		||||
            15 => LastfmApiError::ExpiredToken,
 | 
			
		||||
            26 => LastfmApiError::SuspendedKey,
 | 
			
		||||
            29 => LastfmApiError::RateLimitExceeded,
 | 
			
		||||
            _ => LastfmApiError::Unknown,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(test)]
 | 
			
		||||
mod tests {
 | 
			
		||||
    use super::*;
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn it_works() {
 | 
			
		||||
        let result = add(2, 2);
 | 
			
		||||
        assert_eq!(result, 4);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										76
									
								
								src/user/get_recent_tracks.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								src/user/get_recent_tracks.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,76 @@
 | 
			
		|||
use crate::util::i32_from_string;
 | 
			
		||||
use serde::Deserialize;
 | 
			
		||||
use crate::{ApiError, LastfmApiError, LastfmClient};
 | 
			
		||||
use crate::user::Track;
 | 
			
		||||
 | 
			
		||||
#[derive(Deserialize, Clone, Debug)]
 | 
			
		||||
pub struct GetRecentTracksResponse {
 | 
			
		||||
    pub recenttracks: RecentTracks,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Deserialize, Clone, Debug)]
 | 
			
		||||
pub struct RecentTracks {
 | 
			
		||||
    pub track: Vec<Track>,
 | 
			
		||||
    #[serde(rename(deserialize = "@attr"))]
 | 
			
		||||
    pub attributes: GetRecentTracksAttributes,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Deserialize, Clone, Debug)]
 | 
			
		||||
#[serde(rename_all = "camelCase")]
 | 
			
		||||
pub struct GetRecentTracksAttributes {
 | 
			
		||||
    pub user: String,
 | 
			
		||||
    #[serde(deserialize_with = "i32_from_string")]
 | 
			
		||||
    pub total_pages: i32,
 | 
			
		||||
    #[serde(deserialize_with = "i32_from_string")]
 | 
			
		||||
    pub page: i32,
 | 
			
		||||
    #[serde(deserialize_with = "i32_from_string")]
 | 
			
		||||
    pub per_page: i32,
 | 
			
		||||
    #[serde(deserialize_with = "i32_from_string")]
 | 
			
		||||
    pub total: i32
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub struct GetRecentTracks;
 | 
			
		||||
 | 
			
		||||
impl GetRecentTracks {
 | 
			
		||||
    pub async fn send(lastfm_client: &LastfmClient, username: String, limit: Option<i32>, page: Option<i32>, from: Option<i32>, to: Option<i32>, extended: bool) -> Result<GetRecentTracksResponse, LastfmApiError> {
 | 
			
		||||
        let mut params: Vec<(&str, String)> = Vec::new();
 | 
			
		||||
 | 
			
		||||
        params.push(("user", username));
 | 
			
		||||
 | 
			
		||||
        if let Some(l) = limit {
 | 
			
		||||
            params.push(("limit", l.to_string()));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if let Some(p) = page {
 | 
			
		||||
            params.push(("page", p.to_string()));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if let Some(f) = from {
 | 
			
		||||
            params.push(("from", f.to_string()));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if let Some(t) = to {
 | 
			
		||||
            params.push(("to", t.to_string()));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if extended {
 | 
			
		||||
            params.push(("extended", "1".to_string()));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let api_url = lastfm_client.build_get_url("user.getRecentTracks", params.iter().map(| (k, v) | (*k, v.as_str())).collect::<Vec<(&str, &str)>>().as_slice(), false);
 | 
			
		||||
 | 
			
		||||
        let resp = reqwest::get(api_url.as_str())
 | 
			
		||||
            .await?
 | 
			
		||||
            .text()
 | 
			
		||||
            .await?;
 | 
			
		||||
 | 
			
		||||
        match serde_json::from_str::<GetRecentTracksResponse>(resp.as_str()) {
 | 
			
		||||
            Ok(resp) => Ok(resp),
 | 
			
		||||
            Err(e) => {
 | 
			
		||||
                println!("{:#?}", e);
 | 
			
		||||
 | 
			
		||||
                Err(serde_json::from_str::<ApiError>(resp.as_str())?.into_error())
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										82
									
								
								src/user/mod.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								src/user/mod.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,82 @@
 | 
			
		|||
use crate::util::bool_from_string_u8;
 | 
			
		||||
use crate::util::i64_from_string;
 | 
			
		||||
use crate::util::bool_from_string;
 | 
			
		||||
use serde::Deserialize;
 | 
			
		||||
 | 
			
		||||
pub mod get_recent_tracks;
 | 
			
		||||
 | 
			
		||||
#[derive(Deserialize, Clone, Debug)]
 | 
			
		||||
pub struct Track {
 | 
			
		||||
    pub mbid: String,
 | 
			
		||||
    pub name: String,
 | 
			
		||||
    pub url: String,
 | 
			
		||||
    #[serde(deserialize_with = "bool_from_string_u8")]
 | 
			
		||||
    pub streamable: bool,
 | 
			
		||||
    pub image: Vec<Image>,
 | 
			
		||||
    pub artist: Option<Artist>,
 | 
			
		||||
    pub album: Option<MbidText>,
 | 
			
		||||
    pub date: Option<UnixTimestampText>,
 | 
			
		||||
    #[serde(default)]
 | 
			
		||||
    #[serde(deserialize_with = "bool_from_string_u8")]
 | 
			
		||||
    pub loved: bool,
 | 
			
		||||
    #[serde(rename(deserialize = "@attr"))]
 | 
			
		||||
    pub attributes: Option<TrackAttributes>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Deserialize, Clone, Debug)]
 | 
			
		||||
pub struct Artist {
 | 
			
		||||
    pub url: Option<String>,
 | 
			
		||||
    pub name: Option<String>,
 | 
			
		||||
    pub image: Option<Vec<Image>>,
 | 
			
		||||
    pub mbid: String,
 | 
			
		||||
    #[serde(rename(deserialize = "#text"))]
 | 
			
		||||
    pub text: Option<String>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Deserialize, Clone, Debug)]
 | 
			
		||||
pub struct MbidText {
 | 
			
		||||
    pub mbid: String,
 | 
			
		||||
    #[serde(rename(deserialize = "#text"))]
 | 
			
		||||
    pub text: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Deserialize, Clone, Debug)]
 | 
			
		||||
pub struct UnixTimestampText {
 | 
			
		||||
    #[serde(deserialize_with = "i64_from_string")]
 | 
			
		||||
    pub uts: i64,
 | 
			
		||||
    #[serde(rename(deserialize = "#text"))]
 | 
			
		||||
    pub text: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Deserialize, Clone, Debug)]
 | 
			
		||||
pub struct Image {
 | 
			
		||||
    pub size: String,
 | 
			
		||||
    #[serde(rename(deserialize = "#text"))]
 | 
			
		||||
    pub text: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Deserialize, Clone, Debug)]
 | 
			
		||||
pub struct TrackAttributes {
 | 
			
		||||
    #[serde(deserialize_with = "bool_from_string")]
 | 
			
		||||
    pub nowplaying: bool,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Track {
 | 
			
		||||
    pub fn is_playing(&self) -> bool {
 | 
			
		||||
        if let Some(attr) = self.clone().attributes {
 | 
			
		||||
            attr.nowplaying
 | 
			
		||||
        } else {
 | 
			
		||||
            false
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Artist {
 | 
			
		||||
    pub fn get_artist_name(&self) -> String {
 | 
			
		||||
        if self.name.is_some() {
 | 
			
		||||
            self.name.clone().unwrap()
 | 
			
		||||
        } else {
 | 
			
		||||
            self.mbid.clone()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										63
									
								
								src/util.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								src/util.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,63 @@
 | 
			
		|||
use serde::de::{self, Deserialize, Deserializer, Unexpected};
 | 
			
		||||
 | 
			
		||||
pub(crate) fn bool_from_string<'de, D>(deserializer: D) -> Result<bool, D::Error>
 | 
			
		||||
where
 | 
			
		||||
    D: Deserializer<'de>,
 | 
			
		||||
{
 | 
			
		||||
    match String::deserialize(deserializer)?.as_str() {
 | 
			
		||||
        "false" => Ok(false),
 | 
			
		||||
        "true" => Ok(true),
 | 
			
		||||
        other => Err(de::Error::invalid_value(
 | 
			
		||||
            Unexpected::Str(other),
 | 
			
		||||
            &"zero or one",
 | 
			
		||||
        )),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub(crate) fn bool_from_string_u8<'de, D>(deserializer: D) -> Result<bool, D::Error>
 | 
			
		||||
where
 | 
			
		||||
    D: Deserializer<'de>,
 | 
			
		||||
{
 | 
			
		||||
    match String::deserialize(deserializer)?.as_str() {
 | 
			
		||||
        "0" => Ok(false),
 | 
			
		||||
        "1" => Ok(true),
 | 
			
		||||
        other => Err(de::Error::invalid_value(
 | 
			
		||||
            Unexpected::Str(other),
 | 
			
		||||
            &"zero or one",
 | 
			
		||||
        )),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub(crate) fn bool_from_u8<'de, D>(deserializer: D) -> Result<bool, D::Error>
 | 
			
		||||
where
 | 
			
		||||
    D: Deserializer<'de>,
 | 
			
		||||
{
 | 
			
		||||
    match u8::deserialize(deserializer)? {
 | 
			
		||||
        0 => Ok(false),
 | 
			
		||||
        1 => Ok(true),
 | 
			
		||||
        other => Err(de::Error::invalid_value(
 | 
			
		||||
            Unexpected::Unsigned(other as u64),
 | 
			
		||||
            &"zero or one",
 | 
			
		||||
        )),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub(crate) fn i32_from_string<'de, D>(deserializer: D) -> Result<i32, D::Error>
 | 
			
		||||
where
 | 
			
		||||
    D: Deserializer<'de>,
 | 
			
		||||
{
 | 
			
		||||
    match String::deserialize(deserializer)?.as_str().parse::<i32>() {
 | 
			
		||||
        Ok(res) => Ok(res),
 | 
			
		||||
        Err(err) => Err(de::Error::invalid_value(Unexpected::Str(&err.to_string()), &"i32")),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub(crate) fn i64_from_string<'de, D>(deserializer: D) -> Result<i64, D::Error>
 | 
			
		||||
where
 | 
			
		||||
    D: Deserializer<'de>,
 | 
			
		||||
{
 | 
			
		||||
    match String::deserialize(deserializer)?.as_str().parse::<i64>() {
 | 
			
		||||
        Ok(res) => Ok(res),
 | 
			
		||||
        Err(err) => Err(de::Error::invalid_value(Unexpected::Str(&err.to_string()), &"i64")),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	Add table
		
		Reference in a new issue