use crate::api::{
LovedTracksClient, RecentTracksClient, TopAlbumsClient, TopArtistsClient, TopTracksClient,
};
use crate::client::{
HttpClient, RateLimitedClient, RateLimiter, ReqwestClient, RetryClient, RetryPolicy,
};
use crate::config::{Config, ConfigBuilder};
use crate::error::Result;
use std::sync::Arc;
pub struct LastFmClient {
http: Arc<dyn HttpClient>,
config: Arc<Config>,
recent_tracks_client: RecentTracksClient,
loved_tracks_client: LovedTracksClient,
top_tracks_client: TopTracksClient,
top_artists_client: TopArtistsClient,
top_albums_client: TopAlbumsClient,
}
impl std::fmt::Debug for LastFmClient {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LastFmClient")
.field("config", &self.config)
.finish_non_exhaustive()
}
}
impl LastFmClient {
#[must_use]
pub fn builder() -> ConfigBuilder {
ConfigBuilder::new()
}
pub fn new() -> Result<Self> {
let config = ConfigBuilder::build_with_defaults()?;
Ok(Self::from_config(config))
}
#[must_use]
pub fn from_config(config: Config) -> Self {
let base_client = ReqwestClient::new();
let retry_policy = RetryPolicy::exponential(config.retry_attempts());
let http: Arc<dyn HttpClient> = if let Some(rate_limit_config) = config.rate_limit() {
let retry_client = RetryClient::new(base_client, retry_policy);
let limiter = Arc::new(RateLimiter::new(
rate_limit_config.max_requests,
rate_limit_config.per_duration,
));
Arc::new(RateLimitedClient::new(retry_client, limiter))
} else {
Arc::new(RetryClient::new(base_client, retry_policy))
};
let config = Arc::new(config);
let recent_tracks_client = RecentTracksClient::new(http.clone(), config.clone());
let loved_tracks_client = LovedTracksClient::new(http.clone(), config.clone());
let top_tracks_client = TopTracksClient::new(http.clone(), config.clone());
let top_artists_client = TopArtistsClient::new(http.clone(), config.clone());
let top_albums_client = TopAlbumsClient::new(http.clone(), config.clone());
Self {
http,
config,
recent_tracks_client,
loved_tracks_client,
top_tracks_client,
top_artists_client,
top_albums_client,
}
}
pub fn with_http(config: Config, http: Arc<dyn HttpClient>) -> Self {
let config = Arc::new(config);
let recent_tracks_client = RecentTracksClient::new(http.clone(), config.clone());
let loved_tracks_client = LovedTracksClient::new(http.clone(), config.clone());
let top_tracks_client = TopTracksClient::new(http.clone(), config.clone());
let top_artists_client = TopArtistsClient::new(http.clone(), config.clone());
let top_albums_client = TopAlbumsClient::new(http.clone(), config.clone());
Self {
http,
config,
recent_tracks_client,
loved_tracks_client,
top_tracks_client,
top_artists_client,
top_albums_client,
}
}
pub fn recent_tracks(
&self,
username: impl Into<String>,
) -> crate::api::RecentTracksRequestBuilder {
self.recent_tracks_client.builder(username)
}
pub fn loved_tracks(
&self,
username: impl Into<String>,
) -> crate::api::LovedTracksRequestBuilder {
self.loved_tracks_client.builder(username)
}
pub fn top_tracks(&self, username: impl Into<String>) -> crate::api::TopTracksRequestBuilder {
self.top_tracks_client.builder(username)
}
pub fn top_artists(&self, username: impl Into<String>) -> crate::api::TopArtistsRequestBuilder {
self.top_artists_client.builder(username)
}
pub fn top_albums(&self, username: impl Into<String>) -> crate::api::TopAlbumsRequestBuilder {
self.top_albums_client.builder(username)
}
pub async fn user_exists(&self, username: impl Into<String>) -> Result<bool> {
use crate::api::constants::BASE_URL;
use crate::error::LastFmError;
use crate::url_builder::{QueryParams, Url};
let username = username.into();
let mut params = QueryParams::new();
params.insert("method".to_string(), "user.getinfo".to_string());
params.insert("user".to_string(), username);
params.insert("api_key".to_string(), self.config.api_key().to_string());
params.insert("format".to_string(), "json".to_string());
let url = Url::new(BASE_URL).add_args(params).build();
match self.http.get(&url).await {
Ok(_) => Ok(true),
Err(LastFmError::Api { error_code, .. }) if error_code == 6 || error_code == 7 => {
Ok(false)
}
Err(e) => Err(e),
}
}
#[must_use]
pub fn config(&self) -> &Config {
&self.config
}
}
impl ConfigBuilder {
pub fn build_client(self) -> Result<LastFmClient> {
self.build().map(LastFmClient::from_config)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use crate::client::MockClient;
#[test]
fn test_client_from_config() {
let config = ConfigBuilder::new().api_key("test_key").build().unwrap();
let client = LastFmClient::from_config(config);
assert_eq!(client.config().api_key(), "test_key");
}
#[test]
fn test_client_with_mock() {
let config = ConfigBuilder::new().api_key("test_key").build().unwrap();
let mock = MockClient::new();
let http = Arc::new(mock);
let client = LastFmClient::with_http(config, http);
assert_eq!(client.config().api_key(), "test_key");
}
#[test]
fn test_builder() {
let client = LastFmClient::builder()
.api_key("test_key")
.build()
.map(LastFmClient::from_config)
.unwrap();
assert_eq!(client.config().api_key(), "test_key");
}
#[tokio::test]
async fn test_user_exists_returns_true() {
use serde_json::json;
let config = ConfigBuilder::new().api_key("test_key").build().unwrap();
let mock = MockClient::new().with_response(
"user.getinfo",
json!({
"user": {
"name": "rj",
"realname": "Richard Jones",
"url": "https://www.last.fm/user/rj"
}
}),
);
let client = LastFmClient::with_http(config, Arc::new(mock));
let result = client.user_exists("rj").await;
assert!(result.is_ok());
assert!(result.unwrap());
}
#[tokio::test]
async fn test_user_exists_returns_false_for_error_6() {
use serde_json::json;
let config = ConfigBuilder::new().api_key("test_key").build().unwrap();
let mock = MockClient::new().with_response(
"user.getinfo",
json!({
"error": 6,
"message": "User not found"
}),
);
let client = LastFmClient::with_http(config, Arc::new(mock));
let result = client.user_exists("nonexistentuser").await;
assert!(result.is_ok());
assert!(!result.unwrap());
}
#[tokio::test]
async fn test_user_exists_returns_false_for_error_7() {
use serde_json::json;
let config = ConfigBuilder::new().api_key("test_key").build().unwrap();
let mock = MockClient::new().with_response(
"user.getinfo",
json!({
"error": 7,
"message": "Invalid resource specified"
}),
);
let client = LastFmClient::with_http(config, Arc::new(mock));
let result = client.user_exists("invaliduser").await;
assert!(result.is_ok());
assert!(!result.unwrap());
}
#[tokio::test]
async fn test_user_exists_propagates_other_api_errors() {
use crate::error::LastFmError;
use serde_json::json;
let config = ConfigBuilder::new().api_key("test_key").build().unwrap();
let mock = MockClient::new().with_response(
"user.getinfo",
json!({
"error": 10,
"message": "Invalid API key"
}),
);
let client = LastFmClient::with_http(config, Arc::new(mock));
let result = client.user_exists("someuser").await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, LastFmError::Api { error_code: 10, .. }));
}
}