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