lastfm_edit/
iterator.rs

1use crate::{Album, AlbumPage, LastFmEditClient, Result, Track, TrackPage};
2
3/// Async iterator trait for paginated Last.fm data.
4///
5/// This trait provides a common interface for iterating over paginated data from Last.fm,
6/// such as tracks, albums, and recent scrobbles. All iterators implement efficient streaming
7/// with automatic pagination and built-in rate limiting.
8///
9/// # Examples
10///
11/// ```rust,no_run
12/// use lastfm_edit::{LastFmEditClient, AsyncPaginatedIterator};
13///
14/// # tokio_test::block_on(async {
15/// let mut client = LastFmEditClient::new(Box::new(http_client::native::NativeClient::new()));
16/// // client.login(...).await?;
17///
18/// let mut tracks = client.artist_tracks("Radiohead");
19///
20/// // Iterate one by one
21/// while let Some(track) = tracks.next().await? {
22///     println!("{}", track.name);
23/// }
24///
25/// // Or collect a limited number
26/// let first_10 = tracks.take(10).await?;
27/// # Ok::<(), Box<dyn std::error::Error>>(())
28/// # });
29/// ```
30#[allow(async_fn_in_trait)]
31pub trait AsyncPaginatedIterator {
32    /// The item type yielded by this iterator
33    type Item;
34
35    /// Fetch the next item from the iterator.
36    ///
37    /// This method automatically handles pagination, fetching new pages as needed.
38    /// Returns `None` when there are no more items available.
39    ///
40    /// # Returns
41    ///
42    /// - `Ok(Some(item))` - Next item in the sequence
43    /// - `Ok(None)` - No more items available
44    /// - `Err(...)` - Network or parsing error occurred
45    async fn next(&mut self) -> Result<Option<Self::Item>>;
46
47    /// Collect all remaining items into a Vec.
48    ///
49    /// **Warning**: This method will fetch ALL remaining pages, which could be
50    /// many thousands of items for large libraries. Use [`take`](Self::take) for
51    /// safer bounded collection.
52    ///
53    /// # Examples
54    ///
55    /// ```rust,no_run
56    /// # use lastfm_edit::{LastFmEditClient, AsyncPaginatedIterator};
57    /// # tokio_test::block_on(async {
58    /// let mut client = LastFmEditClient::new(Box::new(http_client::native::NativeClient::new()));
59    /// let mut tracks = client.artist_tracks("Small Artist");
60    /// let all_tracks = tracks.collect_all().await?;
61    /// println!("Found {} tracks total", all_tracks.len());
62    /// # Ok::<(), Box<dyn std::error::Error>>(())
63    /// # });
64    /// ```
65    async fn collect_all(&mut self) -> Result<Vec<Self::Item>> {
66        let mut items = Vec::new();
67        while let Some(item) = self.next().await? {
68            items.push(item);
69        }
70        Ok(items)
71    }
72
73    /// Take up to n items from the iterator.
74    ///
75    /// This is the recommended way to collect a bounded number of items
76    /// from potentially large datasets.
77    ///
78    /// # Arguments
79    ///
80    /// * `n` - Maximum number of items to collect
81    ///
82    /// # Examples
83    ///
84    /// ```rust,no_run
85    /// # use lastfm_edit::{LastFmEditClient, AsyncPaginatedIterator};
86    /// # tokio_test::block_on(async {
87    /// let mut client = LastFmEditClient::new(Box::new(http_client::native::NativeClient::new()));
88    /// let mut tracks = client.artist_tracks("Radiohead");
89    /// let top_20 = tracks.take(20).await?;
90    /// println!("Top 20 tracks: {:?}", top_20);
91    /// # Ok::<(), Box<dyn std::error::Error>>(())
92    /// # });
93    /// ```
94    async fn take(&mut self, n: usize) -> Result<Vec<Self::Item>> {
95        let mut items = Vec::new();
96        for _ in 0..n {
97            match self.next().await? {
98                Some(item) => items.push(item),
99                None => break,
100            }
101        }
102        Ok(items)
103    }
104
105    /// Get the current page number (0-indexed).
106    ///
107    /// Returns the page number of the most recently fetched page.
108    fn current_page(&self) -> u32;
109}
110
111/// Iterator for browsing an artist's tracks from a user's library.
112///
113/// This iterator provides paginated access to all tracks by a specific artist
114/// in the authenticated user's Last.fm library, ordered by play count.
115///
116/// # Examples
117///
118/// ```rust,no_run
119/// # use lastfm_edit::{LastFmEditClient, AsyncPaginatedIterator};
120/// # tokio_test::block_on(async {
121/// let mut client = LastFmEditClient::new(Box::new(http_client::native::NativeClient::new()));
122/// // client.login(...).await?;
123///
124/// let mut tracks = client.artist_tracks("The Beatles");
125///
126/// // Get the top 5 most played tracks
127/// let top_tracks = tracks.take(5).await?;
128/// for track in top_tracks {
129///     println!("{} (played {} times)", track.name, track.playcount);
130/// }
131/// # Ok::<(), Box<dyn std::error::Error>>(())
132/// # });
133/// ```
134pub struct ArtistTracksIterator<'a> {
135    client: &'a LastFmEditClient,
136    artist: String,
137    current_page: u32,
138    has_more: bool,
139    buffer: Vec<Track>,
140    total_pages: Option<u32>,
141}
142
143impl<'a> AsyncPaginatedIterator for ArtistTracksIterator<'a> {
144    type Item = Track;
145
146    async fn next(&mut self) -> Result<Option<Self::Item>> {
147        // If buffer is empty, try to load next page
148        if self.buffer.is_empty() {
149            if let Some(page) = self.next_page().await? {
150                self.buffer = page.tracks;
151                self.buffer.reverse(); // Reverse so we can pop from end efficiently
152            }
153        }
154
155        Ok(self.buffer.pop())
156    }
157
158    fn current_page(&self) -> u32 {
159        self.current_page.saturating_sub(1)
160    }
161}
162
163impl<'a> ArtistTracksIterator<'a> {
164    /// Create a new artist tracks iterator.
165    ///
166    /// This is typically called via [`LastFmEditClient::artist_tracks`](crate::LastFmEditClient::artist_tracks).
167    pub fn new(client: &'a LastFmEditClient, artist: String) -> Self {
168        Self {
169            client,
170            artist,
171            current_page: 1,
172            has_more: true,
173            buffer: Vec::new(),
174            total_pages: None,
175        }
176    }
177
178    /// Fetch the next page of tracks.
179    ///
180    /// This method handles pagination automatically and includes rate limiting
181    /// to be respectful to Last.fm's servers.
182    pub async fn next_page(&mut self) -> Result<Option<TrackPage>> {
183        if !self.has_more {
184            return Ok(None);
185        }
186
187        let page = self
188            .client
189            .get_artist_tracks_page(&self.artist, self.current_page)
190            .await?;
191
192        self.has_more = page.has_next_page;
193        self.current_page += 1;
194        self.total_pages = page.total_pages;
195
196        Ok(Some(page))
197    }
198
199    /// Get the total number of pages, if known.
200    ///
201    /// Returns `None` until at least one page has been fetched.
202    pub fn total_pages(&self) -> Option<u32> {
203        self.total_pages
204    }
205}
206
207/// Iterator for browsing an artist's albums from a user's library.
208///
209/// This iterator provides paginated access to all albums by a specific artist
210/// in the authenticated user's Last.fm library, ordered by play count.
211///
212/// # Examples
213///
214/// ```rust,no_run
215/// # use lastfm_edit::{LastFmEditClient, AsyncPaginatedIterator};
216/// # tokio_test::block_on(async {
217/// let mut client = LastFmEditClient::new(Box::new(http_client::native::NativeClient::new()));
218/// // client.login(...).await?;
219///
220/// let mut albums = client.artist_albums("Pink Floyd");
221///
222/// // Get all albums (be careful with large discographies!)
223/// while let Some(album) = albums.next().await? {
224///     println!("{} (played {} times)", album.name, album.playcount);
225/// }
226/// # Ok::<(), Box<dyn std::error::Error>>(())
227/// # });
228/// ```
229pub struct ArtistAlbumsIterator<'a> {
230    client: &'a LastFmEditClient,
231    artist: String,
232    current_page: u32,
233    has_more: bool,
234    buffer: Vec<Album>,
235    total_pages: Option<u32>,
236}
237
238impl<'a> AsyncPaginatedIterator for ArtistAlbumsIterator<'a> {
239    type Item = Album;
240
241    async fn next(&mut self) -> Result<Option<Self::Item>> {
242        // If buffer is empty, try to load next page
243        if self.buffer.is_empty() {
244            if let Some(page) = self.next_page().await? {
245                self.buffer = page.albums;
246                self.buffer.reverse(); // Reverse so we can pop from end efficiently
247            }
248        }
249
250        Ok(self.buffer.pop())
251    }
252
253    fn current_page(&self) -> u32 {
254        self.current_page.saturating_sub(1)
255    }
256}
257
258impl<'a> ArtistAlbumsIterator<'a> {
259    /// Create a new artist albums iterator.
260    ///
261    /// This is typically called via [`LastFmEditClient::artist_albums`](crate::LastFmEditClient::artist_albums).
262    pub fn new(client: &'a LastFmEditClient, artist: String) -> Self {
263        Self {
264            client,
265            artist,
266            current_page: 1,
267            has_more: true,
268            buffer: Vec::new(),
269            total_pages: None,
270        }
271    }
272
273    /// Fetch the next page of albums.
274    ///
275    /// This method handles pagination automatically and includes rate limiting.
276    pub async fn next_page(&mut self) -> Result<Option<AlbumPage>> {
277        if !self.has_more {
278            return Ok(None);
279        }
280
281        let page = self
282            .client
283            .get_artist_albums_page(&self.artist, self.current_page)
284            .await?;
285
286        self.has_more = page.has_next_page;
287        self.current_page += 1;
288        self.total_pages = page.total_pages;
289
290        Ok(Some(page))
291    }
292
293    /// Get the total number of pages, if known.
294    ///
295    /// Returns `None` until at least one page has been fetched.
296    pub fn total_pages(&self) -> Option<u32> {
297        self.total_pages
298    }
299}
300
301/// Iterator for browsing a user's recent tracks/scrobbles.
302///
303/// This iterator provides access to the user's recent listening history with timestamps,
304/// which is essential for finding tracks that can be edited. It supports optional
305/// timestamp-based filtering to avoid reprocessing old data.
306///
307/// # Examples
308///
309/// ```rust,no_run
310/// # use lastfm_edit::{LastFmEditClient, AsyncPaginatedIterator};
311/// # tokio_test::block_on(async {
312/// let mut client = LastFmEditClient::new(Box::new(http_client::native::NativeClient::new()));
313/// // client.login(...).await?;
314///
315/// // Get recent tracks with timestamps
316/// let mut recent = client.recent_tracks();
317/// while let Some(track) = recent.next().await? {
318///     if let Some(timestamp) = track.timestamp {
319///         println!("{} - {} ({})", track.artist, track.name, timestamp);
320///     }
321/// }
322///
323/// // Or stop at a specific timestamp to avoid reprocessing
324/// let last_processed = 1640995200;
325/// let mut recent = client.recent_tracks().with_stop_timestamp(last_processed);
326/// let new_tracks = recent.collect_all().await?;
327/// # Ok::<(), Box<dyn std::error::Error>>(())
328/// # });
329/// ```
330pub struct RecentTracksIterator<'a> {
331    client: &'a LastFmEditClient,
332    current_page: u32,
333    has_more: bool,
334    buffer: Vec<Track>,
335    stop_at_timestamp: Option<u64>,
336}
337
338impl<'a> AsyncPaginatedIterator for RecentTracksIterator<'a> {
339    type Item = Track;
340
341    async fn next(&mut self) -> Result<Option<Self::Item>> {
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<'a> RecentTracksIterator<'a> {
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: &'a LastFmEditClient) -> 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, AsyncPaginatedIterator};
406    /// # tokio_test::block_on(async {
407    /// let mut client = LastFmEditClient::new(Box::new(http_client::native::NativeClient::new()));
408    ///
409    /// // Start from page 5
410    /// let mut recent = client.recent_tracks_from_page(5);
411    /// let tracks = recent.take(10).await?;
412    /// # Ok::<(), Box<dyn std::error::Error>>(())
413    /// # });
414    /// ```
415    pub fn with_starting_page(client: &'a LastFmEditClient, starting_page: u32) -> Self {
416        let page = std::cmp::max(1, starting_page);
417        Self {
418            client,
419            current_page: page,
420            has_more: true,
421            buffer: Vec::new(),
422            stop_at_timestamp: None,
423        }
424    }
425
426    /// Set a timestamp to stop iteration at.
427    ///
428    /// When this is set, the iterator will stop returning tracks once it encounters
429    /// a track with a timestamp less than or equal to the specified value. This is
430    /// useful for incremental processing to avoid reprocessing old data.
431    ///
432    /// # Arguments
433    ///
434    /// * `timestamp` - Unix timestamp to stop at
435    ///
436    /// # Examples
437    ///
438    /// ```rust,no_run
439    /// # use lastfm_edit::{LastFmEditClient, AsyncPaginatedIterator};
440    /// # tokio_test::block_on(async {
441    /// let mut client = LastFmEditClient::new(Box::new(http_client::native::NativeClient::new()));
442    /// let last_processed = 1640995200; // Some previous timestamp
443    ///
444    /// let mut recent = client.recent_tracks().with_stop_timestamp(last_processed);
445    /// let new_tracks = recent.collect_all().await?; // Only gets new tracks
446    /// # Ok::<(), Box<dyn std::error::Error>>(())
447    /// # });
448    /// ```
449    pub fn with_stop_timestamp(mut self, timestamp: u64) -> Self {
450        self.stop_at_timestamp = Some(timestamp);
451        self
452    }
453}