lastfm_edit/
iterator.rs

1use crate::{Album, AlbumPage, LastFmClient, 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::{LastFmClient, AsyncPaginatedIterator};
13///
14/// # tokio_test::block_on(async {
15/// let mut client = LastFmClient::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::{LastFmClient, AsyncPaginatedIterator};
57    /// # tokio_test::block_on(async {
58    /// let mut client = LastFmClient::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::{LastFmClient, AsyncPaginatedIterator};
86    /// # tokio_test::block_on(async {
87    /// let mut client = LastFmClient::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::{LastFmClient, AsyncPaginatedIterator};
120/// # tokio_test::block_on(async {
121/// let mut client = LastFmClient::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 mut LastFmClient,
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 [`LastFmClient::artist_tracks`](crate::LastFmClient::artist_tracks).
167    pub fn new(client: &'a mut LastFmClient, 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        // Add a small delay for paginated requests to be polite to the server
188        if self.current_page > 1 {
189            tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
190        }
191
192        let page = self
193            .client
194            .get_artist_tracks_page(&self.artist, self.current_page)
195            .await?;
196
197        self.has_more = page.has_next_page;
198        self.current_page += 1;
199        self.total_pages = page.total_pages;
200
201        Ok(Some(page))
202    }
203
204    /// Get the total number of pages, if known.
205    ///
206    /// Returns `None` until at least one page has been fetched.
207    pub fn total_pages(&self) -> Option<u32> {
208        self.total_pages
209    }
210}
211
212/// Iterator for browsing an artist's albums from a user's library.
213///
214/// This iterator provides paginated access to all albums by a specific artist
215/// in the authenticated user's Last.fm library, ordered by play count.
216///
217/// # Examples
218///
219/// ```rust,no_run
220/// # use lastfm_edit::{LastFmClient, AsyncPaginatedIterator};
221/// # tokio_test::block_on(async {
222/// let mut client = LastFmClient::new(Box::new(http_client::native::NativeClient::new()));
223/// // client.login(...).await?;
224///
225/// let mut albums = client.artist_albums("Pink Floyd");
226///
227/// // Get all albums (be careful with large discographies!)
228/// while let Some(album) = albums.next().await? {
229///     println!("{} (played {} times)", album.name, album.playcount);
230/// }
231/// # Ok::<(), Box<dyn std::error::Error>>(())
232/// # });
233/// ```
234pub struct ArtistAlbumsIterator<'a> {
235    client: &'a mut LastFmClient,
236    artist: String,
237    current_page: u32,
238    has_more: bool,
239    buffer: Vec<Album>,
240    total_pages: Option<u32>,
241}
242
243impl<'a> AsyncPaginatedIterator for ArtistAlbumsIterator<'a> {
244    type Item = Album;
245
246    async fn next(&mut self) -> Result<Option<Self::Item>> {
247        // If buffer is empty, try to load next page
248        if self.buffer.is_empty() {
249            if let Some(page) = self.next_page().await? {
250                self.buffer = page.albums;
251                self.buffer.reverse(); // Reverse so we can pop from end efficiently
252            }
253        }
254
255        Ok(self.buffer.pop())
256    }
257
258    fn current_page(&self) -> u32 {
259        self.current_page.saturating_sub(1)
260    }
261}
262
263impl<'a> ArtistAlbumsIterator<'a> {
264    /// Create a new artist albums iterator.
265    ///
266    /// This is typically called via [`LastFmClient::artist_albums`](crate::LastFmClient::artist_albums).
267    pub fn new(client: &'a mut LastFmClient, artist: String) -> Self {
268        Self {
269            client,
270            artist,
271            current_page: 1,
272            has_more: true,
273            buffer: Vec::new(),
274            total_pages: None,
275        }
276    }
277
278    /// Fetch the next page of albums.
279    ///
280    /// This method handles pagination automatically and includes rate limiting.
281    pub async fn next_page(&mut self) -> Result<Option<AlbumPage>> {
282        if !self.has_more {
283            return Ok(None);
284        }
285
286        // Add a small delay for paginated requests to be polite to the server
287        if self.current_page > 1 {
288            tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
289        }
290
291        let page = self
292            .client
293            .get_artist_albums_page(&self.artist, self.current_page)
294            .await?;
295
296        self.has_more = page.has_next_page;
297        self.current_page += 1;
298        self.total_pages = page.total_pages;
299
300        Ok(Some(page))
301    }
302
303    /// Get the total number of pages, if known.
304    ///
305    /// Returns `None` until at least one page has been fetched.
306    pub fn total_pages(&self) -> Option<u32> {
307        self.total_pages
308    }
309}
310
311/// Iterator for browsing a user's recent tracks/scrobbles.
312///
313/// This iterator provides access to the user's recent listening history with timestamps,
314/// which is essential for finding tracks that can be edited. It supports optional
315/// timestamp-based filtering to avoid reprocessing old data.
316///
317/// # Examples
318///
319/// ```rust,no_run
320/// # use lastfm_edit::{LastFmClient, AsyncPaginatedIterator};
321/// # tokio_test::block_on(async {
322/// let mut client = LastFmClient::new(Box::new(http_client::native::NativeClient::new()));
323/// // client.login(...).await?;
324///
325/// // Get recent tracks with timestamps
326/// let mut recent = client.recent_tracks();
327/// while let Some(track) = recent.next().await? {
328///     if let Some(timestamp) = track.timestamp {
329///         println!("{} - {} ({})", track.artist, track.name, timestamp);
330///     }
331/// }
332///
333/// // Or stop at a specific timestamp to avoid reprocessing
334/// let last_processed = 1640995200;
335/// let mut recent = client.recent_tracks().with_stop_timestamp(last_processed);
336/// let new_tracks = recent.collect_all().await?;
337/// # Ok::<(), Box<dyn std::error::Error>>(())
338/// # });
339/// ```
340pub struct RecentTracksIterator<'a> {
341    client: &'a mut LastFmClient,
342    current_page: u32,
343    has_more: bool,
344    buffer: Vec<Track>,
345    stop_at_timestamp: Option<u64>,
346}
347
348impl<'a> AsyncPaginatedIterator for RecentTracksIterator<'a> {
349    type Item = Track;
350
351    async fn next(&mut self) -> Result<Option<Self::Item>> {
352        // If buffer is empty, try to load next page
353        if self.buffer.is_empty() {
354            if !self.has_more {
355                return Ok(None);
356            }
357
358            // Add a small delay for paginated requests to be polite to the server
359            if self.current_page > 1 {
360                tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
361            }
362
363            let tracks = self.client.get_recent_scrobbles(self.current_page).await?;
364
365            if tracks.is_empty() {
366                self.has_more = false;
367                return Ok(None);
368            }
369
370            // Check if we should stop based on timestamp
371            if let Some(stop_timestamp) = self.stop_at_timestamp {
372                let mut filtered_tracks = Vec::new();
373                for track in tracks {
374                    if let Some(track_timestamp) = track.timestamp {
375                        if track_timestamp <= stop_timestamp {
376                            self.has_more = false;
377                            break;
378                        }
379                    }
380                    filtered_tracks.push(track);
381                }
382                self.buffer = filtered_tracks;
383            } else {
384                self.buffer = tracks;
385            }
386
387            self.buffer.reverse(); // Reverse so we can pop from end efficiently
388            self.current_page += 1;
389        }
390
391        Ok(self.buffer.pop())
392    }
393
394    fn current_page(&self) -> u32 {
395        self.current_page.saturating_sub(1)
396    }
397}
398
399impl<'a> RecentTracksIterator<'a> {
400    /// Create a new recent tracks iterator.
401    ///
402    /// This is typically called via [`LastFmClient::recent_tracks`](crate::LastFmClient::recent_tracks).
403    pub fn new(client: &'a mut LastFmClient) -> Self {
404        Self {
405            client,
406            current_page: 1,
407            has_more: true,
408            buffer: Vec::new(),
409            stop_at_timestamp: None,
410        }
411    }
412
413    /// Set a timestamp to stop iteration at.
414    ///
415    /// When this is set, the iterator will stop returning tracks once it encounters
416    /// a track with a timestamp less than or equal to the specified value. This is
417    /// useful for incremental processing to avoid reprocessing old data.
418    ///
419    /// # Arguments
420    ///
421    /// * `timestamp` - Unix timestamp to stop at
422    ///
423    /// # Examples
424    ///
425    /// ```rust,no_run
426    /// # use lastfm_edit::{LastFmClient, AsyncPaginatedIterator};
427    /// # tokio_test::block_on(async {
428    /// let mut client = LastFmClient::new(Box::new(http_client::native::NativeClient::new()));
429    /// let last_processed = 1640995200; // Some previous timestamp
430    ///
431    /// let mut recent = client.recent_tracks().with_stop_timestamp(last_processed);
432    /// let new_tracks = recent.collect_all().await?; // Only gets new tracks
433    /// # Ok::<(), Box<dyn std::error::Error>>(())
434    /// # });
435    /// ```
436    pub fn with_stop_timestamp(mut self, timestamp: u64) -> Self {
437        self.stop_at_timestamp = Some(timestamp);
438        self
439    }
440}