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