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
114/// Iterator for browsing an artist's tracks from a user's library.
115///
116/// This iterator provides paginated access to all tracks by a specific artist
117/// in the authenticated user's Last.fm library, ordered by play count.
118///
119/// # Examples
120///
121/// ```rust,no_run
122/// # use lastfm_edit::{LastFmEditClient, LastFmEditClientImpl, LastFmEditSession, AsyncPaginatedIterator};
123/// # tokio_test::block_on(async {
124/// # let test_session = LastFmEditSession::new("test".to_string(), vec!["sessionid=.test123".to_string()], Some("csrf".to_string()), "https://www.last.fm".to_string());
125/// let mut client = LastFmEditClientImpl::from_session(Box::new(http_client::native::NativeClient::new()), test_session);
126///
127/// let mut tracks = client.artist_tracks("The Beatles");
128///
129/// // Get the top 5 most played tracks
130/// let top_tracks = tracks.take(5).await?;
131/// for track in top_tracks {
132///     println!("{} (played {} times)", track.name, track.playcount);
133/// }
134/// # Ok::<(), Box<dyn std::error::Error>>(())
135/// # });
136/// ```
137pub struct ArtistTracksIterator<C: LastFmEditClient> {
138    client: C,
139    artist: String,
140    current_page: u32,
141    has_more: bool,
142    buffer: Vec<Track>,
143    total_pages: Option<u32>,
144}
145
146#[async_trait(?Send)]
147impl<C: LastFmEditClient> AsyncPaginatedIterator<Track> for ArtistTracksIterator<C> {
148    async fn next(&mut self) -> Result<Option<Track>> {
149        // If buffer is empty, try to load next page
150        if self.buffer.is_empty() {
151            if let Some(page) = self.next_page().await? {
152                self.buffer = page.tracks;
153                self.buffer.reverse(); // Reverse so we can pop from end efficiently
154            }
155        }
156
157        Ok(self.buffer.pop())
158    }
159
160    fn current_page(&self) -> u32 {
161        self.current_page.saturating_sub(1)
162    }
163}
164
165impl<C: LastFmEditClient> ArtistTracksIterator<C> {
166    /// Create a new artist tracks iterator.
167    ///
168    /// This is typically called via [`LastFmEditClient::artist_tracks`](crate::LastFmEditClient::artist_tracks).
169    pub fn new(client: C, artist: String) -> Self {
170        Self {
171            client,
172            artist,
173            current_page: 1,
174            has_more: true,
175            buffer: Vec::new(),
176            total_pages: None,
177        }
178    }
179
180    /// Fetch the next page of tracks.
181    ///
182    /// This method handles pagination automatically and includes rate limiting
183    /// to be respectful to Last.fm's servers.
184    pub async fn next_page(&mut self) -> Result<Option<TrackPage>> {
185        if !self.has_more {
186            return Ok(None);
187        }
188
189        let page = self
190            .client
191            .get_artist_tracks_page(&self.artist, self.current_page)
192            .await?;
193
194        self.has_more = page.has_next_page;
195        self.current_page += 1;
196        self.total_pages = page.total_pages;
197
198        Ok(Some(page))
199    }
200
201    /// Get the total number of pages, if known.
202    ///
203    /// Returns `None` until at least one page has been fetched.
204    pub fn total_pages(&self) -> Option<u32> {
205        self.total_pages
206    }
207}
208
209/// Iterator for browsing an artist's albums from a user's library.
210///
211/// This iterator provides paginated access to all albums by a specific artist
212/// in the authenticated user's Last.fm library, ordered by play count.
213///
214/// # Examples
215///
216/// ```rust,no_run
217/// # use lastfm_edit::{LastFmEditClient, LastFmEditClientImpl, LastFmEditSession, AsyncPaginatedIterator};
218/// # tokio_test::block_on(async {
219/// # let test_session = LastFmEditSession::new("test".to_string(), vec!["sessionid=.test123".to_string()], Some("csrf".to_string()), "https://www.last.fm".to_string());
220/// let mut client = LastFmEditClientImpl::from_session(Box::new(http_client::native::NativeClient::new()), test_session);
221///
222/// let mut albums = client.artist_albums("Pink Floyd");
223///
224/// // Get all albums (be careful with large discographies!)
225/// while let Some(album) = albums.next().await? {
226///     println!("{} (played {} times)", album.name, album.playcount);
227/// }
228/// # Ok::<(), Box<dyn std::error::Error>>(())
229/// # });
230/// ```
231pub struct ArtistAlbumsIterator<C: LastFmEditClient> {
232    client: C,
233    artist: String,
234    current_page: u32,
235    has_more: bool,
236    buffer: Vec<Album>,
237    total_pages: Option<u32>,
238}
239
240#[async_trait(?Send)]
241impl<C: LastFmEditClient> AsyncPaginatedIterator<Album> for ArtistAlbumsIterator<C> {
242    async fn next(&mut self) -> Result<Option<Album>> {
243        // If buffer is empty, try to load next page
244        if self.buffer.is_empty() {
245            if let Some(page) = self.next_page().await? {
246                self.buffer = page.albums;
247                self.buffer.reverse(); // Reverse so we can pop from end efficiently
248            }
249        }
250
251        Ok(self.buffer.pop())
252    }
253
254    fn current_page(&self) -> u32 {
255        self.current_page.saturating_sub(1)
256    }
257}
258
259impl<C: LastFmEditClient> ArtistAlbumsIterator<C> {
260    /// Create a new artist albums iterator.
261    ///
262    /// This is typically called via [`LastFmEditClient::artist_albums`](crate::LastFmEditClient::artist_albums).
263    pub fn new(client: C, artist: String) -> Self {
264        Self {
265            client,
266            artist,
267            current_page: 1,
268            has_more: true,
269            buffer: Vec::new(),
270            total_pages: None,
271        }
272    }
273
274    /// Fetch the next page of albums.
275    ///
276    /// This method handles pagination automatically and includes rate limiting.
277    pub async fn next_page(&mut self) -> Result<Option<AlbumPage>> {
278        if !self.has_more {
279            return Ok(None);
280        }
281
282        let page = self
283            .client
284            .get_artist_albums_page(&self.artist, self.current_page)
285            .await?;
286
287        self.has_more = page.has_next_page;
288        self.current_page += 1;
289        self.total_pages = page.total_pages;
290
291        Ok(Some(page))
292    }
293
294    /// Get the total number of pages, if known.
295    ///
296    /// Returns `None` until at least one page has been fetched.
297    pub fn total_pages(&self) -> Option<u32> {
298        self.total_pages
299    }
300}
301
302/// Iterator for browsing a user's recent tracks/scrobbles.
303///
304/// This iterator provides access to the user's recent listening history with timestamps,
305/// which is essential for finding tracks that can be edited. It supports optional
306/// timestamp-based filtering to avoid reprocessing old data.
307///
308/// # Examples
309///
310/// ```rust,no_run
311/// # use lastfm_edit::{LastFmEditClient, LastFmEditClientImpl, LastFmEditSession, AsyncPaginatedIterator};
312/// # tokio_test::block_on(async {
313/// # let test_session = LastFmEditSession::new("test".to_string(), vec!["sessionid=.test123".to_string()], Some("csrf".to_string()), "https://www.last.fm".to_string());
314/// let mut client = LastFmEditClientImpl::from_session(Box::new(http_client::native::NativeClient::new()), test_session);
315///
316/// // Get recent tracks with timestamps
317/// let mut recent = client.recent_tracks();
318/// while let Some(track) = recent.next().await? {
319///     if let Some(timestamp) = track.timestamp {
320///         println!("{} - {} ({})", track.artist, track.name, timestamp);
321///     }
322/// }
323///
324/// // Or stop at a specific timestamp to avoid reprocessing
325/// let last_processed = 1640995200;
326/// let mut recent = client.recent_tracks().with_stop_timestamp(last_processed);
327/// let new_tracks = recent.collect_all().await?;
328/// # Ok::<(), Box<dyn std::error::Error>>(())
329/// # });
330/// ```
331pub struct RecentTracksIterator<C: LastFmEditClient> {
332    client: C,
333    current_page: u32,
334    has_more: bool,
335    buffer: Vec<Track>,
336    stop_at_timestamp: Option<u64>,
337}
338
339#[async_trait(?Send)]
340impl<C: LastFmEditClient> AsyncPaginatedIterator<Track> for RecentTracksIterator<C> {
341    async fn next(&mut self) -> Result<Option<Track>> {
342        // If buffer is empty, try to load next page
343        if self.buffer.is_empty() {
344            if !self.has_more {
345                return Ok(None);
346            }
347
348            let tracks = self.client.get_recent_scrobbles(self.current_page).await?;
349
350            if tracks.is_empty() {
351                self.has_more = false;
352                return Ok(None);
353            }
354
355            // Check if we should stop based on timestamp
356            if let Some(stop_timestamp) = self.stop_at_timestamp {
357                let mut filtered_tracks = Vec::new();
358                for track in tracks {
359                    if let Some(track_timestamp) = track.timestamp {
360                        if track_timestamp <= stop_timestamp {
361                            self.has_more = false;
362                            break;
363                        }
364                    }
365                    filtered_tracks.push(track);
366                }
367                self.buffer = filtered_tracks;
368            } else {
369                self.buffer = tracks;
370            }
371
372            self.buffer.reverse(); // Reverse so we can pop from end efficiently
373            self.current_page += 1;
374        }
375
376        Ok(self.buffer.pop())
377    }
378
379    fn current_page(&self) -> u32 {
380        self.current_page.saturating_sub(1)
381    }
382}
383
384impl<C: LastFmEditClient> RecentTracksIterator<C> {
385    /// Create a new recent tracks iterator starting from page 1.
386    ///
387    /// This is typically called via [`LastFmEditClient::recent_tracks`](crate::LastFmEditClient::recent_tracks).
388    pub fn new(client: C) -> Self {
389        Self::with_starting_page(client, 1)
390    }
391
392    /// Create a new recent tracks iterator starting from a specific page.
393    ///
394    /// This allows resuming pagination from an arbitrary page, useful for
395    /// continuing from where a previous iteration left off.
396    ///
397    /// # Arguments
398    ///
399    /// * `client` - The LastFmEditClient to use for API calls
400    /// * `starting_page` - The page number to start from (1-indexed)
401    ///
402    /// # Examples
403    ///
404    /// ```rust,no_run
405    /// # use lastfm_edit::{LastFmEditClient, LastFmEditClientImpl, LastFmEditSession, AsyncPaginatedIterator};
406    /// # tokio_test::block_on(async {
407    /// # let test_session = LastFmEditSession::new("test".to_string(), vec!["sessionid=.test123".to_string()], Some("csrf".to_string()), "https://www.last.fm".to_string());
408    /// let mut client = LastFmEditClientImpl::from_session(Box::new(http_client::native::NativeClient::new()), test_session);
409    ///
410    /// // Start from page 5
411    /// let mut recent = client.recent_tracks_from_page(5);
412    /// let tracks = recent.take(10).await?;
413    /// # Ok::<(), Box<dyn std::error::Error>>(())
414    /// # });
415    /// ```
416    pub fn with_starting_page(client: C, starting_page: u32) -> Self {
417        let page = std::cmp::max(1, starting_page);
418        Self {
419            client,
420            current_page: page,
421            has_more: true,
422            buffer: Vec::new(),
423            stop_at_timestamp: None,
424        }
425    }
426
427    /// Set a timestamp to stop iteration at.
428    ///
429    /// When this is set, the iterator will stop returning tracks once it encounters
430    /// a track with a timestamp less than or equal to the specified value. This is
431    /// useful for incremental processing to avoid reprocessing old data.
432    ///
433    /// # Arguments
434    ///
435    /// * `timestamp` - Unix timestamp to stop at
436    ///
437    /// # Examples
438    ///
439    /// ```rust,no_run
440    /// # use lastfm_edit::{LastFmEditClient, LastFmEditClientImpl, LastFmEditSession, AsyncPaginatedIterator};
441    /// # tokio_test::block_on(async {
442    /// # let test_session = LastFmEditSession::new("test".to_string(), vec!["sessionid=.test123".to_string()], Some("csrf".to_string()), "https://www.last.fm".to_string());
443    /// let mut client = LastFmEditClientImpl::from_session(Box::new(http_client::native::NativeClient::new()), test_session);
444    /// let last_processed = 1640995200; // Some previous timestamp
445    ///
446    /// let mut recent = client.recent_tracks().with_stop_timestamp(last_processed);
447    /// let new_tracks = recent.collect_all().await?; // Only gets new tracks
448    /// # Ok::<(), Box<dyn std::error::Error>>(())
449    /// # });
450    /// ```
451    pub fn with_stop_timestamp(mut self, timestamp: u64) -> Self {
452        self.stop_at_timestamp = Some(timestamp);
453        self
454    }
455}
456
457/// Iterator for browsing tracks in a specific album from a user's library.
458///
459/// This iterator provides access to all tracks in a specific album by an artist
460/// in the authenticated user's Last.fm library. Unlike paginated iterators,
461/// this loads tracks once and iterates through them.
462///
463/// # Examples
464///
465/// ```rust,no_run
466/// # use lastfm_edit::{LastFmEditClient, LastFmEditClientImpl, LastFmEditSession, AsyncPaginatedIterator};
467/// # tokio_test::block_on(async {
468/// # let test_session = LastFmEditSession::new("test".to_string(), vec!["sessionid=.test123".to_string()], Some("csrf".to_string()), "https://www.last.fm".to_string());
469/// let mut client = LastFmEditClientImpl::from_session(Box::new(http_client::native::NativeClient::new()), test_session);
470///
471/// let mut tracks = client.album_tracks("The Dark Side of the Moon", "Pink Floyd");
472///
473/// // Get all tracks in the album
474/// while let Some(track) = tracks.next().await? {
475///     println!("{} - {}", track.name, track.artist);
476/// }
477/// # Ok::<(), Box<dyn std::error::Error>>(())
478/// # });
479/// ```
480pub struct AlbumTracksIterator<C: LastFmEditClient> {
481    client: C,
482    album_name: String,
483    artist_name: String,
484    tracks: Option<Vec<Track>>,
485    index: usize,
486}
487
488#[async_trait(?Send)]
489impl<C: LastFmEditClient> AsyncPaginatedIterator<Track> for AlbumTracksIterator<C> {
490    async fn next(&mut self) -> Result<Option<Track>> {
491        // Load tracks if not already loaded
492        if self.tracks.is_none() {
493            let tracks = self
494                .client
495                .get_album_tracks(&self.album_name, &self.artist_name)
496                .await?;
497            self.tracks = Some(tracks);
498        }
499
500        // Return next track
501        if let Some(tracks) = &self.tracks {
502            if self.index < tracks.len() {
503                let track = tracks[self.index].clone();
504                self.index += 1;
505                Ok(Some(track))
506            } else {
507                Ok(None)
508            }
509        } else {
510            Ok(None)
511        }
512    }
513
514    fn current_page(&self) -> u32 {
515        // Album tracks don't have pages, so return 0
516        0
517    }
518}
519
520impl<C: LastFmEditClient> AlbumTracksIterator<C> {
521    /// Create a new album tracks iterator.
522    ///
523    /// This is typically called via [`LastFmEditClient::album_tracks`](crate::LastFmEditClient::album_tracks).
524    pub fn new(client: C, album_name: String, artist_name: String) -> Self {
525        Self {
526            client,
527            album_name,
528            artist_name,
529            tracks: None,
530            index: 0,
531        }
532    }
533}