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