lastfm_edit/
iterator.rs

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