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                        // Create album tracks iterator for this album
184                        self.current_album_tracks = Some(AlbumTracksIterator::new(
185                            self.client.clone(),
186                            album.name.clone(),
187                            self.artist.clone(),
188                        ));
189                    } else {
190                        // No more albums, we're done
191                        self.finished = true;
192                        return Ok(None);
193                    }
194                }
195            }
196
197            // Get tracks from current album
198            if let Some(ref mut album_tracks) = self.current_album_tracks {
199                if let Some(track) = album_tracks.next().await? {
200                    self.track_buffer.push(track);
201                } else {
202                    // This album is exhausted, move to next album
203                    self.current_album_tracks = None;
204                    // Continue the loop to try getting the next album
205                }
206            }
207        }
208
209        // Return the next track from our buffer
210        Ok(self.track_buffer.pop())
211    }
212
213    fn current_page(&self) -> u32 {
214        // Since we're iterating through albums, return the album iterator's current page
215        if let Some(ref album_iter) = self.album_iterator {
216            album_iter.current_page()
217        } else {
218            0
219        }
220    }
221
222    fn total_pages(&self) -> Option<u32> {
223        // Since we're iterating through albums, return the album iterator's total pages
224        if let Some(ref album_iter) = self.album_iterator {
225            album_iter.total_pages()
226        } else {
227            None
228        }
229    }
230}
231
232impl<C: LastFmEditClient + Clone> ArtistTracksIterator<C> {
233    /// Create a new artist tracks iterator.
234    ///
235    /// This is typically called via [`LastFmEditClient::artist_tracks`](crate::LastFmEditClient::artist_tracks).
236    pub fn new(client: C, artist: String) -> Self {
237        Self {
238            client,
239            artist,
240            album_iterator: None,
241            current_album_tracks: None,
242            track_buffer: Vec::new(),
243            finished: false,
244        }
245    }
246}
247
248/// Iterator for browsing an artist's albums from a user's library.
249///
250/// This iterator provides paginated access to all albums by a specific artist
251/// in the authenticated user's Last.fm library, ordered by play count.
252///
253/// # Examples
254///
255/// ```rust,no_run
256/// # use lastfm_edit::{LastFmEditClient, LastFmEditClientImpl, LastFmEditSession, AsyncPaginatedIterator};
257/// # tokio_test::block_on(async {
258/// # let test_session = LastFmEditSession::new("test".to_string(), vec!["sessionid=.test123".to_string()], Some("csrf".to_string()), "https://www.last.fm".to_string());
259/// let mut client = LastFmEditClientImpl::from_session(Box::new(http_client::native::NativeClient::new()), test_session);
260///
261/// let mut albums = client.artist_albums("Pink Floyd");
262///
263/// // Get all albums (be careful with large discographies!)
264/// while let Some(album) = albums.next().await? {
265///     println!("{} (played {} times)", album.name, album.playcount);
266/// }
267/// # Ok::<(), Box<dyn std::error::Error>>(())
268/// # });
269/// ```
270pub struct ArtistAlbumsIterator<C: LastFmEditClient> {
271    client: C,
272    artist: String,
273    current_page: u32,
274    has_more: bool,
275    buffer: Vec<Album>,
276    total_pages: Option<u32>,
277}
278
279#[async_trait(?Send)]
280impl<C: LastFmEditClient> AsyncPaginatedIterator<Album> for ArtistAlbumsIterator<C> {
281    async fn next(&mut self) -> Result<Option<Album>> {
282        // If buffer is empty, try to load next page
283        if self.buffer.is_empty() {
284            if let Some(page) = self.next_page().await? {
285                self.buffer = page.albums;
286                self.buffer.reverse(); // Reverse so we can pop from end efficiently
287            }
288        }
289
290        Ok(self.buffer.pop())
291    }
292
293    fn current_page(&self) -> u32 {
294        self.current_page.saturating_sub(1)
295    }
296
297    fn total_pages(&self) -> Option<u32> {
298        self.total_pages
299    }
300}
301
302impl<C: LastFmEditClient> ArtistAlbumsIterator<C> {
303    /// Create a new artist albums iterator.
304    ///
305    /// This is typically called via [`LastFmEditClient::artist_albums`](crate::LastFmEditClient::artist_albums).
306    pub fn new(client: C, artist: String) -> Self {
307        Self {
308            client,
309            artist,
310            current_page: 1,
311            has_more: true,
312            buffer: Vec::new(),
313            total_pages: None,
314        }
315    }
316
317    /// Fetch the next page of albums.
318    ///
319    /// This method handles pagination automatically and includes rate limiting.
320    pub async fn next_page(&mut self) -> Result<Option<AlbumPage>> {
321        if !self.has_more {
322            return Ok(None);
323        }
324
325        let page = self
326            .client
327            .get_artist_albums_page(&self.artist, self.current_page)
328            .await?;
329
330        self.has_more = page.has_next_page;
331        self.current_page += 1;
332        self.total_pages = page.total_pages;
333
334        Ok(Some(page))
335    }
336
337    /// Get the total number of pages, if known.
338    ///
339    /// Returns `None` until at least one page has been fetched.
340    pub fn total_pages(&self) -> Option<u32> {
341        self.total_pages
342    }
343}
344
345/// Iterator for browsing a user's recent tracks/scrobbles.
346///
347/// This iterator provides access to the user's recent listening history with timestamps,
348/// which is essential for finding tracks that can be edited. It supports optional
349/// timestamp-based filtering to avoid reprocessing old data.
350///
351/// # Examples
352///
353/// ```rust,no_run
354/// # use lastfm_edit::{LastFmEditClient, LastFmEditClientImpl, LastFmEditSession, AsyncPaginatedIterator};
355/// # tokio_test::block_on(async {
356/// # let test_session = LastFmEditSession::new("test".to_string(), vec!["sessionid=.test123".to_string()], Some("csrf".to_string()), "https://www.last.fm".to_string());
357/// let mut client = LastFmEditClientImpl::from_session(Box::new(http_client::native::NativeClient::new()), test_session);
358///
359/// // Get recent tracks with timestamps
360/// let mut recent = client.recent_tracks();
361/// while let Some(track) = recent.next().await? {
362///     if let Some(timestamp) = track.timestamp {
363///         println!("{} - {} ({})", track.artist, track.name, timestamp);
364///     }
365/// }
366///
367/// // Or stop at a specific timestamp to avoid reprocessing
368/// let last_processed = 1640995200;
369/// let mut recent = lastfm_edit::RecentTracksIterator::new(client).with_stop_timestamp(last_processed);
370/// let new_tracks = recent.collect_all().await?;
371/// # Ok::<(), Box<dyn std::error::Error>>(())
372/// # });
373/// ```
374pub struct RecentTracksIterator<C: LastFmEditClient> {
375    client: C,
376    current_page: u32,
377    has_more: bool,
378    buffer: Vec<Track>,
379    stop_at_timestamp: Option<u64>,
380}
381
382#[async_trait(?Send)]
383impl<C: LastFmEditClient> AsyncPaginatedIterator<Track> for RecentTracksIterator<C> {
384    async fn next(&mut self) -> Result<Option<Track>> {
385        // If buffer is empty, try to load next page
386        if self.buffer.is_empty() {
387            if !self.has_more {
388                return Ok(None);
389            }
390
391            let tracks = self.client.get_recent_scrobbles(self.current_page).await?;
392
393            if tracks.is_empty() {
394                self.has_more = false;
395                return Ok(None);
396            }
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 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 = 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: LastFmEditClient> RecentTracksIterator<C> {
428    /// Create a new recent tracks iterator starting from page 1.
429    ///
430    /// This is typically called via [`LastFmEditClient::recent_tracks`](crate::LastFmEditClient::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 LastFmEditClient to use for API calls
443    /// * `starting_page` - The page number to start from (1-indexed)
444    ///
445    /// # Examples
446    ///
447    /// ```rust,no_run
448    /// # use lastfm_edit::{LastFmEditClient, LastFmEditClientImpl, LastFmEditSession, AsyncPaginatedIterator};
449    /// # tokio_test::block_on(async {
450    /// # let test_session = LastFmEditSession::new("test".to_string(), vec!["sessionid=.test123".to_string()], Some("csrf".to_string()), "https://www.last.fm".to_string());
451    /// let mut client = LastFmEditClientImpl::from_session(Box::new(http_client::native::NativeClient::new()), test_session);
452    ///
453    /// // Start from page 5
454    /// let mut recent = client.recent_tracks_from_page(5);
455    /// let tracks = recent.take(10).await?;
456    /// # Ok::<(), Box<dyn std::error::Error>>(())
457    /// # });
458    /// ```
459    pub fn with_starting_page(client: C, starting_page: u32) -> Self {
460        let page = std::cmp::max(1, starting_page);
461        Self {
462            client,
463            current_page: page,
464            has_more: true,
465            buffer: Vec::new(),
466            stop_at_timestamp: None,
467        }
468    }
469
470    /// Set a timestamp to stop iteration at.
471    ///
472    /// When this is set, the iterator will stop returning tracks once it encounters
473    /// a track with a timestamp less than or equal to the specified value. This is
474    /// useful for incremental processing to avoid reprocessing old data.
475    ///
476    /// # Arguments
477    ///
478    /// * `timestamp` - Unix timestamp to stop at
479    ///
480    /// # Examples
481    ///
482    /// ```rust,no_run
483    /// # use lastfm_edit::{LastFmEditClient, LastFmEditClientImpl, LastFmEditSession, AsyncPaginatedIterator};
484    /// # tokio_test::block_on(async {
485    /// # let test_session = LastFmEditSession::new("test".to_string(), vec!["sessionid=.test123".to_string()], Some("csrf".to_string()), "https://www.last.fm".to_string());
486    /// let mut client = LastFmEditClientImpl::from_session(Box::new(http_client::native::NativeClient::new()), test_session);
487    /// let last_processed = 1640995200; // Some previous timestamp
488    ///
489    /// let mut recent = lastfm_edit::RecentTracksIterator::new(client).with_stop_timestamp(last_processed);
490    /// let new_tracks = recent.collect_all().await?; // Only gets new tracks
491    /// # Ok::<(), Box<dyn std::error::Error>>(())
492    /// # });
493    /// ```
494    pub fn with_stop_timestamp(mut self, timestamp: u64) -> Self {
495        self.stop_at_timestamp = Some(timestamp);
496        self
497    }
498}
499
500/// Iterator for browsing tracks in a specific album from a user's library.
501///
502/// This iterator provides access to all tracks in a specific album by an artist
503/// in the authenticated user's Last.fm library. Unlike paginated iterators,
504/// this loads tracks once and iterates through them.
505///
506/// # Examples
507///
508/// ```rust,no_run
509/// # use lastfm_edit::{LastFmEditClient, LastFmEditClientImpl, LastFmEditSession, AsyncPaginatedIterator};
510/// # tokio_test::block_on(async {
511/// # let test_session = LastFmEditSession::new("test".to_string(), vec!["sessionid=.test123".to_string()], Some("csrf".to_string()), "https://www.last.fm".to_string());
512/// let mut client = LastFmEditClientImpl::from_session(Box::new(http_client::native::NativeClient::new()), test_session);
513///
514/// let mut tracks = client.album_tracks("The Dark Side of the Moon", "Pink Floyd");
515///
516/// // Get all tracks in the album
517/// while let Some(track) = tracks.next().await? {
518///     println!("{} - {}", track.name, track.artist);
519/// }
520/// # Ok::<(), Box<dyn std::error::Error>>(())
521/// # });
522/// ```
523pub struct AlbumTracksIterator<C: LastFmEditClient> {
524    client: C,
525    album_name: String,
526    artist_name: String,
527    tracks: Option<Vec<Track>>,
528    index: usize,
529}
530
531#[async_trait(?Send)]
532impl<C: LastFmEditClient> AsyncPaginatedIterator<Track> for AlbumTracksIterator<C> {
533    async fn next(&mut self) -> Result<Option<Track>> {
534        // Load tracks if not already loaded
535        if self.tracks.is_none() {
536            // Use get_album_tracks_page instead of get_album_tracks to avoid infinite recursion
537            let tracks_page = self
538                .client
539                .get_album_tracks_page(&self.album_name, &self.artist_name, 1)
540                .await?;
541            self.tracks = Some(tracks_page.tracks);
542        }
543
544        // Return next track
545        if let Some(tracks) = &self.tracks {
546            if self.index < tracks.len() {
547                let track = tracks[self.index].clone();
548                self.index += 1;
549                Ok(Some(track))
550            } else {
551                Ok(None)
552            }
553        } else {
554            Ok(None)
555        }
556    }
557
558    fn current_page(&self) -> u32 {
559        // Album tracks don't have pages, so return 0
560        0
561    }
562}
563
564impl<C: LastFmEditClient> AlbumTracksIterator<C> {
565    /// Create a new album tracks iterator.
566    ///
567    /// This is typically called via [`LastFmEditClient::album_tracks`](crate::LastFmEditClient::album_tracks).
568    pub fn new(client: C, album_name: String, artist_name: String) -> Self {
569        Self {
570            client,
571            album_name,
572            artist_name,
573            tracks: None,
574            index: 0,
575        }
576    }
577}
578
579/// Iterator for searching tracks in the user's library.
580///
581/// This iterator provides paginated access to tracks that match a search query
582/// in the authenticated user's Last.fm library, using Last.fm's built-in search functionality.
583///
584/// # Examples
585///
586/// ```rust,no_run
587/// # use lastfm_edit::{LastFmEditClient, LastFmEditClientImpl, LastFmEditSession, AsyncPaginatedIterator};
588/// # tokio_test::block_on(async {
589/// # let test_session = LastFmEditSession::new("test".to_string(), vec!["sessionid=.test123".to_string()], Some("csrf".to_string()), "https://www.last.fm".to_string());
590/// let mut client = LastFmEditClientImpl::from_session(Box::new(http_client::native::NativeClient::new()), test_session);
591///
592/// let mut search_results = client.search_tracks("remaster");
593///
594/// // Get first 20 search results
595/// while let Some(track) = search_results.next().await? {
596///     println!("{} - {} (played {} times)", track.artist, track.name, track.playcount);
597/// }
598/// # Ok::<(), Box<dyn std::error::Error>>(())
599/// # });
600/// ```
601pub struct SearchTracksIterator<C: LastFmEditClient> {
602    client: C,
603    query: String,
604    current_page: u32,
605    has_more: bool,
606    buffer: Vec<Track>,
607    total_pages: Option<u32>,
608}
609
610#[async_trait(?Send)]
611impl<C: LastFmEditClient> AsyncPaginatedIterator<Track> for SearchTracksIterator<C> {
612    async fn next(&mut self) -> Result<Option<Track>> {
613        // If buffer is empty, try to load next page
614        if self.buffer.is_empty() {
615            if let Some(page) = self.next_page().await? {
616                self.buffer = page.tracks;
617                self.buffer.reverse(); // Reverse so we can pop from end efficiently
618            }
619        }
620
621        Ok(self.buffer.pop())
622    }
623
624    fn current_page(&self) -> u32 {
625        self.current_page.saturating_sub(1)
626    }
627
628    fn total_pages(&self) -> Option<u32> {
629        self.total_pages
630    }
631}
632
633impl<C: LastFmEditClient> SearchTracksIterator<C> {
634    /// Create a new search tracks iterator.
635    ///
636    /// This is typically called via [`LastFmEditClient::search_tracks`](crate::LastFmEditClient::search_tracks).
637    pub fn new(client: C, query: String) -> Self {
638        Self {
639            client,
640            query,
641            current_page: 1,
642            has_more: true,
643            buffer: Vec::new(),
644            total_pages: None,
645        }
646    }
647
648    /// Create a new search tracks iterator starting from a specific page.
649    ///
650    /// This is useful for implementing offset functionality efficiently by starting
651    /// at the appropriate page rather than iterating through all previous pages.
652    pub fn with_starting_page(client: C, query: String, starting_page: u32) -> Self {
653        let page = std::cmp::max(1, starting_page);
654        Self {
655            client,
656            query,
657            current_page: page,
658            has_more: true,
659            buffer: Vec::new(),
660            total_pages: None,
661        }
662    }
663
664    /// Fetch the next page of search results.
665    ///
666    /// This method handles pagination automatically and includes rate limiting
667    /// to be respectful to Last.fm's servers.
668    pub async fn next_page(&mut self) -> Result<Option<TrackPage>> {
669        if !self.has_more {
670            return Ok(None);
671        }
672
673        let page = self
674            .client
675            .search_tracks_page(&self.query, self.current_page)
676            .await?;
677
678        self.has_more = page.has_next_page;
679        self.current_page += 1;
680        self.total_pages = page.total_pages;
681
682        Ok(Some(page))
683    }
684
685    /// Get the total number of pages, if known.
686    ///
687    /// Returns `None` until at least one page has been fetched.
688    pub fn total_pages(&self) -> Option<u32> {
689        self.total_pages
690    }
691}
692
693/// Iterator for searching albums in the user's library.
694///
695/// This iterator provides paginated access to albums that match a search query
696/// in the authenticated user's Last.fm library, using Last.fm's built-in search functionality.
697///
698/// # Examples
699///
700/// ```rust,no_run
701/// # use lastfm_edit::{LastFmEditClient, LastFmEditClientImpl, LastFmEditSession, AsyncPaginatedIterator};
702/// # tokio_test::block_on(async {
703/// # let test_session = LastFmEditSession::new("test".to_string(), vec!["sessionid=.test123".to_string()], Some("csrf".to_string()), "https://www.last.fm".to_string());
704/// let mut client = LastFmEditClientImpl::from_session(Box::new(http_client::native::NativeClient::new()), test_session);
705///
706/// let mut search_results = client.search_albums("deluxe");
707///
708/// // Get first 10 search results
709/// let top_10 = search_results.take(10).await?;
710/// for album in top_10 {
711///     println!("{} - {} (played {} times)", album.artist, album.name, album.playcount);
712/// }
713/// # Ok::<(), Box<dyn std::error::Error>>(())
714/// # });
715/// ```
716pub struct SearchAlbumsIterator<C: LastFmEditClient> {
717    client: C,
718    query: String,
719    current_page: u32,
720    has_more: bool,
721    buffer: Vec<Album>,
722    total_pages: Option<u32>,
723}
724
725#[async_trait(?Send)]
726impl<C: LastFmEditClient> AsyncPaginatedIterator<Album> for SearchAlbumsIterator<C> {
727    async fn next(&mut self) -> Result<Option<Album>> {
728        // If buffer is empty, try to load next page
729        if self.buffer.is_empty() {
730            if let Some(page) = self.next_page().await? {
731                self.buffer = page.albums;
732                self.buffer.reverse(); // Reverse so we can pop from end efficiently
733            }
734        }
735
736        Ok(self.buffer.pop())
737    }
738
739    fn current_page(&self) -> u32 {
740        self.current_page.saturating_sub(1)
741    }
742
743    fn total_pages(&self) -> Option<u32> {
744        self.total_pages
745    }
746}
747
748impl<C: LastFmEditClient> SearchAlbumsIterator<C> {
749    /// Create a new search albums iterator.
750    ///
751    /// This is typically called via [`LastFmEditClient::search_albums`](crate::LastFmEditClient::search_albums).
752    pub fn new(client: C, query: String) -> Self {
753        Self {
754            client,
755            query,
756            current_page: 1,
757            has_more: true,
758            buffer: Vec::new(),
759            total_pages: None,
760        }
761    }
762
763    /// Create a new search albums iterator starting from a specific page.
764    ///
765    /// This is useful for implementing offset functionality efficiently by starting
766    /// at the appropriate page rather than iterating through all previous pages.
767    pub fn with_starting_page(client: C, query: String, starting_page: u32) -> Self {
768        let page = std::cmp::max(1, starting_page);
769        Self {
770            client,
771            query,
772            current_page: page,
773            has_more: true,
774            buffer: Vec::new(),
775            total_pages: None,
776        }
777    }
778
779    /// Fetch the next page of search results.
780    ///
781    /// This method handles pagination automatically and includes rate limiting
782    /// to be respectful to Last.fm's servers.
783    pub async fn next_page(&mut self) -> Result<Option<AlbumPage>> {
784        if !self.has_more {
785            return Ok(None);
786        }
787
788        let page = self
789            .client
790            .search_albums_page(&self.query, self.current_page)
791            .await?;
792
793        self.has_more = page.has_next_page;
794        self.current_page += 1;
795        self.total_pages = page.total_pages;
796
797        Ok(Some(page))
798    }
799
800    /// Get the total number of pages, if known.
801    ///
802    /// Returns `None` until at least one page has been fetched.
803    pub fn total_pages(&self) -> Option<u32> {
804        self.total_pages
805    }
806}
807
808// =============================================================================
809// ARTISTS ITERATOR
810// =============================================================================
811
812/// Iterator for browsing all artists in the user's library.
813///
814/// This iterator provides access to all artists in the authenticated user's Last.fm library,
815/// sorted by play count (highest first). The iterator loads artists as needed and handles
816/// rate limiting automatically to be respectful to Last.fm's servers.
817///
818/// # Examples
819///
820/// ```rust,no_run
821/// # use lastfm_edit::{LastFmEditClient, LastFmEditClientImpl, LastFmEditSession, AsyncPaginatedIterator};
822/// # tokio_test::block_on(async {
823/// # let test_session = LastFmEditSession::new("test".to_string(), vec!["sessionid=.test123".to_string()], Some("csrf".to_string()), "https://www.last.fm".to_string());
824/// let mut client = LastFmEditClientImpl::from_session(Box::new(http_client::native::NativeClient::new()), test_session);
825///
826/// let mut artists = client.artists();
827///
828/// // Get the top 10 artists
829/// let top_artists = artists.take(10).await?;
830/// for artist in top_artists {
831///     println!("{} ({} plays)", artist.name, artist.playcount);
832/// }
833/// # Ok::<(), Box<dyn std::error::Error>>(())
834/// # });
835/// ```
836pub struct ArtistsIterator<C: LastFmEditClient> {
837    client: C,
838    current_page: u32,
839    has_more: bool,
840    buffer: Vec<crate::Artist>,
841    total_pages: Option<u32>,
842}
843
844#[async_trait(?Send)]
845impl<C: LastFmEditClient> AsyncPaginatedIterator<crate::Artist> for ArtistsIterator<C> {
846    async fn next(&mut self) -> Result<Option<crate::Artist>> {
847        // If buffer is empty, try to load next page
848        if self.buffer.is_empty() {
849            if let Some(page) = self.next_page().await? {
850                self.buffer = page.artists;
851                self.buffer.reverse(); // Reverse so we can pop from end efficiently
852            }
853        }
854
855        Ok(self.buffer.pop())
856    }
857
858    fn current_page(&self) -> u32 {
859        self.current_page.saturating_sub(1)
860    }
861
862    fn total_pages(&self) -> Option<u32> {
863        self.total_pages
864    }
865}
866
867impl<C: LastFmEditClient> ArtistsIterator<C> {
868    /// Create a new artists iterator.
869    ///
870    /// This iterator will start from page 1 and load all artists in the user's library.
871    pub fn new(client: C) -> Self {
872        Self {
873            client,
874            current_page: 1,
875            has_more: true,
876            buffer: Vec::new(),
877            total_pages: None,
878        }
879    }
880
881    /// Create a new artists iterator starting from a specific page.
882    ///
883    /// This is useful for implementing offset functionality efficiently by starting
884    /// at the appropriate page rather than iterating through all previous pages.
885    pub fn with_starting_page(client: C, starting_page: u32) -> Self {
886        let page = std::cmp::max(1, starting_page);
887        Self {
888            client,
889            current_page: page,
890            has_more: true,
891            buffer: Vec::new(),
892            total_pages: None,
893        }
894    }
895
896    /// Fetch the next page of artists.
897    ///
898    /// This method handles pagination automatically and includes rate limiting
899    /// to be respectful to Last.fm's servers.
900    pub async fn next_page(&mut self) -> Result<Option<crate::ArtistPage>> {
901        if !self.has_more {
902            return Ok(None);
903        }
904
905        let page = self.client.get_artists_page(self.current_page).await?;
906
907        self.has_more = page.has_next_page;
908        self.current_page += 1;
909        self.total_pages = page.total_pages;
910
911        Ok(Some(page))
912    }
913
914    /// Get the total number of pages, if known.
915    ///
916    /// Returns `None` until at least one page has been fetched.
917    pub fn total_pages(&self) -> Option<u32> {
918        self.total_pages
919    }
920}