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}