Skip to main content

lastfm_edit/
iterator.rs

1use crate::api::LastFmApiClient;
2use crate::r#trait::LastFmBaseClient;
3use crate::{Album, AlbumPage, Result, Track, TrackPage};
4
5use async_trait::async_trait;
6
7/// Async iterator trait for paginated Last.fm data.
8///
9/// This trait provides a common interface for iterating over paginated data from Last.fm,
10/// such as tracks, albums, and recent scrobbles. All iterators implement efficient streaming
11/// with automatic pagination and built-in rate limiting.
12#[cfg_attr(feature = "mock", mockall::automock)]
13#[async_trait(?Send)]
14pub trait AsyncPaginatedIterator<T> {
15    /// Fetch the next item from the iterator.
16    ///
17    /// This method automatically handles pagination, fetching new pages as needed.
18    /// Returns `None` when there are no more items available.
19    ///
20    /// # Returns
21    ///
22    /// - `Ok(Some(item))` - Next item in the sequence
23    /// - `Ok(None)` - No more items available
24    /// - `Err(...)` - Network or parsing error occurred
25    async fn next(&mut self) -> Result<Option<T>>;
26
27    /// Collect all remaining items into a Vec.
28    ///
29    /// **Warning**: This method will fetch ALL remaining pages, which could be
30    /// many thousands of items for large libraries. Use [`take`](Self::take) for
31    /// safer bounded collection.
32    async fn collect_all(&mut self) -> Result<Vec<T>> {
33        let mut items = Vec::new();
34        while let Some(item) = self.next().await? {
35            items.push(item);
36        }
37        Ok(items)
38    }
39
40    /// Take up to n items from the iterator.
41    ///
42    /// This is the recommended way to collect a bounded number of items
43    /// from potentially large datasets.
44    ///
45    /// # Arguments
46    ///
47    /// * `n` - Maximum number of items to collect
48    async fn take(&mut self, n: usize) -> Result<Vec<T>> {
49        let mut items = Vec::new();
50        for _ in 0..n {
51            match self.next().await? {
52                Some(item) => items.push(item),
53                None => break,
54            }
55        }
56        Ok(items)
57    }
58
59    /// Get the current page number (0-indexed).
60    ///
61    /// Returns the page number of the most recently fetched page.
62    fn current_page(&self) -> u32;
63
64    /// Get the total number of pages, if known.
65    ///
66    /// Returns `Some(n)` if the total page count is known, `None` otherwise.
67    /// This information may not be available until at least one page has been fetched.
68    fn total_pages(&self) -> Option<u32> {
69        None // Default implementation returns None
70    }
71}
72
73/// Iterator for browsing an artist's tracks from a user's library.
74///
75/// This iterator provides access to all tracks by a specific artist
76/// in the authenticated user's Last.fm library. Unlike the basic track listing,
77/// this iterator fetches tracks by iterating through the artist's albums first,
78/// which provides complete album information for each track.
79///
80/// The iterator loads albums and their tracks as needed and handles rate limiting
81/// automatically to be respectful to Last.fm's servers.
82pub struct ArtistTracksIterator<C: LastFmBaseClient> {
83    client: C,
84    artist: String,
85    album_iterator: Option<ArtistAlbumsIterator<C>>,
86    current_album_tracks: Option<AlbumTracksIterator<C>>,
87    track_buffer: Vec<Track>,
88    finished: bool,
89}
90
91#[async_trait(?Send)]
92impl<C: LastFmBaseClient + Clone> AsyncPaginatedIterator<Track> for ArtistTracksIterator<C> {
93    async fn next(&mut self) -> Result<Option<Track>> {
94        // If we're finished, return None
95        if self.finished {
96            return Ok(None);
97        }
98
99        // If track buffer is empty, try to get more tracks
100        while self.track_buffer.is_empty() {
101            // If we don't have a current album tracks iterator, get the next album
102            if self.current_album_tracks.is_none() {
103                // Initialize album iterator if needed
104                if self.album_iterator.is_none() {
105                    self.album_iterator = Some(ArtistAlbumsIterator::new(
106                        self.client.clone(),
107                        self.artist.clone(),
108                    ));
109                }
110
111                // Get next album
112                if let Some(ref mut album_iter) = self.album_iterator {
113                    if let Some(album) = album_iter.next().await? {
114                        log::debug!(
115                            "Processing album '{}' for artist '{}'",
116                            album.name,
117                            self.artist
118                        );
119                        // Create album tracks iterator for this album
120                        self.current_album_tracks = Some(AlbumTracksIterator::new(
121                            self.client.clone(),
122                            album.name.clone(),
123                            self.artist.clone(),
124                        ));
125                    } else {
126                        // No more albums, we're done
127                        log::debug!("No more albums for artist '{}'", self.artist);
128                        self.finished = true;
129                        return Ok(None);
130                    }
131                }
132            }
133
134            // Get tracks from current album
135            if let Some(ref mut album_tracks) = self.current_album_tracks {
136                if let Some(track) = album_tracks.next().await? {
137                    self.track_buffer.push(track);
138                } else {
139                    // This album is exhausted, move to next album
140                    log::debug!(
141                        "Finished processing current album for artist '{}'",
142                        self.artist
143                    );
144                    self.current_album_tracks = None;
145                    // Continue the loop to try getting the next album
146                }
147            }
148        }
149
150        // Return the next track from our buffer
151        Ok(self.track_buffer.pop())
152    }
153
154    fn current_page(&self) -> u32 {
155        // Since we're iterating through albums, return the album iterator's current page
156        if let Some(ref album_iter) = self.album_iterator {
157            album_iter.current_page()
158        } else {
159            0
160        }
161    }
162
163    fn total_pages(&self) -> Option<u32> {
164        // Since we're iterating through albums, return the album iterator's total pages
165        if let Some(ref album_iter) = self.album_iterator {
166            album_iter.total_pages()
167        } else {
168            None
169        }
170    }
171}
172
173impl<C: LastFmBaseClient + Clone> ArtistTracksIterator<C> {
174    /// Create a new artist tracks iterator.
175    ///
176    /// This is typically called via [`LastFmBaseClient::artist_tracks`](crate::LastFmBaseClient::artist_tracks).
177    pub fn new(client: C, artist: String) -> Self {
178        Self {
179            client,
180            artist,
181            album_iterator: None,
182            current_album_tracks: None,
183            track_buffer: Vec::new(),
184            finished: false,
185        }
186    }
187}
188
189/// Iterator for browsing an artist's tracks directly using the paginated artist tracks endpoint.
190///
191/// This iterator provides access to all tracks by a specific artist
192/// in the authenticated user's Last.fm library by directly using the
193/// `/user/{username}/library/music/{artist}/+tracks` endpoint with pagination.
194/// This is more efficient than the album-based approach as it doesn't need to
195/// iterate through albums first.
196pub struct ArtistTracksDirectIterator<C: LastFmBaseClient> {
197    client: C,
198    artist: String,
199    current_page: u32,
200    has_more: bool,
201    buffer: Vec<Track>,
202    total_pages: Option<u32>,
203    tracks_yielded: u32,
204}
205
206#[async_trait(?Send)]
207impl<C: LastFmBaseClient> AsyncPaginatedIterator<Track> for ArtistTracksDirectIterator<C> {
208    async fn next(&mut self) -> Result<Option<Track>> {
209        // If buffer is empty, try to load next page
210        if self.buffer.is_empty() {
211            if let Some(page) = self.next_page().await? {
212                self.buffer = page.tracks;
213                self.buffer.reverse(); // Reverse so we can pop from end efficiently
214            }
215        }
216
217        if let Some(track) = self.buffer.pop() {
218            self.tracks_yielded += 1;
219            Ok(Some(track))
220        } else {
221            Ok(None)
222        }
223    }
224
225    fn current_page(&self) -> u32 {
226        self.current_page.saturating_sub(1)
227    }
228
229    fn total_pages(&self) -> Option<u32> {
230        self.total_pages
231    }
232}
233
234impl<C: LastFmBaseClient> ArtistTracksDirectIterator<C> {
235    /// Create a new direct artist tracks iterator.
236    ///
237    /// This is typically called via [`LastFmBaseClient::artist_tracks_direct`](crate::LastFmBaseClient::artist_tracks_direct).
238    pub fn new(client: C, artist: String) -> Self {
239        Self {
240            client,
241            artist,
242            current_page: 1,
243            has_more: true,
244            buffer: Vec::new(),
245            total_pages: None,
246            tracks_yielded: 0,
247        }
248    }
249
250    /// Fetch the next page of tracks.
251    ///
252    /// This method handles pagination automatically and includes rate limiting.
253    pub async fn next_page(&mut self) -> Result<Option<TrackPage>> {
254        if !self.has_more {
255            return Ok(None);
256        }
257
258        log::debug!(
259            "Fetching page {} of {} tracks (yielded {} tracks so far)",
260            self.current_page,
261            self.artist,
262            self.tracks_yielded
263        );
264
265        let page = self
266            .client
267            .get_artist_tracks_page(&self.artist, self.current_page)
268            .await?;
269
270        self.has_more = page.has_next_page;
271        self.current_page += 1;
272        self.total_pages = page.total_pages;
273
274        Ok(Some(page))
275    }
276
277    /// Get the total number of pages, if known.
278    ///
279    /// Returns `None` until at least one page has been fetched.
280    pub fn total_pages(&self) -> Option<u32> {
281        self.total_pages
282    }
283}
284
285/// Iterator for browsing an artist's albums from a user's library.
286///
287/// This iterator provides paginated access to all albums by a specific artist
288/// in the authenticated user's Last.fm library, ordered by play count.
289pub struct ArtistAlbumsIterator<C: LastFmBaseClient> {
290    client: C,
291    artist: String,
292    current_page: u32,
293    has_more: bool,
294    buffer: Vec<Album>,
295    total_pages: Option<u32>,
296}
297
298#[async_trait(?Send)]
299impl<C: LastFmBaseClient> AsyncPaginatedIterator<Album> for ArtistAlbumsIterator<C> {
300    async fn next(&mut self) -> Result<Option<Album>> {
301        // If buffer is empty, try to load next page
302        if self.buffer.is_empty() {
303            if let Some(page) = self.next_page().await? {
304                self.buffer = page.albums;
305                self.buffer.reverse(); // Reverse so we can pop from end efficiently
306            }
307        }
308
309        Ok(self.buffer.pop())
310    }
311
312    fn current_page(&self) -> u32 {
313        self.current_page.saturating_sub(1)
314    }
315
316    fn total_pages(&self) -> Option<u32> {
317        self.total_pages
318    }
319}
320
321impl<C: LastFmBaseClient> ArtistAlbumsIterator<C> {
322    /// Create a new artist albums iterator.
323    ///
324    /// This is typically called via [`LastFmBaseClient::artist_albums`](crate::LastFmBaseClient::artist_albums).
325    pub fn new(client: C, artist: String) -> Self {
326        Self {
327            client,
328            artist,
329            current_page: 1,
330            has_more: true,
331            buffer: Vec::new(),
332            total_pages: None,
333        }
334    }
335
336    /// Fetch the next page of albums.
337    ///
338    /// This method handles pagination automatically and includes rate limiting.
339    pub async fn next_page(&mut self) -> Result<Option<AlbumPage>> {
340        if !self.has_more {
341            return Ok(None);
342        }
343
344        let page = self
345            .client
346            .get_artist_albums_page(&self.artist, self.current_page)
347            .await?;
348
349        self.has_more = page.has_next_page;
350        self.current_page += 1;
351        self.total_pages = page.total_pages;
352
353        Ok(Some(page))
354    }
355
356    /// Get the total number of pages, if known.
357    ///
358    /// Returns `None` until at least one page has been fetched.
359    pub fn total_pages(&self) -> Option<u32> {
360        self.total_pages
361    }
362}
363
364/// Iterator for browsing a user's recent tracks/scrobbles.
365///
366/// This iterator provides access to the user's recent listening history with timestamps,
367/// which is essential for finding tracks that can be edited. It supports optional
368/// timestamp-based filtering to avoid reprocessing old data.
369pub struct RecentTracksIterator<C: LastFmBaseClient> {
370    client: C,
371    current_page: u32,
372    has_more: bool,
373    buffer: Vec<Track>,
374    stop_at_timestamp: Option<u64>,
375}
376
377#[async_trait(?Send)]
378impl<C: LastFmBaseClient> AsyncPaginatedIterator<Track> for RecentTracksIterator<C> {
379    async fn next(&mut self) -> Result<Option<Track>> {
380        // If buffer is empty, try to load next page
381        if self.buffer.is_empty() {
382            if !self.has_more {
383                return Ok(None);
384            }
385
386            let page = self
387                .client
388                .get_recent_tracks_page(self.current_page)
389                .await?;
390
391            if page.tracks.is_empty() {
392                self.has_more = false;
393                return Ok(None);
394            }
395
396            self.has_more = page.has_next_page;
397
398            // Check if we should stop based on timestamp
399            if let Some(stop_timestamp) = self.stop_at_timestamp {
400                let mut filtered_tracks = Vec::new();
401                for track in page.tracks {
402                    if let Some(track_timestamp) = track.timestamp {
403                        if track_timestamp <= stop_timestamp {
404                            self.has_more = false;
405                            break;
406                        }
407                    }
408                    filtered_tracks.push(track);
409                }
410                self.buffer = filtered_tracks;
411            } else {
412                self.buffer = page.tracks;
413            }
414
415            self.buffer.reverse(); // Reverse so we can pop from end efficiently
416            self.current_page += 1;
417        }
418
419        Ok(self.buffer.pop())
420    }
421
422    fn current_page(&self) -> u32 {
423        self.current_page.saturating_sub(1)
424    }
425}
426
427impl<C: LastFmBaseClient> RecentTracksIterator<C> {
428    /// Create a new recent tracks iterator starting from page 1.
429    ///
430    /// This is typically called via [`LastFmBaseClient::recent_tracks`](crate::LastFmBaseClient::recent_tracks).
431    pub fn new(client: C) -> Self {
432        Self::with_starting_page(client, 1)
433    }
434
435    /// Create a new recent tracks iterator starting from a specific page.
436    ///
437    /// This allows resuming pagination from an arbitrary page, useful for
438    /// continuing from where a previous iteration left off.
439    ///
440    /// # Arguments
441    ///
442    /// * `client` - The LastFmBaseClient to use for API calls
443    /// * `starting_page` - The page number to start from (1-indexed)
444    pub fn with_starting_page(client: C, starting_page: u32) -> Self {
445        let page = std::cmp::max(1, starting_page);
446        Self {
447            client,
448            current_page: page,
449            has_more: true,
450            buffer: Vec::new(),
451            stop_at_timestamp: None,
452        }
453    }
454
455    /// Set a timestamp to stop iteration at.
456    ///
457    /// When this is set, the iterator will stop returning tracks once it encounters
458    /// a track with a timestamp less than or equal to the specified value. This is
459    /// useful for incremental processing to avoid reprocessing old data.
460    ///
461    /// # Arguments
462    ///
463    /// * `timestamp` - Unix timestamp to stop at
464    pub fn with_stop_timestamp(mut self, timestamp: u64) -> Self {
465        self.stop_at_timestamp = Some(timestamp);
466        self
467    }
468}
469
470/// Iterator for browsing a user's recent tracks via the Last.fm JSON API.
471///
472/// This iterator uses the `user.getRecentTracks` API endpoint which supports
473/// up to 200 items per page and is less aggressively rate-limited than web scraping.
474/// It supports optional timestamp-based filtering identical to [`RecentTracksIterator`].
475pub struct ApiRecentTracksIterator<C: LastFmApiClient> {
476    client: C,
477    current_page: u32,
478    has_more: bool,
479    buffer: Vec<Track>,
480    stop_at_timestamp: Option<u64>,
481    total_pages: Option<u32>,
482}
483
484#[async_trait(?Send)]
485impl<C: LastFmApiClient> AsyncPaginatedIterator<Track> for ApiRecentTracksIterator<C> {
486    async fn next(&mut self) -> Result<Option<Track>> {
487        if self.buffer.is_empty() {
488            if !self.has_more {
489                return Ok(None);
490            }
491
492            let page = self
493                .client
494                .api_get_recent_tracks_page(self.current_page)
495                .await?;
496
497            if page.tracks.is_empty() {
498                self.has_more = false;
499                return Ok(None);
500            }
501
502            self.has_more = page.has_next_page;
503            self.total_pages = page.total_pages;
504
505            if let Some(stop_timestamp) = self.stop_at_timestamp {
506                let mut filtered_tracks = Vec::new();
507                for track in page.tracks {
508                    if let Some(track_timestamp) = track.timestamp {
509                        if track_timestamp <= stop_timestamp {
510                            self.has_more = false;
511                            break;
512                        }
513                    }
514                    filtered_tracks.push(track);
515                }
516                self.buffer = filtered_tracks;
517            } else {
518                self.buffer = page.tracks;
519            }
520
521            self.buffer.reverse();
522            self.current_page += 1;
523        }
524
525        Ok(self.buffer.pop())
526    }
527
528    fn current_page(&self) -> u32 {
529        self.current_page.saturating_sub(1)
530    }
531
532    fn total_pages(&self) -> Option<u32> {
533        self.total_pages
534    }
535}
536
537impl<C: LastFmApiClient> ApiRecentTracksIterator<C> {
538    pub fn new(client: C) -> Self {
539        Self::with_starting_page(client, 1)
540    }
541
542    pub fn with_starting_page(client: C, starting_page: u32) -> Self {
543        let page = std::cmp::max(1, starting_page);
544        Self {
545            client,
546            current_page: page,
547            has_more: true,
548            buffer: Vec::new(),
549            stop_at_timestamp: None,
550            total_pages: None,
551        }
552    }
553
554    pub fn with_stop_timestamp(mut self, timestamp: u64) -> Self {
555        self.stop_at_timestamp = Some(timestamp);
556        self
557    }
558}
559
560/// Iterator for browsing tracks in a specific album from a user's library.
561///
562/// This iterator provides access to all tracks in a specific album by an artist
563/// in the authenticated user's Last.fm library. Unlike paginated iterators,
564/// this loads tracks once and iterates through them.
565pub struct AlbumTracksIterator<C: LastFmBaseClient> {
566    client: C,
567    album_name: String,
568    artist_name: String,
569    tracks: Option<Vec<Track>>,
570    index: usize,
571}
572
573#[async_trait(?Send)]
574impl<C: LastFmBaseClient> AsyncPaginatedIterator<Track> for AlbumTracksIterator<C> {
575    async fn next(&mut self) -> Result<Option<Track>> {
576        // Load tracks if not already loaded
577        if self.tracks.is_none() {
578            // Use get_album_tracks_page instead of get_album_tracks to avoid infinite recursion
579            let tracks_page = self
580                .client
581                .get_album_tracks_page(&self.album_name, &self.artist_name, 1)
582                .await?;
583            log::debug!(
584                "Album '{}' by '{}' has {} tracks: {:?}",
585                self.album_name,
586                self.artist_name,
587                tracks_page.tracks.len(),
588                tracks_page
589                    .tracks
590                    .iter()
591                    .map(|t| &t.name)
592                    .collect::<Vec<_>>()
593            );
594
595            if tracks_page.tracks.is_empty() {
596                log::warn!(
597                    "🚨 ZERO TRACKS FOUND for album '{}' by '{}' - investigating...",
598                    self.album_name,
599                    self.artist_name
600                );
601                log::debug!("Full TrackPage for empty album: has_next_page={}, page_number={}, total_pages={:?}",
602                           tracks_page.has_next_page, tracks_page.page_number, tracks_page.total_pages);
603            }
604            self.tracks = Some(tracks_page.tracks);
605        }
606
607        // Return next track
608        if let Some(tracks) = &self.tracks {
609            if self.index < tracks.len() {
610                let track = tracks[self.index].clone();
611                self.index += 1;
612                Ok(Some(track))
613            } else {
614                Ok(None)
615            }
616        } else {
617            Ok(None)
618        }
619    }
620
621    fn current_page(&self) -> u32 {
622        // Album tracks don't have pages, so return 0
623        0
624    }
625}
626
627impl<C: LastFmBaseClient> AlbumTracksIterator<C> {
628    /// Create a new album tracks iterator.
629    ///
630    /// This is typically called via [`LastFmBaseClient::album_tracks`](crate::LastFmBaseClient::album_tracks).
631    pub fn new(client: C, album_name: String, artist_name: String) -> Self {
632        Self {
633            client,
634            album_name,
635            artist_name,
636            tracks: None,
637            index: 0,
638        }
639    }
640}
641
642/// Iterator for searching tracks in the user's library.
643///
644/// This iterator provides paginated access to tracks that match a search query
645/// in the authenticated user's Last.fm library, using Last.fm's built-in search functionality.
646pub struct SearchTracksIterator<C: LastFmBaseClient> {
647    client: C,
648    query: String,
649    current_page: u32,
650    has_more: bool,
651    buffer: Vec<Track>,
652    total_pages: Option<u32>,
653}
654
655#[async_trait(?Send)]
656impl<C: LastFmBaseClient> AsyncPaginatedIterator<Track> for SearchTracksIterator<C> {
657    async fn next(&mut self) -> Result<Option<Track>> {
658        // If buffer is empty, try to load next page
659        if self.buffer.is_empty() {
660            if let Some(page) = self.next_page().await? {
661                self.buffer = page.tracks;
662                self.buffer.reverse(); // Reverse so we can pop from end efficiently
663            }
664        }
665
666        Ok(self.buffer.pop())
667    }
668
669    fn current_page(&self) -> u32 {
670        self.current_page.saturating_sub(1)
671    }
672
673    fn total_pages(&self) -> Option<u32> {
674        self.total_pages
675    }
676}
677
678impl<C: LastFmBaseClient> SearchTracksIterator<C> {
679    /// Create a new search tracks iterator.
680    ///
681    /// This is typically called via [`LastFmBaseClient::search_tracks`](crate::LastFmBaseClient::search_tracks).
682    pub fn new(client: C, query: String) -> Self {
683        Self {
684            client,
685            query,
686            current_page: 1,
687            has_more: true,
688            buffer: Vec::new(),
689            total_pages: None,
690        }
691    }
692
693    /// Create a new search tracks iterator starting from a specific page.
694    ///
695    /// This is useful for implementing offset functionality efficiently by starting
696    /// at the appropriate page rather than iterating through all previous pages.
697    pub fn with_starting_page(client: C, query: String, starting_page: u32) -> Self {
698        let page = std::cmp::max(1, starting_page);
699        Self {
700            client,
701            query,
702            current_page: page,
703            has_more: true,
704            buffer: Vec::new(),
705            total_pages: None,
706        }
707    }
708
709    /// Fetch the next page of search results.
710    ///
711    /// This method handles pagination automatically and includes rate limiting
712    /// to be respectful to Last.fm's servers.
713    pub async fn next_page(&mut self) -> Result<Option<TrackPage>> {
714        if !self.has_more {
715            return Ok(None);
716        }
717
718        let page = self
719            .client
720            .search_tracks_page(&self.query, self.current_page)
721            .await?;
722
723        self.has_more = page.has_next_page;
724        self.current_page += 1;
725        self.total_pages = page.total_pages;
726
727        Ok(Some(page))
728    }
729
730    /// Get the total number of pages, if known.
731    ///
732    /// Returns `None` until at least one page has been fetched.
733    pub fn total_pages(&self) -> Option<u32> {
734        self.total_pages
735    }
736}
737
738/// Iterator for searching albums in the user's library.
739///
740/// This iterator provides paginated access to albums that match a search query
741/// in the authenticated user's Last.fm library, using Last.fm's built-in search functionality.
742///
743/// # Examples
744pub struct SearchAlbumsIterator<C: LastFmBaseClient> {
745    client: C,
746    query: String,
747    current_page: u32,
748    has_more: bool,
749    buffer: Vec<Album>,
750    total_pages: Option<u32>,
751}
752
753#[async_trait(?Send)]
754impl<C: LastFmBaseClient> AsyncPaginatedIterator<Album> for SearchAlbumsIterator<C> {
755    async fn next(&mut self) -> Result<Option<Album>> {
756        // If buffer is empty, try to load next page
757        if self.buffer.is_empty() {
758            if let Some(page) = self.next_page().await? {
759                self.buffer = page.albums;
760                self.buffer.reverse(); // Reverse so we can pop from end efficiently
761            }
762        }
763
764        Ok(self.buffer.pop())
765    }
766
767    fn current_page(&self) -> u32 {
768        self.current_page.saturating_sub(1)
769    }
770
771    fn total_pages(&self) -> Option<u32> {
772        self.total_pages
773    }
774}
775
776impl<C: LastFmBaseClient> SearchAlbumsIterator<C> {
777    /// Create a new search albums iterator.
778    ///
779    /// This is typically called via [`LastFmBaseClient::search_albums`](crate::LastFmBaseClient::search_albums).
780    pub fn new(client: C, query: String) -> Self {
781        Self {
782            client,
783            query,
784            current_page: 1,
785            has_more: true,
786            buffer: Vec::new(),
787            total_pages: None,
788        }
789    }
790
791    /// Create a new search albums iterator starting from a specific page.
792    ///
793    /// This is useful for implementing offset functionality efficiently by starting
794    /// at the appropriate page rather than iterating through all previous pages.
795    pub fn with_starting_page(client: C, query: String, starting_page: u32) -> Self {
796        let page = std::cmp::max(1, starting_page);
797        Self {
798            client,
799            query,
800            current_page: page,
801            has_more: true,
802            buffer: Vec::new(),
803            total_pages: None,
804        }
805    }
806
807    /// Fetch the next page of search results.
808    ///
809    /// This method handles pagination automatically and includes rate limiting
810    /// to be respectful to Last.fm's servers.
811    pub async fn next_page(&mut self) -> Result<Option<AlbumPage>> {
812        if !self.has_more {
813            return Ok(None);
814        }
815
816        let page = self
817            .client
818            .search_albums_page(&self.query, self.current_page)
819            .await?;
820
821        self.has_more = page.has_next_page;
822        self.current_page += 1;
823        self.total_pages = page.total_pages;
824
825        Ok(Some(page))
826    }
827
828    /// Get the total number of pages, if known.
829    ///
830    /// Returns `None` until at least one page has been fetched.
831    pub fn total_pages(&self) -> Option<u32> {
832        self.total_pages
833    }
834}
835
836/// Iterator for searching artists in the user's library.
837///
838/// This iterator provides paginated access to artists that match a search query
839/// in the authenticated user's Last.fm library, using Last.fm's built-in search functionality.
840pub struct SearchArtistsIterator<C: LastFmBaseClient> {
841    client: C,
842    query: String,
843    current_page: u32,
844    has_more: bool,
845    buffer: Vec<crate::Artist>,
846    total_pages: Option<u32>,
847}
848
849#[async_trait(?Send)]
850impl<C: LastFmBaseClient> AsyncPaginatedIterator<crate::Artist> for SearchArtistsIterator<C> {
851    async fn next(&mut self) -> Result<Option<crate::Artist>> {
852        // If buffer is empty, try to load next page
853        if self.buffer.is_empty() {
854            if let Some(page) = self.next_page().await? {
855                self.buffer = page.artists;
856                self.buffer.reverse(); // Reverse so we can pop from end efficiently
857            }
858        }
859
860        Ok(self.buffer.pop())
861    }
862
863    fn current_page(&self) -> u32 {
864        self.current_page.saturating_sub(1)
865    }
866
867    fn total_pages(&self) -> Option<u32> {
868        self.total_pages
869    }
870}
871
872impl<C: LastFmBaseClient> SearchArtistsIterator<C> {
873    /// Create a new search artists iterator.
874    ///
875    /// This is typically called via [`LastFmBaseClient::search_artists`](crate::LastFmBaseClient::search_artists).
876    pub fn new(client: C, query: String) -> Self {
877        Self {
878            client,
879            query,
880            current_page: 1,
881            has_more: true,
882            buffer: Vec::new(),
883            total_pages: None,
884        }
885    }
886
887    /// Create a new search artists iterator starting from a specific page.
888    ///
889    /// This is useful for implementing offset functionality efficiently by starting
890    /// at the appropriate page rather than iterating through all previous pages.
891    pub fn with_starting_page(client: C, query: String, starting_page: u32) -> Self {
892        let page = std::cmp::max(1, starting_page);
893        Self {
894            client,
895            query,
896            current_page: page,
897            has_more: true,
898            buffer: Vec::new(),
899            total_pages: None,
900        }
901    }
902
903    /// Fetch the next page of search results.
904    ///
905    /// This method handles pagination automatically and includes rate limiting
906    /// to be respectful to Last.fm's servers.
907    pub async fn next_page(&mut self) -> Result<Option<crate::ArtistPage>> {
908        if !self.has_more {
909            return Ok(None);
910        }
911
912        let page = self
913            .client
914            .search_artists_page(&self.query, self.current_page)
915            .await?;
916
917        self.has_more = page.has_next_page;
918        self.current_page += 1;
919        self.total_pages = page.total_pages;
920
921        Ok(Some(page))
922    }
923
924    /// Get the total number of pages, if known.
925    ///
926    /// Returns `None` until at least one page has been fetched.
927    pub fn total_pages(&self) -> Option<u32> {
928        self.total_pages
929    }
930}
931
932// =============================================================================
933// ARTISTS ITERATOR
934// =============================================================================
935
936/// Iterator for browsing all artists in the user's library.
937///
938/// This iterator provides access to all artists in the authenticated user's Last.fm library,
939/// sorted by play count (highest first). The iterator loads artists as needed and handles
940/// rate limiting automatically to be respectful to Last.fm's servers.
941pub struct ArtistsIterator<C: LastFmBaseClient> {
942    client: C,
943    current_page: u32,
944    has_more: bool,
945    buffer: Vec<crate::Artist>,
946    total_pages: Option<u32>,
947}
948
949#[async_trait(?Send)]
950impl<C: LastFmBaseClient> AsyncPaginatedIterator<crate::Artist> for ArtistsIterator<C> {
951    async fn next(&mut self) -> Result<Option<crate::Artist>> {
952        // If buffer is empty, try to load next page
953        if self.buffer.is_empty() {
954            if let Some(page) = self.next_page().await? {
955                self.buffer = page.artists;
956                self.buffer.reverse(); // Reverse so we can pop from end efficiently
957            }
958        }
959
960        Ok(self.buffer.pop())
961    }
962
963    fn current_page(&self) -> u32 {
964        self.current_page.saturating_sub(1)
965    }
966
967    fn total_pages(&self) -> Option<u32> {
968        self.total_pages
969    }
970}
971
972impl<C: LastFmBaseClient> ArtistsIterator<C> {
973    /// Create a new artists iterator.
974    ///
975    /// This iterator will start from page 1 and load all artists in the user's library.
976    pub fn new(client: C) -> Self {
977        Self {
978            client,
979            current_page: 1,
980            has_more: true,
981            buffer: Vec::new(),
982            total_pages: None,
983        }
984    }
985
986    /// Create a new artists iterator starting from a specific page.
987    ///
988    /// This is useful for implementing offset functionality efficiently by starting
989    /// at the appropriate page rather than iterating through all previous pages.
990    pub fn with_starting_page(client: C, starting_page: u32) -> Self {
991        let page = std::cmp::max(1, starting_page);
992        Self {
993            client,
994            current_page: page,
995            has_more: true,
996            buffer: Vec::new(),
997            total_pages: None,
998        }
999    }
1000
1001    /// Fetch the next page of artists.
1002    ///
1003    /// This method handles pagination automatically and includes rate limiting
1004    /// to be respectful to Last.fm's servers.
1005    pub async fn next_page(&mut self) -> Result<Option<crate::ArtistPage>> {
1006        if !self.has_more {
1007            return Ok(None);
1008        }
1009
1010        let page = self.client.get_artists_page(self.current_page).await?;
1011
1012        self.has_more = page.has_next_page;
1013        self.current_page += 1;
1014        self.total_pages = page.total_pages;
1015
1016        Ok(Some(page))
1017    }
1018
1019    /// Get the total number of pages, if known.
1020    ///
1021    /// Returns `None` until at least one page has been fetched.
1022    pub fn total_pages(&self) -> Option<u32> {
1023        self.total_pages
1024    }
1025}