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