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}