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    from: Option<u64>,
483    to: Option<u64>,
484}
485
486#[async_trait(?Send)]
487impl<C: LastFmApiClient> AsyncPaginatedIterator<Track> for ApiRecentTracksIterator<C> {
488    async fn next(&mut self) -> Result<Option<Track>> {
489        if self.buffer.is_empty() {
490            if !self.has_more {
491                return Ok(None);
492            }
493
494            let page = self
495                .client
496                .api_get_recent_tracks_page_in_range(self.current_page, self.from, self.to)
497                .await?;
498
499            if page.tracks.is_empty() {
500                self.has_more = false;
501                return Ok(None);
502            }
503
504            self.has_more = page.has_next_page;
505            self.total_pages = page.total_pages;
506
507            if let Some(stop_timestamp) = self.stop_at_timestamp {
508                let mut filtered_tracks = Vec::new();
509                for track in page.tracks {
510                    if let Some(track_timestamp) = track.timestamp {
511                        if track_timestamp <= stop_timestamp {
512                            self.has_more = false;
513                            break;
514                        }
515                    }
516                    filtered_tracks.push(track);
517                }
518                self.buffer = filtered_tracks;
519            } else {
520                self.buffer = page.tracks;
521            }
522
523            self.buffer.reverse();
524            self.current_page += 1;
525        }
526
527        Ok(self.buffer.pop())
528    }
529
530    fn current_page(&self) -> u32 {
531        self.current_page.saturating_sub(1)
532    }
533
534    fn total_pages(&self) -> Option<u32> {
535        self.total_pages
536    }
537}
538
539impl<C: LastFmApiClient> ApiRecentTracksIterator<C> {
540    pub fn new(client: C) -> Self {
541        Self::with_starting_page(client, 1)
542    }
543
544    pub fn with_starting_page(client: C, starting_page: u32) -> Self {
545        let page = std::cmp::max(1, starting_page);
546        Self {
547            client,
548            current_page: page,
549            has_more: true,
550            buffer: Vec::new(),
551            stop_at_timestamp: None,
552            total_pages: None,
553            from: None,
554            to: None,
555        }
556    }
557
558    /// Create an iterator restricted to a unix-timestamp window.
559    ///
560    /// `from` and `to` are forwarded to the `user.getRecentTracks` endpoint's optional
561    /// query parameters on every page fetch. Per the last.fm API documentation, `from`
562    /// selects tracks strictly after the given timestamp and `to` selects tracks
563    /// strictly before it — but the exact edge inclusivity has not yet been confirmed
564    /// against the live service (to be verified via VCR recording). A caller wanting a
565    /// half-open `[from, to)` window should conservatively pass
566    /// `from = window.start - 1`, or dedupe results by timestamp.
567    pub fn with_range(client: C, from: Option<u64>, to: Option<u64>) -> Self {
568        let mut iterator = Self::new(client);
569        iterator.from = from;
570        iterator.to = to;
571        iterator
572    }
573
574    pub fn with_stop_timestamp(mut self, timestamp: u64) -> Self {
575        self.stop_at_timestamp = Some(timestamp);
576        self
577    }
578}
579
580/// Iterator for browsing tracks in a specific album from a user's library.
581///
582/// This iterator provides access to all tracks in a specific album by an artist
583/// in the authenticated user's Last.fm library. Unlike paginated iterators,
584/// this loads tracks once and iterates through them.
585pub struct AlbumTracksIterator<C: LastFmBaseClient> {
586    client: C,
587    album_name: String,
588    artist_name: String,
589    tracks: Option<Vec<Track>>,
590    index: usize,
591}
592
593#[async_trait(?Send)]
594impl<C: LastFmBaseClient> AsyncPaginatedIterator<Track> for AlbumTracksIterator<C> {
595    async fn next(&mut self) -> Result<Option<Track>> {
596        // Load tracks if not already loaded
597        if self.tracks.is_none() {
598            // Use get_album_tracks_page instead of get_album_tracks to avoid infinite recursion
599            let tracks_page = self
600                .client
601                .get_album_tracks_page(&self.album_name, &self.artist_name, 1)
602                .await?;
603            log::debug!(
604                "Album '{}' by '{}' has {} tracks: {:?}",
605                self.album_name,
606                self.artist_name,
607                tracks_page.tracks.len(),
608                tracks_page
609                    .tracks
610                    .iter()
611                    .map(|t| &t.name)
612                    .collect::<Vec<_>>()
613            );
614
615            if tracks_page.tracks.is_empty() {
616                log::warn!(
617                    "🚨 ZERO TRACKS FOUND for album '{}' by '{}' - investigating...",
618                    self.album_name,
619                    self.artist_name
620                );
621                log::debug!("Full TrackPage for empty album: has_next_page={}, page_number={}, total_pages={:?}",
622                           tracks_page.has_next_page, tracks_page.page_number, tracks_page.total_pages);
623            }
624            self.tracks = Some(tracks_page.tracks);
625        }
626
627        // Return next track
628        if let Some(tracks) = &self.tracks {
629            if self.index < tracks.len() {
630                let track = tracks[self.index].clone();
631                self.index += 1;
632                Ok(Some(track))
633            } else {
634                Ok(None)
635            }
636        } else {
637            Ok(None)
638        }
639    }
640
641    fn current_page(&self) -> u32 {
642        // Album tracks don't have pages, so return 0
643        0
644    }
645}
646
647impl<C: LastFmBaseClient> AlbumTracksIterator<C> {
648    /// Create a new album tracks iterator.
649    ///
650    /// This is typically called via [`LastFmBaseClient::album_tracks`](crate::LastFmBaseClient::album_tracks).
651    pub fn new(client: C, album_name: String, artist_name: String) -> Self {
652        Self {
653            client,
654            album_name,
655            artist_name,
656            tracks: None,
657            index: 0,
658        }
659    }
660}
661
662/// Iterator for searching tracks in the user's library.
663///
664/// This iterator provides paginated access to tracks that match a search query
665/// in the authenticated user's Last.fm library, using Last.fm's built-in search functionality.
666pub struct SearchTracksIterator<C: LastFmBaseClient> {
667    client: C,
668    query: String,
669    current_page: u32,
670    has_more: bool,
671    buffer: Vec<Track>,
672    total_pages: Option<u32>,
673}
674
675#[async_trait(?Send)]
676impl<C: LastFmBaseClient> AsyncPaginatedIterator<Track> for SearchTracksIterator<C> {
677    async fn next(&mut self) -> Result<Option<Track>> {
678        // If buffer is empty, try to load next page
679        if self.buffer.is_empty() {
680            if let Some(page) = self.next_page().await? {
681                self.buffer = page.tracks;
682                self.buffer.reverse(); // Reverse so we can pop from end efficiently
683            }
684        }
685
686        Ok(self.buffer.pop())
687    }
688
689    fn current_page(&self) -> u32 {
690        self.current_page.saturating_sub(1)
691    }
692
693    fn total_pages(&self) -> Option<u32> {
694        self.total_pages
695    }
696}
697
698impl<C: LastFmBaseClient> SearchTracksIterator<C> {
699    /// Create a new search tracks iterator.
700    ///
701    /// This is typically called via [`LastFmBaseClient::search_tracks`](crate::LastFmBaseClient::search_tracks).
702    pub fn new(client: C, query: String) -> Self {
703        Self {
704            client,
705            query,
706            current_page: 1,
707            has_more: true,
708            buffer: Vec::new(),
709            total_pages: None,
710        }
711    }
712
713    /// Create a new search tracks iterator starting from a specific page.
714    ///
715    /// This is useful for implementing offset functionality efficiently by starting
716    /// at the appropriate page rather than iterating through all previous pages.
717    pub fn with_starting_page(client: C, query: String, starting_page: u32) -> Self {
718        let page = std::cmp::max(1, starting_page);
719        Self {
720            client,
721            query,
722            current_page: page,
723            has_more: true,
724            buffer: Vec::new(),
725            total_pages: None,
726        }
727    }
728
729    /// Fetch the next page of search results.
730    ///
731    /// This method handles pagination automatically and includes rate limiting
732    /// to be respectful to Last.fm's servers.
733    pub async fn next_page(&mut self) -> Result<Option<TrackPage>> {
734        if !self.has_more {
735            return Ok(None);
736        }
737
738        let page = self
739            .client
740            .search_tracks_page(&self.query, self.current_page)
741            .await?;
742
743        self.has_more = page.has_next_page;
744        self.current_page += 1;
745        self.total_pages = page.total_pages;
746
747        Ok(Some(page))
748    }
749
750    /// Get the total number of pages, if known.
751    ///
752    /// Returns `None` until at least one page has been fetched.
753    pub fn total_pages(&self) -> Option<u32> {
754        self.total_pages
755    }
756}
757
758/// Iterator for searching albums in the user's library.
759///
760/// This iterator provides paginated access to albums that match a search query
761/// in the authenticated user's Last.fm library, using Last.fm's built-in search functionality.
762///
763/// # Examples
764pub struct SearchAlbumsIterator<C: LastFmBaseClient> {
765    client: C,
766    query: String,
767    current_page: u32,
768    has_more: bool,
769    buffer: Vec<Album>,
770    total_pages: Option<u32>,
771}
772
773#[async_trait(?Send)]
774impl<C: LastFmBaseClient> AsyncPaginatedIterator<Album> for SearchAlbumsIterator<C> {
775    async fn next(&mut self) -> Result<Option<Album>> {
776        // If buffer is empty, try to load next page
777        if self.buffer.is_empty() {
778            if let Some(page) = self.next_page().await? {
779                self.buffer = page.albums;
780                self.buffer.reverse(); // Reverse so we can pop from end efficiently
781            }
782        }
783
784        Ok(self.buffer.pop())
785    }
786
787    fn current_page(&self) -> u32 {
788        self.current_page.saturating_sub(1)
789    }
790
791    fn total_pages(&self) -> Option<u32> {
792        self.total_pages
793    }
794}
795
796impl<C: LastFmBaseClient> SearchAlbumsIterator<C> {
797    /// Create a new search albums iterator.
798    ///
799    /// This is typically called via [`LastFmBaseClient::search_albums`](crate::LastFmBaseClient::search_albums).
800    pub fn new(client: C, query: String) -> Self {
801        Self {
802            client,
803            query,
804            current_page: 1,
805            has_more: true,
806            buffer: Vec::new(),
807            total_pages: None,
808        }
809    }
810
811    /// Create a new search albums iterator starting from a specific page.
812    ///
813    /// This is useful for implementing offset functionality efficiently by starting
814    /// at the appropriate page rather than iterating through all previous pages.
815    pub fn with_starting_page(client: C, query: String, starting_page: u32) -> Self {
816        let page = std::cmp::max(1, starting_page);
817        Self {
818            client,
819            query,
820            current_page: page,
821            has_more: true,
822            buffer: Vec::new(),
823            total_pages: None,
824        }
825    }
826
827    /// Fetch the next page of search results.
828    ///
829    /// This method handles pagination automatically and includes rate limiting
830    /// to be respectful to Last.fm's servers.
831    pub async fn next_page(&mut self) -> Result<Option<AlbumPage>> {
832        if !self.has_more {
833            return Ok(None);
834        }
835
836        let page = self
837            .client
838            .search_albums_page(&self.query, self.current_page)
839            .await?;
840
841        self.has_more = page.has_next_page;
842        self.current_page += 1;
843        self.total_pages = page.total_pages;
844
845        Ok(Some(page))
846    }
847
848    /// Get the total number of pages, if known.
849    ///
850    /// Returns `None` until at least one page has been fetched.
851    pub fn total_pages(&self) -> Option<u32> {
852        self.total_pages
853    }
854}
855
856/// Iterator for searching artists in the user's library.
857///
858/// This iterator provides paginated access to artists that match a search query
859/// in the authenticated user's Last.fm library, using Last.fm's built-in search functionality.
860pub struct SearchArtistsIterator<C: LastFmBaseClient> {
861    client: C,
862    query: String,
863    current_page: u32,
864    has_more: bool,
865    buffer: Vec<crate::Artist>,
866    total_pages: Option<u32>,
867}
868
869#[async_trait(?Send)]
870impl<C: LastFmBaseClient> AsyncPaginatedIterator<crate::Artist> for SearchArtistsIterator<C> {
871    async fn next(&mut self) -> Result<Option<crate::Artist>> {
872        // If buffer is empty, try to load next page
873        if self.buffer.is_empty() {
874            if let Some(page) = self.next_page().await? {
875                self.buffer = page.artists;
876                self.buffer.reverse(); // Reverse so we can pop from end efficiently
877            }
878        }
879
880        Ok(self.buffer.pop())
881    }
882
883    fn current_page(&self) -> u32 {
884        self.current_page.saturating_sub(1)
885    }
886
887    fn total_pages(&self) -> Option<u32> {
888        self.total_pages
889    }
890}
891
892impl<C: LastFmBaseClient> SearchArtistsIterator<C> {
893    /// Create a new search artists iterator.
894    ///
895    /// This is typically called via [`LastFmBaseClient::search_artists`](crate::LastFmBaseClient::search_artists).
896    pub fn new(client: C, query: String) -> Self {
897        Self {
898            client,
899            query,
900            current_page: 1,
901            has_more: true,
902            buffer: Vec::new(),
903            total_pages: None,
904        }
905    }
906
907    /// Create a new search artists iterator starting from a specific page.
908    ///
909    /// This is useful for implementing offset functionality efficiently by starting
910    /// at the appropriate page rather than iterating through all previous pages.
911    pub fn with_starting_page(client: C, query: String, starting_page: u32) -> Self {
912        let page = std::cmp::max(1, starting_page);
913        Self {
914            client,
915            query,
916            current_page: page,
917            has_more: true,
918            buffer: Vec::new(),
919            total_pages: None,
920        }
921    }
922
923    /// Fetch the next page of search results.
924    ///
925    /// This method handles pagination automatically and includes rate limiting
926    /// to be respectful to Last.fm's servers.
927    pub async fn next_page(&mut self) -> Result<Option<crate::ArtistPage>> {
928        if !self.has_more {
929            return Ok(None);
930        }
931
932        let page = self
933            .client
934            .search_artists_page(&self.query, self.current_page)
935            .await?;
936
937        self.has_more = page.has_next_page;
938        self.current_page += 1;
939        self.total_pages = page.total_pages;
940
941        Ok(Some(page))
942    }
943
944    /// Get the total number of pages, if known.
945    ///
946    /// Returns `None` until at least one page has been fetched.
947    pub fn total_pages(&self) -> Option<u32> {
948        self.total_pages
949    }
950}
951
952// =============================================================================
953// ARTISTS ITERATOR
954// =============================================================================
955
956/// Iterator for browsing all artists in the user's library.
957///
958/// This iterator provides access to all artists in the authenticated user's Last.fm library,
959/// sorted by play count (highest first). The iterator loads artists as needed and handles
960/// rate limiting automatically to be respectful to Last.fm's servers.
961pub struct ArtistsIterator<C: LastFmBaseClient> {
962    client: C,
963    current_page: u32,
964    has_more: bool,
965    buffer: Vec<crate::Artist>,
966    total_pages: Option<u32>,
967}
968
969#[async_trait(?Send)]
970impl<C: LastFmBaseClient> AsyncPaginatedIterator<crate::Artist> for ArtistsIterator<C> {
971    async fn next(&mut self) -> Result<Option<crate::Artist>> {
972        // If buffer is empty, try to load next page
973        if self.buffer.is_empty() {
974            if let Some(page) = self.next_page().await? {
975                self.buffer = page.artists;
976                self.buffer.reverse(); // Reverse so we can pop from end efficiently
977            }
978        }
979
980        Ok(self.buffer.pop())
981    }
982
983    fn current_page(&self) -> u32 {
984        self.current_page.saturating_sub(1)
985    }
986
987    fn total_pages(&self) -> Option<u32> {
988        self.total_pages
989    }
990}
991
992impl<C: LastFmBaseClient> ArtistsIterator<C> {
993    /// Create a new artists iterator.
994    ///
995    /// This iterator will start from page 1 and load all artists in the user's library.
996    pub fn new(client: C) -> Self {
997        Self {
998            client,
999            current_page: 1,
1000            has_more: true,
1001            buffer: Vec::new(),
1002            total_pages: None,
1003        }
1004    }
1005
1006    /// Create a new artists iterator starting from a specific page.
1007    ///
1008    /// This is useful for implementing offset functionality efficiently by starting
1009    /// at the appropriate page rather than iterating through all previous pages.
1010    pub fn with_starting_page(client: C, starting_page: u32) -> Self {
1011        let page = std::cmp::max(1, starting_page);
1012        Self {
1013            client,
1014            current_page: page,
1015            has_more: true,
1016            buffer: Vec::new(),
1017            total_pages: None,
1018        }
1019    }
1020
1021    /// Fetch the next page of artists.
1022    ///
1023    /// This method handles pagination automatically and includes rate limiting
1024    /// to be respectful to Last.fm's servers.
1025    pub async fn next_page(&mut self) -> Result<Option<crate::ArtistPage>> {
1026        if !self.has_more {
1027            return Ok(None);
1028        }
1029
1030        let page = self.client.get_artists_page(self.current_page).await?;
1031
1032        self.has_more = page.has_next_page;
1033        self.current_page += 1;
1034        self.total_pages = page.total_pages;
1035
1036        Ok(Some(page))
1037    }
1038
1039    /// Get the total number of pages, if known.
1040    ///
1041    /// Returns `None` until at least one page has been fetched.
1042    pub fn total_pages(&self) -> Option<u32> {
1043        self.total_pages
1044    }
1045}