use crate::api::{
FriendsRequestBuilder, LovedTracksRequestBuilder, PersonalTagsRequestBuilder,
RecentTracksRequestBuilder, TopAlbumsRequestBuilder, TopArtistsRequestBuilder,
TopTagsRequestBuilder, TopTracksRequestBuilder, UserInfoRequestBuilder,
WeeklyAlbumChartRequestBuilder, WeeklyArtistChartRequestBuilder, WeeklyChartListRequestBuilder,
WeeklyTrackChartRequestBuilder,
};
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 {
config: Arc<Config>,
http: Arc<dyn HttpClient>,
}
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))
};
Self {
config: Arc::new(config),
http,
}
}
pub fn with_http(config: Config, http: Arc<dyn HttpClient>) -> Self {
Self {
config: Arc::new(config),
http,
}
}
pub fn recent_tracks(&self, username: impl Into<String>) -> RecentTracksRequestBuilder {
RecentTracksRequestBuilder::new(self.http.clone(), self.config.clone(), username.into())
}
pub fn loved_tracks(&self, username: impl Into<String>) -> LovedTracksRequestBuilder {
LovedTracksRequestBuilder::new(self.http.clone(), self.config.clone(), username.into())
}
pub fn top_tracks(&self, username: impl Into<String>) -> TopTracksRequestBuilder {
TopTracksRequestBuilder::new(self.http.clone(), self.config.clone(), username.into())
}
pub fn top_artists(&self, username: impl Into<String>) -> TopArtistsRequestBuilder {
TopArtistsRequestBuilder::new(self.http.clone(), self.config.clone(), username.into())
}
pub fn top_albums(&self, username: impl Into<String>) -> TopAlbumsRequestBuilder {
TopAlbumsRequestBuilder::new(self.http.clone(), self.config.clone(), username.into())
}
pub fn top_tags(&self, username: impl Into<String>) -> TopTagsRequestBuilder {
TopTagsRequestBuilder::new(self.http.clone(), self.config.clone(), username.into())
}
pub fn weekly_chart_list(&self, username: impl Into<String>) -> WeeklyChartListRequestBuilder {
WeeklyChartListRequestBuilder::new(self.http.clone(), self.config.clone(), username.into())
}
pub fn weekly_track_chart(
&self,
username: impl Into<String>,
) -> WeeklyTrackChartRequestBuilder {
WeeklyTrackChartRequestBuilder::new(self.http.clone(), self.config.clone(), username.into())
}
pub fn weekly_artist_chart(
&self,
username: impl Into<String>,
) -> WeeklyArtistChartRequestBuilder {
WeeklyArtistChartRequestBuilder::new(
self.http.clone(),
self.config.clone(),
username.into(),
)
}
pub fn weekly_album_chart(
&self,
username: impl Into<String>,
) -> WeeklyAlbumChartRequestBuilder {
WeeklyAlbumChartRequestBuilder::new(self.http.clone(), self.config.clone(), username.into())
}
pub fn friends(&self, username: impl Into<String>) -> FriendsRequestBuilder {
FriendsRequestBuilder::new(self.http.clone(), self.config.clone(), username.into())
}
pub fn personal_tags(
&self,
username: impl Into<String>,
tag: impl Into<String>,
) -> PersonalTagsRequestBuilder {
PersonalTagsRequestBuilder::new(
self.http.clone(),
self.config.clone(),
username.into(),
tag.into(),
)
}
pub fn user_info(&self, username: impl Into<String>) -> UserInfoRequestBuilder {
UserInfoRequestBuilder::new(self.http.clone(), self.config.clone(), username.into())
}
pub async fn user_exists(&self, username: impl Into<String>) -> Result<bool> {
use crate::error::LastFmError;
match self.user_info(username).fetch().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",
"country": "UK",
"age": "0",
"gender": "m",
"subscriber": "0",
"playcount": "12345",
"playlists": "0",
"registered": { "unixtime": "1104874958", "#text": "2005-01-05 00:00" }
}
}),
);
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, .. }));
}
}