use crate::api::LastFmApiClient;
use crate::r#trait::LastFmBaseClient;
use crate::{Album, AlbumPage, Result, Track, TrackPage};
use async_trait::async_trait;
#[cfg_attr(feature = "mock", mockall::automock)]
#[async_trait(?Send)]
pub trait AsyncPaginatedIterator<T> {
async fn next(&mut self) -> Result<Option<T>>;
async fn collect_all(&mut self) -> Result<Vec<T>> {
let mut items = Vec::new();
while let Some(item) = self.next().await? {
items.push(item);
}
Ok(items)
}
async fn take(&mut self, n: usize) -> Result<Vec<T>> {
let mut items = Vec::new();
for _ in 0..n {
match self.next().await? {
Some(item) => items.push(item),
None => break,
}
}
Ok(items)
}
fn current_page(&self) -> u32;
fn total_pages(&self) -> Option<u32> {
None }
}
pub struct ArtistTracksIterator<C: LastFmBaseClient> {
client: C,
artist: String,
album_iterator: Option<ArtistAlbumsIterator<C>>,
current_album_tracks: Option<AlbumTracksIterator<C>>,
track_buffer: Vec<Track>,
finished: bool,
}
#[async_trait(?Send)]
impl<C: LastFmBaseClient + Clone> AsyncPaginatedIterator<Track> for ArtistTracksIterator<C> {
async fn next(&mut self) -> Result<Option<Track>> {
if self.finished {
return Ok(None);
}
while self.track_buffer.is_empty() {
if self.current_album_tracks.is_none() {
if self.album_iterator.is_none() {
self.album_iterator = Some(ArtistAlbumsIterator::new(
self.client.clone(),
self.artist.clone(),
));
}
if let Some(ref mut album_iter) = self.album_iterator {
if let Some(album) = album_iter.next().await? {
log::debug!(
"Processing album '{}' for artist '{}'",
album.name,
self.artist
);
self.current_album_tracks = Some(AlbumTracksIterator::new(
self.client.clone(),
album.name.clone(),
self.artist.clone(),
));
} else {
log::debug!("No more albums for artist '{}'", self.artist);
self.finished = true;
return Ok(None);
}
}
}
if let Some(ref mut album_tracks) = self.current_album_tracks {
if let Some(track) = album_tracks.next().await? {
self.track_buffer.push(track);
} else {
log::debug!(
"Finished processing current album for artist '{}'",
self.artist
);
self.current_album_tracks = None;
}
}
}
Ok(self.track_buffer.pop())
}
fn current_page(&self) -> u32 {
if let Some(ref album_iter) = self.album_iterator {
album_iter.current_page()
} else {
0
}
}
fn total_pages(&self) -> Option<u32> {
if let Some(ref album_iter) = self.album_iterator {
album_iter.total_pages()
} else {
None
}
}
}
impl<C: LastFmBaseClient + Clone> ArtistTracksIterator<C> {
pub fn new(client: C, artist: String) -> Self {
Self {
client,
artist,
album_iterator: None,
current_album_tracks: None,
track_buffer: Vec::new(),
finished: false,
}
}
}
pub struct ArtistTracksDirectIterator<C: LastFmBaseClient> {
client: C,
artist: String,
current_page: u32,
has_more: bool,
buffer: Vec<Track>,
total_pages: Option<u32>,
tracks_yielded: u32,
}
#[async_trait(?Send)]
impl<C: LastFmBaseClient> AsyncPaginatedIterator<Track> for ArtistTracksDirectIterator<C> {
async fn next(&mut self) -> Result<Option<Track>> {
if self.buffer.is_empty() {
if let Some(page) = self.next_page().await? {
self.buffer = page.tracks;
self.buffer.reverse(); }
}
if let Some(track) = self.buffer.pop() {
self.tracks_yielded += 1;
Ok(Some(track))
} else {
Ok(None)
}
}
fn current_page(&self) -> u32 {
self.current_page.saturating_sub(1)
}
fn total_pages(&self) -> Option<u32> {
self.total_pages
}
}
impl<C: LastFmBaseClient> ArtistTracksDirectIterator<C> {
pub fn new(client: C, artist: String) -> Self {
Self {
client,
artist,
current_page: 1,
has_more: true,
buffer: Vec::new(),
total_pages: None,
tracks_yielded: 0,
}
}
pub async fn next_page(&mut self) -> Result<Option<TrackPage>> {
if !self.has_more {
return Ok(None);
}
log::debug!(
"Fetching page {} of {} tracks (yielded {} tracks so far)",
self.current_page,
self.artist,
self.tracks_yielded
);
let page = self
.client
.get_artist_tracks_page(&self.artist, self.current_page)
.await?;
self.has_more = page.has_next_page;
self.current_page += 1;
self.total_pages = page.total_pages;
Ok(Some(page))
}
pub fn total_pages(&self) -> Option<u32> {
self.total_pages
}
}
pub struct ArtistAlbumsIterator<C: LastFmBaseClient> {
client: C,
artist: String,
current_page: u32,
has_more: bool,
buffer: Vec<Album>,
total_pages: Option<u32>,
}
#[async_trait(?Send)]
impl<C: LastFmBaseClient> AsyncPaginatedIterator<Album> for ArtistAlbumsIterator<C> {
async fn next(&mut self) -> Result<Option<Album>> {
if self.buffer.is_empty() {
if let Some(page) = self.next_page().await? {
self.buffer = page.albums;
self.buffer.reverse(); }
}
Ok(self.buffer.pop())
}
fn current_page(&self) -> u32 {
self.current_page.saturating_sub(1)
}
fn total_pages(&self) -> Option<u32> {
self.total_pages
}
}
impl<C: LastFmBaseClient> ArtistAlbumsIterator<C> {
pub fn new(client: C, artist: String) -> Self {
Self {
client,
artist,
current_page: 1,
has_more: true,
buffer: Vec::new(),
total_pages: None,
}
}
pub async fn next_page(&mut self) -> Result<Option<AlbumPage>> {
if !self.has_more {
return Ok(None);
}
let page = self
.client
.get_artist_albums_page(&self.artist, self.current_page)
.await?;
self.has_more = page.has_next_page;
self.current_page += 1;
self.total_pages = page.total_pages;
Ok(Some(page))
}
pub fn total_pages(&self) -> Option<u32> {
self.total_pages
}
}
pub struct RecentTracksIterator<C: LastFmBaseClient> {
client: C,
current_page: u32,
has_more: bool,
buffer: Vec<Track>,
stop_at_timestamp: Option<u64>,
}
#[async_trait(?Send)]
impl<C: LastFmBaseClient> AsyncPaginatedIterator<Track> for RecentTracksIterator<C> {
async fn next(&mut self) -> Result<Option<Track>> {
if self.buffer.is_empty() {
if !self.has_more {
return Ok(None);
}
let page = self
.client
.get_recent_tracks_page(self.current_page)
.await?;
if page.tracks.is_empty() {
self.has_more = false;
return Ok(None);
}
self.has_more = page.has_next_page;
if let Some(stop_timestamp) = self.stop_at_timestamp {
let mut filtered_tracks = Vec::new();
for track in page.tracks {
if let Some(track_timestamp) = track.timestamp {
if track_timestamp <= stop_timestamp {
self.has_more = false;
break;
}
}
filtered_tracks.push(track);
}
self.buffer = filtered_tracks;
} else {
self.buffer = page.tracks;
}
self.buffer.reverse(); self.current_page += 1;
}
Ok(self.buffer.pop())
}
fn current_page(&self) -> u32 {
self.current_page.saturating_sub(1)
}
}
impl<C: LastFmBaseClient> RecentTracksIterator<C> {
pub fn new(client: C) -> Self {
Self::with_starting_page(client, 1)
}
pub fn with_starting_page(client: C, starting_page: u32) -> Self {
let page = std::cmp::max(1, starting_page);
Self {
client,
current_page: page,
has_more: true,
buffer: Vec::new(),
stop_at_timestamp: None,
}
}
pub fn with_stop_timestamp(mut self, timestamp: u64) -> Self {
self.stop_at_timestamp = Some(timestamp);
self
}
}
pub struct ApiRecentTracksIterator<C: LastFmApiClient> {
client: C,
current_page: u32,
has_more: bool,
buffer: Vec<Track>,
stop_at_timestamp: Option<u64>,
total_pages: Option<u32>,
}
#[async_trait(?Send)]
impl<C: LastFmApiClient> AsyncPaginatedIterator<Track> for ApiRecentTracksIterator<C> {
async fn next(&mut self) -> Result<Option<Track>> {
if self.buffer.is_empty() {
if !self.has_more {
return Ok(None);
}
let page = self
.client
.api_get_recent_tracks_page(self.current_page)
.await?;
if page.tracks.is_empty() {
self.has_more = false;
return Ok(None);
}
self.has_more = page.has_next_page;
self.total_pages = page.total_pages;
if let Some(stop_timestamp) = self.stop_at_timestamp {
let mut filtered_tracks = Vec::new();
for track in page.tracks {
if let Some(track_timestamp) = track.timestamp {
if track_timestamp <= stop_timestamp {
self.has_more = false;
break;
}
}
filtered_tracks.push(track);
}
self.buffer = filtered_tracks;
} else {
self.buffer = page.tracks;
}
self.buffer.reverse();
self.current_page += 1;
}
Ok(self.buffer.pop())
}
fn current_page(&self) -> u32 {
self.current_page.saturating_sub(1)
}
fn total_pages(&self) -> Option<u32> {
self.total_pages
}
}
impl<C: LastFmApiClient> ApiRecentTracksIterator<C> {
pub fn new(client: C) -> Self {
Self::with_starting_page(client, 1)
}
pub fn with_starting_page(client: C, starting_page: u32) -> Self {
let page = std::cmp::max(1, starting_page);
Self {
client,
current_page: page,
has_more: true,
buffer: Vec::new(),
stop_at_timestamp: None,
total_pages: None,
}
}
pub fn with_stop_timestamp(mut self, timestamp: u64) -> Self {
self.stop_at_timestamp = Some(timestamp);
self
}
}
pub struct AlbumTracksIterator<C: LastFmBaseClient> {
client: C,
album_name: String,
artist_name: String,
tracks: Option<Vec<Track>>,
index: usize,
}
#[async_trait(?Send)]
impl<C: LastFmBaseClient> AsyncPaginatedIterator<Track> for AlbumTracksIterator<C> {
async fn next(&mut self) -> Result<Option<Track>> {
if self.tracks.is_none() {
let tracks_page = self
.client
.get_album_tracks_page(&self.album_name, &self.artist_name, 1)
.await?;
log::debug!(
"Album '{}' by '{}' has {} tracks: {:?}",
self.album_name,
self.artist_name,
tracks_page.tracks.len(),
tracks_page
.tracks
.iter()
.map(|t| &t.name)
.collect::<Vec<_>>()
);
if tracks_page.tracks.is_empty() {
log::warn!(
"🚨 ZERO TRACKS FOUND for album '{}' by '{}' - investigating...",
self.album_name,
self.artist_name
);
log::debug!("Full TrackPage for empty album: has_next_page={}, page_number={}, total_pages={:?}",
tracks_page.has_next_page, tracks_page.page_number, tracks_page.total_pages);
}
self.tracks = Some(tracks_page.tracks);
}
if let Some(tracks) = &self.tracks {
if self.index < tracks.len() {
let track = tracks[self.index].clone();
self.index += 1;
Ok(Some(track))
} else {
Ok(None)
}
} else {
Ok(None)
}
}
fn current_page(&self) -> u32 {
0
}
}
impl<C: LastFmBaseClient> AlbumTracksIterator<C> {
pub fn new(client: C, album_name: String, artist_name: String) -> Self {
Self {
client,
album_name,
artist_name,
tracks: None,
index: 0,
}
}
}
pub struct SearchTracksIterator<C: LastFmBaseClient> {
client: C,
query: String,
current_page: u32,
has_more: bool,
buffer: Vec<Track>,
total_pages: Option<u32>,
}
#[async_trait(?Send)]
impl<C: LastFmBaseClient> AsyncPaginatedIterator<Track> for SearchTracksIterator<C> {
async fn next(&mut self) -> Result<Option<Track>> {
if self.buffer.is_empty() {
if let Some(page) = self.next_page().await? {
self.buffer = page.tracks;
self.buffer.reverse(); }
}
Ok(self.buffer.pop())
}
fn current_page(&self) -> u32 {
self.current_page.saturating_sub(1)
}
fn total_pages(&self) -> Option<u32> {
self.total_pages
}
}
impl<C: LastFmBaseClient> SearchTracksIterator<C> {
pub fn new(client: C, query: String) -> Self {
Self {
client,
query,
current_page: 1,
has_more: true,
buffer: Vec::new(),
total_pages: None,
}
}
pub fn with_starting_page(client: C, query: String, starting_page: u32) -> Self {
let page = std::cmp::max(1, starting_page);
Self {
client,
query,
current_page: page,
has_more: true,
buffer: Vec::new(),
total_pages: None,
}
}
pub async fn next_page(&mut self) -> Result<Option<TrackPage>> {
if !self.has_more {
return Ok(None);
}
let page = self
.client
.search_tracks_page(&self.query, self.current_page)
.await?;
self.has_more = page.has_next_page;
self.current_page += 1;
self.total_pages = page.total_pages;
Ok(Some(page))
}
pub fn total_pages(&self) -> Option<u32> {
self.total_pages
}
}
pub struct SearchAlbumsIterator<C: LastFmBaseClient> {
client: C,
query: String,
current_page: u32,
has_more: bool,
buffer: Vec<Album>,
total_pages: Option<u32>,
}
#[async_trait(?Send)]
impl<C: LastFmBaseClient> AsyncPaginatedIterator<Album> for SearchAlbumsIterator<C> {
async fn next(&mut self) -> Result<Option<Album>> {
if self.buffer.is_empty() {
if let Some(page) = self.next_page().await? {
self.buffer = page.albums;
self.buffer.reverse(); }
}
Ok(self.buffer.pop())
}
fn current_page(&self) -> u32 {
self.current_page.saturating_sub(1)
}
fn total_pages(&self) -> Option<u32> {
self.total_pages
}
}
impl<C: LastFmBaseClient> SearchAlbumsIterator<C> {
pub fn new(client: C, query: String) -> Self {
Self {
client,
query,
current_page: 1,
has_more: true,
buffer: Vec::new(),
total_pages: None,
}
}
pub fn with_starting_page(client: C, query: String, starting_page: u32) -> Self {
let page = std::cmp::max(1, starting_page);
Self {
client,
query,
current_page: page,
has_more: true,
buffer: Vec::new(),
total_pages: None,
}
}
pub async fn next_page(&mut self) -> Result<Option<AlbumPage>> {
if !self.has_more {
return Ok(None);
}
let page = self
.client
.search_albums_page(&self.query, self.current_page)
.await?;
self.has_more = page.has_next_page;
self.current_page += 1;
self.total_pages = page.total_pages;
Ok(Some(page))
}
pub fn total_pages(&self) -> Option<u32> {
self.total_pages
}
}
pub struct SearchArtistsIterator<C: LastFmBaseClient> {
client: C,
query: String,
current_page: u32,
has_more: bool,
buffer: Vec<crate::Artist>,
total_pages: Option<u32>,
}
#[async_trait(?Send)]
impl<C: LastFmBaseClient> AsyncPaginatedIterator<crate::Artist> for SearchArtistsIterator<C> {
async fn next(&mut self) -> Result<Option<crate::Artist>> {
if self.buffer.is_empty() {
if let Some(page) = self.next_page().await? {
self.buffer = page.artists;
self.buffer.reverse(); }
}
Ok(self.buffer.pop())
}
fn current_page(&self) -> u32 {
self.current_page.saturating_sub(1)
}
fn total_pages(&self) -> Option<u32> {
self.total_pages
}
}
impl<C: LastFmBaseClient> SearchArtistsIterator<C> {
pub fn new(client: C, query: String) -> Self {
Self {
client,
query,
current_page: 1,
has_more: true,
buffer: Vec::new(),
total_pages: None,
}
}
pub fn with_starting_page(client: C, query: String, starting_page: u32) -> Self {
let page = std::cmp::max(1, starting_page);
Self {
client,
query,
current_page: page,
has_more: true,
buffer: Vec::new(),
total_pages: None,
}
}
pub async fn next_page(&mut self) -> Result<Option<crate::ArtistPage>> {
if !self.has_more {
return Ok(None);
}
let page = self
.client
.search_artists_page(&self.query, self.current_page)
.await?;
self.has_more = page.has_next_page;
self.current_page += 1;
self.total_pages = page.total_pages;
Ok(Some(page))
}
pub fn total_pages(&self) -> Option<u32> {
self.total_pages
}
}
pub struct ArtistsIterator<C: LastFmBaseClient> {
client: C,
current_page: u32,
has_more: bool,
buffer: Vec<crate::Artist>,
total_pages: Option<u32>,
}
#[async_trait(?Send)]
impl<C: LastFmBaseClient> AsyncPaginatedIterator<crate::Artist> for ArtistsIterator<C> {
async fn next(&mut self) -> Result<Option<crate::Artist>> {
if self.buffer.is_empty() {
if let Some(page) = self.next_page().await? {
self.buffer = page.artists;
self.buffer.reverse(); }
}
Ok(self.buffer.pop())
}
fn current_page(&self) -> u32 {
self.current_page.saturating_sub(1)
}
fn total_pages(&self) -> Option<u32> {
self.total_pages
}
}
impl<C: LastFmBaseClient> ArtistsIterator<C> {
pub fn new(client: C) -> Self {
Self {
client,
current_page: 1,
has_more: true,
buffer: Vec::new(),
total_pages: None,
}
}
pub fn with_starting_page(client: C, starting_page: u32) -> Self {
let page = std::cmp::max(1, starting_page);
Self {
client,
current_page: page,
has_more: true,
buffer: Vec::new(),
total_pages: None,
}
}
pub async fn next_page(&mut self) -> Result<Option<crate::ArtistPage>> {
if !self.has_more {
return Ok(None);
}
let page = self.client.get_artists_page(self.current_page).await?;
self.has_more = page.has_next_page;
self.current_page += 1;
self.total_pages = page.total_pages;
Ok(Some(page))
}
pub fn total_pages(&self) -> Option<u32> {
self.total_pages
}
}