lastfm_edit/
iterator.rs

1use crate::{Album, AlbumPage, LastFmEditClientImpl, Result, Track, TrackPage};
2
3use async_trait::async_trait;
4
5/// Async iterator trait for paginated Last.fm data.
6///
7/// This trait provides a common interface for iterating over paginated data from Last.fm,
8/// such as tracks, albums, and recent scrobbles. All iterators implement efficient streaming
9/// with automatic pagination and built-in rate limiting.
10///
11/// # Examples
12///
13/// ```rust,no_run
14/// use lastfm_edit::{LastFmEditClient, LastFmEditClientImpl, AsyncPaginatedIterator};
15///
16/// # tokio_test::block_on(async {
17/// let mut client = LastFmEditClientImpl::new(Box::new(http_client::native::NativeClient::new()));
18/// // client.login(...).await?;
19///
20/// let mut tracks = client.artist_tracks("Radiohead");
21///
22/// // Iterate one by one
23/// while let Some(track) = tracks.next().await? {
24///     println!("{}", track.name);
25/// }
26///
27/// // Or collect a limited number
28/// let first_10 = tracks.take(10).await?;
29/// # Ok::<(), Box<dyn std::error::Error>>(())
30/// # });
31/// ```
32#[cfg_attr(feature = "mock", mockall::automock)]
33#[async_trait(?Send)]
34pub trait AsyncPaginatedIterator<T> {
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<T>>;
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, LastFmEditClientImpl, AsyncPaginatedIterator};
57    /// # tokio_test::block_on(async {
58    /// let mut client = LastFmEditClientImpl::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<T>> {
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, LastFmEditClientImpl, AsyncPaginatedIterator};
86    /// # tokio_test::block_on(async {
87    /// let mut client = LastFmEditClientImpl::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<T>> {
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, LastFmEditClientImpl, AsyncPaginatedIterator};
120/// # tokio_test::block_on(async {
121/// let mut client = LastFmEditClientImpl::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 {
135    client: LastFmEditClientImpl,
136    artist: String,
137    current_page: u32,
138    has_more: bool,
139    buffer: Vec<Track>,
140    total_pages: Option<u32>,
141}
142
143#[async_trait(?Send)]
144impl AsyncPaginatedIterator<Track> for ArtistTracksIterator {
145    async fn next(&mut self) -> Result<Option<Track>> {
146        // If buffer is empty, try to load next page
147        if self.buffer.is_empty() {
148            if let Some(page) = self.next_page().await? {
149                self.buffer = page.tracks;
150                self.buffer.reverse(); // Reverse so we can pop from end efficiently
151            }
152        }
153
154        Ok(self.buffer.pop())
155    }
156
157    fn current_page(&self) -> u32 {
158        self.current_page.saturating_sub(1)
159    }
160}
161
162impl ArtistTracksIterator {
163    /// Create a new artist tracks iterator.
164    ///
165    /// This is typically called via [`LastFmEditClient::artist_tracks`](crate::LastFmEditClient::artist_tracks).
166    pub fn new(client: LastFmEditClientImpl, artist: String) -> Self {
167        Self {
168            client,
169            artist,
170            current_page: 1,
171            has_more: true,
172            buffer: Vec::new(),
173            total_pages: None,
174        }
175    }
176
177    /// Fetch the next page of tracks.
178    ///
179    /// This method handles pagination automatically and includes rate limiting
180    /// to be respectful to Last.fm's servers.
181    pub async fn next_page(&mut self) -> Result<Option<TrackPage>> {
182        if !self.has_more {
183            return Ok(None);
184        }
185
186        let page = self
187            .client
188            .get_artist_tracks_page(&self.artist, self.current_page)
189            .await?;
190
191        self.has_more = page.has_next_page;
192        self.current_page += 1;
193        self.total_pages = page.total_pages;
194
195        Ok(Some(page))
196    }
197
198    /// Get the total number of pages, if known.
199    ///
200    /// Returns `None` until at least one page has been fetched.
201    pub fn total_pages(&self) -> Option<u32> {
202        self.total_pages
203    }
204}
205
206/// Iterator for browsing an artist's albums from a user's library.
207///
208/// This iterator provides paginated access to all albums by a specific artist
209/// in the authenticated user's Last.fm library, ordered by play count.
210///
211/// # Examples
212///
213/// ```rust,no_run
214/// # use lastfm_edit::{LastFmEditClient, LastFmEditClientImpl, AsyncPaginatedIterator};
215/// # tokio_test::block_on(async {
216/// let mut client = LastFmEditClientImpl::new(Box::new(http_client::native::NativeClient::new()));
217/// // client.login(...).await?;
218///
219/// let mut albums = client.artist_albums("Pink Floyd");
220///
221/// // Get all albums (be careful with large discographies!)
222/// while let Some(album) = albums.next().await? {
223///     println!("{} (played {} times)", album.name, album.playcount);
224/// }
225/// # Ok::<(), Box<dyn std::error::Error>>(())
226/// # });
227/// ```
228pub struct ArtistAlbumsIterator {
229    client: LastFmEditClientImpl,
230    artist: String,
231    current_page: u32,
232    has_more: bool,
233    buffer: Vec<Album>,
234    total_pages: Option<u32>,
235}
236
237#[async_trait(?Send)]
238impl AsyncPaginatedIterator<Album> for ArtistAlbumsIterator {
239    async fn next(&mut self) -> Result<Option<Album>> {
240        // If buffer is empty, try to load next page
241        if self.buffer.is_empty() {
242            if let Some(page) = self.next_page().await? {
243                self.buffer = page.albums;
244                self.buffer.reverse(); // Reverse so we can pop from end efficiently
245            }
246        }
247
248        Ok(self.buffer.pop())
249    }
250
251    fn current_page(&self) -> u32 {
252        self.current_page.saturating_sub(1)
253    }
254}
255
256impl ArtistAlbumsIterator {
257    /// Create a new artist albums iterator.
258    ///
259    /// This is typically called via [`LastFmEditClient::artist_albums`](crate::LastFmEditClient::artist_albums).
260    pub fn new(client: LastFmEditClientImpl, artist: String) -> Self {
261        Self {
262            client,
263            artist,
264            current_page: 1,
265            has_more: true,
266            buffer: Vec::new(),
267            total_pages: None,
268        }
269    }
270
271    /// Fetch the next page of albums.
272    ///
273    /// This method handles pagination automatically and includes rate limiting.
274    pub async fn next_page(&mut self) -> Result<Option<AlbumPage>> {
275        if !self.has_more {
276            return Ok(None);
277        }
278
279        let page = self
280            .client
281            .get_artist_albums_page(&self.artist, self.current_page)
282            .await?;
283
284        self.has_more = page.has_next_page;
285        self.current_page += 1;
286        self.total_pages = page.total_pages;
287
288        Ok(Some(page))
289    }
290
291    /// Get the total number of pages, if known.
292    ///
293    /// Returns `None` until at least one page has been fetched.
294    pub fn total_pages(&self) -> Option<u32> {
295        self.total_pages
296    }
297}
298
299/// Iterator for browsing a user's recent tracks/scrobbles.
300///
301/// This iterator provides access to the user's recent listening history with timestamps,
302/// which is essential for finding tracks that can be edited. It supports optional
303/// timestamp-based filtering to avoid reprocessing old data.
304///
305/// # Examples
306///
307/// ```rust,no_run
308/// # use lastfm_edit::{LastFmEditClient, LastFmEditClientImpl, AsyncPaginatedIterator};
309/// # tokio_test::block_on(async {
310/// let mut client = LastFmEditClientImpl::new(Box::new(http_client::native::NativeClient::new()));
311/// // client.login(...).await?;
312///
313/// // Get recent tracks with timestamps
314/// let mut recent = client.recent_tracks();
315/// while let Some(track) = recent.next().await? {
316///     if let Some(timestamp) = track.timestamp {
317///         println!("{} - {} ({})", track.artist, track.name, timestamp);
318///     }
319/// }
320///
321/// // Or stop at a specific timestamp to avoid reprocessing
322/// let last_processed = 1640995200;
323/// let mut recent = client.recent_tracks().with_stop_timestamp(last_processed);
324/// let new_tracks = recent.collect_all().await?;
325/// # Ok::<(), Box<dyn std::error::Error>>(())
326/// # });
327/// ```
328pub struct RecentTracksIterator {
329    client: LastFmEditClientImpl,
330    current_page: u32,
331    has_more: bool,
332    buffer: Vec<Track>,
333    stop_at_timestamp: Option<u64>,
334}
335
336#[async_trait(?Send)]
337impl AsyncPaginatedIterator<Track> for RecentTracksIterator {
338    async fn next(&mut self) -> Result<Option<Track>> {
339        // If buffer is empty, try to load next page
340        if self.buffer.is_empty() {
341            if !self.has_more {
342                return Ok(None);
343            }
344
345            let tracks = self.client.get_recent_scrobbles(self.current_page).await?;
346
347            if tracks.is_empty() {
348                self.has_more = false;
349                return Ok(None);
350            }
351
352            // Check if we should stop based on timestamp
353            if let Some(stop_timestamp) = self.stop_at_timestamp {
354                let mut filtered_tracks = Vec::new();
355                for track in tracks {
356                    if let Some(track_timestamp) = track.timestamp {
357                        if track_timestamp <= stop_timestamp {
358                            self.has_more = false;
359                            break;
360                        }
361                    }
362                    filtered_tracks.push(track);
363                }
364                self.buffer = filtered_tracks;
365            } else {
366                self.buffer = tracks;
367            }
368
369            self.buffer.reverse(); // Reverse so we can pop from end efficiently
370            self.current_page += 1;
371        }
372
373        Ok(self.buffer.pop())
374    }
375
376    fn current_page(&self) -> u32 {
377        self.current_page.saturating_sub(1)
378    }
379}
380
381impl RecentTracksIterator {
382    /// Create a new recent tracks iterator starting from page 1.
383    ///
384    /// This is typically called via [`LastFmEditClient::recent_tracks`](crate::LastFmEditClient::recent_tracks).
385    pub fn new(client: LastFmEditClientImpl) -> Self {
386        Self::with_starting_page(client, 1)
387    }
388
389    /// Create a new recent tracks iterator starting from a specific page.
390    ///
391    /// This allows resuming pagination from an arbitrary page, useful for
392    /// continuing from where a previous iteration left off.
393    ///
394    /// # Arguments
395    ///
396    /// * `client` - The LastFmEditClient to use for API calls
397    /// * `starting_page` - The page number to start from (1-indexed)
398    ///
399    /// # Examples
400    ///
401    /// ```rust,no_run
402    /// # use lastfm_edit::{LastFmEditClient, LastFmEditClientImpl, AsyncPaginatedIterator};
403    /// # tokio_test::block_on(async {
404    /// let mut client = LastFmEditClientImpl::new(Box::new(http_client::native::NativeClient::new()));
405    ///
406    /// // Start from page 5
407    /// let mut recent = client.recent_tracks_from_page(5);
408    /// let tracks = recent.take(10).await?;
409    /// # Ok::<(), Box<dyn std::error::Error>>(())
410    /// # });
411    /// ```
412    pub fn with_starting_page(client: LastFmEditClientImpl, starting_page: u32) -> Self {
413        let page = std::cmp::max(1, starting_page);
414        Self {
415            client,
416            current_page: page,
417            has_more: true,
418            buffer: Vec::new(),
419            stop_at_timestamp: None,
420        }
421    }
422
423    /// Set a timestamp to stop iteration at.
424    ///
425    /// When this is set, the iterator will stop returning tracks once it encounters
426    /// a track with a timestamp less than or equal to the specified value. This is
427    /// useful for incremental processing to avoid reprocessing old data.
428    ///
429    /// # Arguments
430    ///
431    /// * `timestamp` - Unix timestamp to stop at
432    ///
433    /// # Examples
434    ///
435    /// ```rust,no_run
436    /// # use lastfm_edit::{LastFmEditClient, LastFmEditClientImpl, AsyncPaginatedIterator};
437    /// # tokio_test::block_on(async {
438    /// let mut client = LastFmEditClientImpl::new(Box::new(http_client::native::NativeClient::new()));
439    /// let last_processed = 1640995200; // Some previous timestamp
440    ///
441    /// let mut recent = client.recent_tracks().with_stop_timestamp(last_processed);
442    /// let new_tracks = recent.collect_all().await?; // Only gets new tracks
443    /// # Ok::<(), Box<dyn std::error::Error>>(())
444    /// # });
445    /// ```
446    pub fn with_stop_timestamp(mut self, timestamp: u64) -> Self {
447        self.stop_at_timestamp = Some(timestamp);
448        self
449    }
450}