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