use crate::analytics::AnalysisHandler;
use crate::client::HttpClient;
use crate::config::Config;
use crate::error::Result;
use crate::file_handler::{FileFormat, FileHandler};
use crate::types::{
RecentTrack, RecentTrackExtended, TrackLimit, UserRecentTracks, UserRecentTracksExtended,
};
use crate::url_builder::QueryParams;
use serde::de::DeserializeOwned;
use std::sync::Arc;
use super::fetch_utils::{TrackContainer, fetch_tracks};
pub struct RecentTracksClient {
http: Arc<dyn HttpClient>,
config: Arc<Config>,
}
impl RecentTracksClient {
pub fn new(http: Arc<dyn HttpClient>, config: Arc<Config>) -> Self {
Self { http, config }
}
pub fn builder(&self, username: impl Into<String>) -> RecentTracksRequestBuilder {
RecentTracksRequestBuilder::new(self.http.clone(), self.config.clone(), username.into())
}
}
pub struct RecentTracksRequestBuilder {
http: Arc<dyn HttpClient>,
config: Arc<Config>,
username: String,
limit: Option<u32>,
from: Option<i64>,
to: Option<i64>,
extended: bool,
}
impl RecentTracksRequestBuilder {
fn new(http: Arc<dyn HttpClient>, config: Arc<Config>, username: String) -> Self {
Self {
http,
config,
username,
limit: None,
from: None,
to: None,
extended: false,
}
}
#[must_use]
pub fn limit(mut self, limit: u32) -> Self {
self.limit = Some(limit);
self
}
#[must_use]
pub fn unlimited(mut self) -> Self {
self.limit = None;
self
}
#[must_use]
pub fn since(mut self, timestamp: i64) -> Self {
self.from = Some(timestamp);
self
}
#[must_use]
pub fn between(mut self, from: i64, to: i64) -> Self {
self.from = Some(from);
self.to = Some(to);
self
}
#[must_use]
pub fn extended(mut self, extended: bool) -> Self {
self.extended = extended;
self
}
pub async fn fetch(self) -> Result<Vec<RecentTrack>> {
if let (Some(from), Some(to)) = (self.from, self.to)
&& to <= from
{
return Err(crate::error::LastFmError::Config(format!(
"Invalid date range: 'to' timestamp ({to}) must be greater than 'from' timestamp ({from})"
)));
}
let mut params = self.build_params();
if self.extended {
params.insert("extended".to_string(), "1".to_string());
}
let limit = self
.limit
.map_or(TrackLimit::Unlimited, TrackLimit::Limited);
self.fetch_tracks::<UserRecentTracks>(limit, params).await
}
pub async fn fetch_extended(self) -> Result<Vec<RecentTrackExtended>> {
if let (Some(from), Some(to)) = (self.from, self.to)
&& to <= from
{
return Err(crate::error::LastFmError::Config(format!(
"Invalid date range: 'to' timestamp ({to}) must be greater than 'from' timestamp ({from})"
)));
}
let mut params = self.build_params();
params.insert("extended".to_string(), "1".to_string());
let limit = self
.limit
.map_or(TrackLimit::Unlimited, TrackLimit::Limited);
self.fetch_tracks_extended::<UserRecentTracksExtended>(limit, params)
.await
}
pub async fn fetch_and_save(self, format: FileFormat, filename_prefix: &str) -> Result<String> {
let tracks = self.fetch().await?;
tracing::info!("Saving {} recent tracks to file", tracks.len());
let filename = FileHandler::save(&tracks, &format, filename_prefix)
.map_err(crate::error::LastFmError::Io)?;
Ok(filename)
}
pub async fn fetch_extended_and_save(
self,
format: FileFormat,
filename_prefix: &str,
) -> Result<String> {
let tracks = self.fetch_extended().await?;
tracing::info!("Saving {} recent tracks (extended) to file", tracks.len());
let filename = FileHandler::save(&tracks, &format, filename_prefix)
.map_err(crate::error::LastFmError::Io)?;
Ok(filename)
}
pub async fn analyze(self, threshold: usize) -> Result<crate::analytics::TrackStats> {
let tracks = self.fetch().await?;
Ok(AnalysisHandler::analyze_tracks(&tracks, threshold))
}
pub async fn analyze_and_print(self, threshold: usize) -> Result<()> {
let stats = self.analyze(threshold).await?;
AnalysisHandler::print_analysis(&stats);
Ok(())
}
pub async fn check_currently_playing(self) -> Result<Option<RecentTrack>> {
let tracks = self.limit(1).fetch().await?;
Ok(tracks.first().and_then(|track| {
if track
.attr
.as_ref()
.is_some_and(|val| val.nowplaying == "true")
{
Some(track.clone())
} else {
None
}
}))
}
fn build_params(&self) -> QueryParams {
let mut params = QueryParams::new();
if let Some(from_timestamp) = self.from {
params.insert("from".to_string(), from_timestamp.to_string());
}
if let Some(to_timestamp) = self.to {
params.insert("to".to_string(), to_timestamp.to_string());
}
params
}
async fn fetch_tracks<T>(
&self,
limit: TrackLimit,
additional_params: QueryParams,
) -> Result<Vec<RecentTrack>>
where
T: DeserializeOwned + TrackContainer<TrackType = RecentTrack>,
{
fetch_tracks::<RecentTrack, T>(
self.http.clone(),
self.config.clone(),
self.username.clone(),
"user.getrecenttracks",
limit,
additional_params,
)
.await
}
async fn fetch_tracks_extended<T>(
&self,
limit: TrackLimit,
additional_params: QueryParams,
) -> Result<Vec<RecentTrackExtended>>
where
T: DeserializeOwned + TrackContainer<TrackType = RecentTrackExtended>,
{
fetch_tracks::<RecentTrackExtended, T>(
self.http.clone(),
self.config.clone(),
self.username.clone(),
"user.getrecenttracks",
limit,
additional_params,
)
.await
}
}
impl TrackContainer for UserRecentTracks {
type TrackType = RecentTrack;
fn total_tracks(&self) -> u32 {
self.recenttracks.attr.total
}
fn tracks(self) -> Vec<Self::TrackType> {
self.recenttracks.track
}
}
impl TrackContainer for UserRecentTracksExtended {
type TrackType = RecentTrackExtended;
fn total_tracks(&self) -> u32 {
self.recenttracks.attr.total
}
fn tracks(self) -> Vec<Self::TrackType> {
self.recenttracks.track
}
}