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 /// Get the total number of pages, if known.
114 ///
115 /// Returns `Some(n)` if the total page count is known, `None` otherwise.
116 /// This information may not be available until at least one page has been fetched.
117 fn total_pages(&self) -> Option<u32> {
118 None // Default implementation returns None
119 }
120}
121
122/// Iterator for browsing an artist's tracks from a user's library.
123///
124/// This iterator provides access to all tracks by a specific artist
125/// in the authenticated user's Last.fm library. Unlike the basic track listing,
126/// this iterator fetches tracks by iterating through the artist's albums first,
127/// which provides complete album information for each track.
128///
129/// The iterator loads albums and their tracks as needed and handles rate limiting
130/// automatically to be respectful to Last.fm's servers.
131///
132/// # Examples
133///
134/// ```rust,no_run
135/// # use lastfm_edit::{LastFmEditClient, LastFmEditClientImpl, LastFmEditSession, AsyncPaginatedIterator};
136/// # tokio_test::block_on(async {
137/// # let test_session = LastFmEditSession::new("test".to_string(), vec!["sessionid=.test123".to_string()], Some("csrf".to_string()), "https://www.last.fm".to_string());
138/// let mut client = LastFmEditClientImpl::from_session(Box::new(http_client::native::NativeClient::new()), test_session);
139///
140/// let mut tracks = client.artist_tracks("The Beatles");
141///
142/// // Get the top 5 tracks with album information
143/// let top_tracks = tracks.take(5).await?;
144/// for track in top_tracks {
145/// let album = track.album.as_deref().unwrap_or("Unknown Album");
146/// println!("{} [{}] (played {} times)", track.name, album, track.playcount);
147/// }
148/// # Ok::<(), Box<dyn std::error::Error>>(())
149/// # });
150/// ```
151pub struct ArtistTracksIterator<C: LastFmEditClient> {
152 client: C,
153 artist: String,
154 album_iterator: Option<ArtistAlbumsIterator<C>>,
155 current_album_tracks: Option<AlbumTracksIterator<C>>,
156 track_buffer: Vec<Track>,
157 finished: bool,
158}
159
160#[async_trait(?Send)]
161impl<C: LastFmEditClient + Clone> AsyncPaginatedIterator<Track> for ArtistTracksIterator<C> {
162 async fn next(&mut self) -> Result<Option<Track>> {
163 // If we're finished, return None
164 if self.finished {
165 return Ok(None);
166 }
167
168 // If track buffer is empty, try to get more tracks
169 while self.track_buffer.is_empty() {
170 // If we don't have a current album tracks iterator, get the next album
171 if self.current_album_tracks.is_none() {
172 // Initialize album iterator if needed
173 if self.album_iterator.is_none() {
174 self.album_iterator = Some(ArtistAlbumsIterator::new(
175 self.client.clone(),
176 self.artist.clone(),
177 ));
178 }
179
180 // Get next album
181 if let Some(ref mut album_iter) = self.album_iterator {
182 if let Some(album) = album_iter.next().await? {
183 log::debug!(
184 "Processing album '{}' for artist '{}'",
185 album.name,
186 self.artist
187 );
188 // Create album tracks iterator for this album
189 self.current_album_tracks = Some(AlbumTracksIterator::new(
190 self.client.clone(),
191 album.name.clone(),
192 self.artist.clone(),
193 ));
194 } else {
195 // No more albums, we're done
196 log::debug!("No more albums for artist '{}'", self.artist);
197 self.finished = true;
198 return Ok(None);
199 }
200 }
201 }
202
203 // Get tracks from current album
204 if let Some(ref mut album_tracks) = self.current_album_tracks {
205 if let Some(track) = album_tracks.next().await? {
206 self.track_buffer.push(track);
207 } else {
208 // This album is exhausted, move to next album
209 log::debug!(
210 "Finished processing current album for artist '{}'",
211 self.artist
212 );
213 self.current_album_tracks = None;
214 // Continue the loop to try getting the next album
215 }
216 }
217 }
218
219 // Return the next track from our buffer
220 Ok(self.track_buffer.pop())
221 }
222
223 fn current_page(&self) -> u32 {
224 // Since we're iterating through albums, return the album iterator's current page
225 if let Some(ref album_iter) = self.album_iterator {
226 album_iter.current_page()
227 } else {
228 0
229 }
230 }
231
232 fn total_pages(&self) -> Option<u32> {
233 // Since we're iterating through albums, return the album iterator's total pages
234 if let Some(ref album_iter) = self.album_iterator {
235 album_iter.total_pages()
236 } else {
237 None
238 }
239 }
240}
241
242impl<C: LastFmEditClient + Clone> ArtistTracksIterator<C> {
243 /// Create a new artist tracks iterator.
244 ///
245 /// This is typically called via [`LastFmEditClient::artist_tracks`](crate::LastFmEditClient::artist_tracks).
246 pub fn new(client: C, artist: String) -> Self {
247 Self {
248 client,
249 artist,
250 album_iterator: None,
251 current_album_tracks: None,
252 track_buffer: Vec::new(),
253 finished: false,
254 }
255 }
256}
257
258/// Iterator for browsing an artist's tracks directly using the paginated artist tracks endpoint.
259///
260/// This iterator provides access to all tracks by a specific artist
261/// in the authenticated user's Last.fm library by directly using the
262/// `/user/{username}/library/music/{artist}/+tracks` endpoint with pagination.
263/// This is more efficient than the album-based approach as it doesn't need to
264/// iterate through albums first.
265///
266/// # Examples
267///
268/// ```rust,no_run
269/// # use lastfm_edit::{LastFmEditClient, LastFmEditClientImpl, LastFmEditSession, AsyncPaginatedIterator};
270/// # tokio_test::block_on(async {
271/// # let test_session = LastFmEditSession::new("test".to_string(), vec!["sessionid=.test123".to_string()], Some("csrf".to_string()), "https://www.last.fm".to_string());
272/// let mut client = LastFmEditClientImpl::from_session(Box::new(http_client::native::NativeClient::new()), test_session);
273///
274/// let mut tracks = client.artist_tracks_direct("The Beatles");
275///
276/// // Get the first 10 tracks directly from the paginated endpoint
277/// let first_10_tracks = tracks.take(10).await?;
278/// for track in first_10_tracks {
279/// println!("{} (played {} times)", track.name, track.playcount);
280/// }
281/// # Ok::<(), Box<dyn std::error::Error>>(())
282/// # });
283/// ```
284pub struct ArtistTracksDirectIterator<C: LastFmEditClient> {
285 client: C,
286 artist: String,
287 current_page: u32,
288 has_more: bool,
289 buffer: Vec<Track>,
290 total_pages: Option<u32>,
291 tracks_yielded: u32,
292}
293
294#[async_trait(?Send)]
295impl<C: LastFmEditClient> AsyncPaginatedIterator<Track> for ArtistTracksDirectIterator<C> {
296 async fn next(&mut self) -> Result<Option<Track>> {
297 // If buffer is empty, try to load next page
298 if self.buffer.is_empty() {
299 if let Some(page) = self.next_page().await? {
300 self.buffer = page.tracks;
301 self.buffer.reverse(); // Reverse so we can pop from end efficiently
302 }
303 }
304
305 if let Some(track) = self.buffer.pop() {
306 self.tracks_yielded += 1;
307 Ok(Some(track))
308 } else {
309 Ok(None)
310 }
311 }
312
313 fn current_page(&self) -> u32 {
314 self.current_page.saturating_sub(1)
315 }
316
317 fn total_pages(&self) -> Option<u32> {
318 self.total_pages
319 }
320}
321
322impl<C: LastFmEditClient> ArtistTracksDirectIterator<C> {
323 /// Create a new direct artist tracks iterator.
324 ///
325 /// This is typically called via [`LastFmEditClient::artist_tracks_direct`](crate::LastFmEditClient::artist_tracks_direct).
326 pub fn new(client: C, artist: String) -> Self {
327 Self {
328 client,
329 artist,
330 current_page: 1,
331 has_more: true,
332 buffer: Vec::new(),
333 total_pages: None,
334 tracks_yielded: 0,
335 }
336 }
337
338 /// Fetch the next page of tracks.
339 ///
340 /// This method handles pagination automatically and includes rate limiting.
341 pub async fn next_page(&mut self) -> Result<Option<TrackPage>> {
342 if !self.has_more {
343 return Ok(None);
344 }
345
346 log::debug!(
347 "Fetching page {} of {} tracks (yielded {} tracks so far)",
348 self.current_page,
349 self.artist,
350 self.tracks_yielded
351 );
352
353 let page = self
354 .client
355 .get_artist_tracks_page(&self.artist, self.current_page)
356 .await?;
357
358 self.has_more = page.has_next_page;
359 self.current_page += 1;
360 self.total_pages = page.total_pages;
361
362 Ok(Some(page))
363 }
364
365 /// Get the total number of pages, if known.
366 ///
367 /// Returns `None` until at least one page has been fetched.
368 pub fn total_pages(&self) -> Option<u32> {
369 self.total_pages
370 }
371}
372
373/// Iterator for browsing an artist's albums from a user's library.
374///
375/// This iterator provides paginated access to all albums by a specific artist
376/// in the authenticated user's Last.fm library, ordered by play count.
377///
378/// # Examples
379///
380/// ```rust,no_run
381/// # use lastfm_edit::{LastFmEditClient, LastFmEditClientImpl, LastFmEditSession, AsyncPaginatedIterator};
382/// # tokio_test::block_on(async {
383/// # let test_session = LastFmEditSession::new("test".to_string(), vec!["sessionid=.test123".to_string()], Some("csrf".to_string()), "https://www.last.fm".to_string());
384/// let mut client = LastFmEditClientImpl::from_session(Box::new(http_client::native::NativeClient::new()), test_session);
385///
386/// let mut albums = client.artist_albums("Pink Floyd");
387///
388/// // Get all albums (be careful with large discographies!)
389/// while let Some(album) = albums.next().await? {
390/// println!("{} (played {} times)", album.name, album.playcount);
391/// }
392/// # Ok::<(), Box<dyn std::error::Error>>(())
393/// # });
394/// ```
395pub struct ArtistAlbumsIterator<C: LastFmEditClient> {
396 client: C,
397 artist: String,
398 current_page: u32,
399 has_more: bool,
400 buffer: Vec<Album>,
401 total_pages: Option<u32>,
402}
403
404#[async_trait(?Send)]
405impl<C: LastFmEditClient> AsyncPaginatedIterator<Album> for ArtistAlbumsIterator<C> {
406 async fn next(&mut self) -> Result<Option<Album>> {
407 // If buffer is empty, try to load next page
408 if self.buffer.is_empty() {
409 if let Some(page) = self.next_page().await? {
410 self.buffer = page.albums;
411 self.buffer.reverse(); // Reverse so we can pop from end efficiently
412 }
413 }
414
415 Ok(self.buffer.pop())
416 }
417
418 fn current_page(&self) -> u32 {
419 self.current_page.saturating_sub(1)
420 }
421
422 fn total_pages(&self) -> Option<u32> {
423 self.total_pages
424 }
425}
426
427impl<C: LastFmEditClient> ArtistAlbumsIterator<C> {
428 /// Create a new artist albums iterator.
429 ///
430 /// This is typically called via [`LastFmEditClient::artist_albums`](crate::LastFmEditClient::artist_albums).
431 pub fn new(client: C, artist: String) -> Self {
432 Self {
433 client,
434 artist,
435 current_page: 1,
436 has_more: true,
437 buffer: Vec::new(),
438 total_pages: None,
439 }
440 }
441
442 /// Fetch the next page of albums.
443 ///
444 /// This method handles pagination automatically and includes rate limiting.
445 pub async fn next_page(&mut self) -> Result<Option<AlbumPage>> {
446 if !self.has_more {
447 return Ok(None);
448 }
449
450 let page = self
451 .client
452 .get_artist_albums_page(&self.artist, self.current_page)
453 .await?;
454
455 self.has_more = page.has_next_page;
456 self.current_page += 1;
457 self.total_pages = page.total_pages;
458
459 Ok(Some(page))
460 }
461
462 /// Get the total number of pages, if known.
463 ///
464 /// Returns `None` until at least one page has been fetched.
465 pub fn total_pages(&self) -> Option<u32> {
466 self.total_pages
467 }
468}
469
470/// Iterator for browsing a user's recent tracks/scrobbles.
471///
472/// This iterator provides access to the user's recent listening history with timestamps,
473/// which is essential for finding tracks that can be edited. It supports optional
474/// timestamp-based filtering to avoid reprocessing old data.
475///
476/// # Examples
477///
478/// ```rust,no_run
479/// # use lastfm_edit::{LastFmEditClient, LastFmEditClientImpl, LastFmEditSession, AsyncPaginatedIterator};
480/// # tokio_test::block_on(async {
481/// # let test_session = LastFmEditSession::new("test".to_string(), vec!["sessionid=.test123".to_string()], Some("csrf".to_string()), "https://www.last.fm".to_string());
482/// let mut client = LastFmEditClientImpl::from_session(Box::new(http_client::native::NativeClient::new()), test_session);
483///
484/// // Get recent tracks with timestamps
485/// let mut recent = client.recent_tracks();
486/// while let Some(track) = recent.next().await? {
487/// if let Some(timestamp) = track.timestamp {
488/// println!("{} - {} ({})", track.artist, track.name, timestamp);
489/// }
490/// }
491///
492/// // Or stop at a specific timestamp to avoid reprocessing
493/// let last_processed = 1640995200;
494/// let mut recent = lastfm_edit::RecentTracksIterator::new(client).with_stop_timestamp(last_processed);
495/// let new_tracks = recent.collect_all().await?;
496/// # Ok::<(), Box<dyn std::error::Error>>(())
497/// # });
498/// ```
499pub struct RecentTracksIterator<C: LastFmEditClient> {
500 client: C,
501 current_page: u32,
502 has_more: bool,
503 buffer: Vec<Track>,
504 stop_at_timestamp: Option<u64>,
505}
506
507#[async_trait(?Send)]
508impl<C: LastFmEditClient> AsyncPaginatedIterator<Track> for RecentTracksIterator<C> {
509 async fn next(&mut self) -> Result<Option<Track>> {
510 // If buffer is empty, try to load next page
511 if self.buffer.is_empty() {
512 if !self.has_more {
513 return Ok(None);
514 }
515
516 let tracks = self.client.get_recent_scrobbles(self.current_page).await?;
517
518 if tracks.is_empty() {
519 self.has_more = false;
520 return Ok(None);
521 }
522
523 // Check if we should stop based on timestamp
524 if let Some(stop_timestamp) = self.stop_at_timestamp {
525 let mut filtered_tracks = Vec::new();
526 for track in tracks {
527 if let Some(track_timestamp) = track.timestamp {
528 if track_timestamp <= stop_timestamp {
529 self.has_more = false;
530 break;
531 }
532 }
533 filtered_tracks.push(track);
534 }
535 self.buffer = filtered_tracks;
536 } else {
537 self.buffer = tracks;
538 }
539
540 self.buffer.reverse(); // Reverse so we can pop from end efficiently
541 self.current_page += 1;
542 }
543
544 Ok(self.buffer.pop())
545 }
546
547 fn current_page(&self) -> u32 {
548 self.current_page.saturating_sub(1)
549 }
550}
551
552impl<C: LastFmEditClient> RecentTracksIterator<C> {
553 /// Create a new recent tracks iterator starting from page 1.
554 ///
555 /// This is typically called via [`LastFmEditClient::recent_tracks`](crate::LastFmEditClient::recent_tracks).
556 pub fn new(client: C) -> Self {
557 Self::with_starting_page(client, 1)
558 }
559
560 /// Create a new recent tracks iterator starting from a specific page.
561 ///
562 /// This allows resuming pagination from an arbitrary page, useful for
563 /// continuing from where a previous iteration left off.
564 ///
565 /// # Arguments
566 ///
567 /// * `client` - The LastFmEditClient to use for API calls
568 /// * `starting_page` - The page number to start from (1-indexed)
569 ///
570 /// # Examples
571 ///
572 /// ```rust,no_run
573 /// # use lastfm_edit::{LastFmEditClient, LastFmEditClientImpl, LastFmEditSession, AsyncPaginatedIterator};
574 /// # tokio_test::block_on(async {
575 /// # let test_session = LastFmEditSession::new("test".to_string(), vec!["sessionid=.test123".to_string()], Some("csrf".to_string()), "https://www.last.fm".to_string());
576 /// let mut client = LastFmEditClientImpl::from_session(Box::new(http_client::native::NativeClient::new()), test_session);
577 ///
578 /// // Start from page 5
579 /// let mut recent = client.recent_tracks_from_page(5);
580 /// let tracks = recent.take(10).await?;
581 /// # Ok::<(), Box<dyn std::error::Error>>(())
582 /// # });
583 /// ```
584 pub fn with_starting_page(client: C, starting_page: u32) -> Self {
585 let page = std::cmp::max(1, starting_page);
586 Self {
587 client,
588 current_page: page,
589 has_more: true,
590 buffer: Vec::new(),
591 stop_at_timestamp: None,
592 }
593 }
594
595 /// Set a timestamp to stop iteration at.
596 ///
597 /// When this is set, the iterator will stop returning tracks once it encounters
598 /// a track with a timestamp less than or equal to the specified value. This is
599 /// useful for incremental processing to avoid reprocessing old data.
600 ///
601 /// # Arguments
602 ///
603 /// * `timestamp` - Unix timestamp to stop at
604 ///
605 /// # Examples
606 ///
607 /// ```rust,no_run
608 /// # use lastfm_edit::{LastFmEditClient, LastFmEditClientImpl, LastFmEditSession, AsyncPaginatedIterator};
609 /// # tokio_test::block_on(async {
610 /// # let test_session = LastFmEditSession::new("test".to_string(), vec!["sessionid=.test123".to_string()], Some("csrf".to_string()), "https://www.last.fm".to_string());
611 /// let mut client = LastFmEditClientImpl::from_session(Box::new(http_client::native::NativeClient::new()), test_session);
612 /// let last_processed = 1640995200; // Some previous timestamp
613 ///
614 /// let mut recent = lastfm_edit::RecentTracksIterator::new(client).with_stop_timestamp(last_processed);
615 /// let new_tracks = recent.collect_all().await?; // Only gets new tracks
616 /// # Ok::<(), Box<dyn std::error::Error>>(())
617 /// # });
618 /// ```
619 pub fn with_stop_timestamp(mut self, timestamp: u64) -> Self {
620 self.stop_at_timestamp = Some(timestamp);
621 self
622 }
623}
624
625/// Iterator for browsing tracks in a specific album from a user's library.
626///
627/// This iterator provides access to all tracks in a specific album by an artist
628/// in the authenticated user's Last.fm library. Unlike paginated iterators,
629/// this loads tracks once and iterates through them.
630///
631/// # Examples
632///
633/// ```rust,no_run
634/// # use lastfm_edit::{LastFmEditClient, LastFmEditClientImpl, LastFmEditSession, AsyncPaginatedIterator};
635/// # tokio_test::block_on(async {
636/// # let test_session = LastFmEditSession::new("test".to_string(), vec!["sessionid=.test123".to_string()], Some("csrf".to_string()), "https://www.last.fm".to_string());
637/// let mut client = LastFmEditClientImpl::from_session(Box::new(http_client::native::NativeClient::new()), test_session);
638///
639/// let mut tracks = client.album_tracks("The Dark Side of the Moon", "Pink Floyd");
640///
641/// // Get all tracks in the album
642/// while let Some(track) = tracks.next().await? {
643/// println!("{} - {}", track.name, track.artist);
644/// }
645/// # Ok::<(), Box<dyn std::error::Error>>(())
646/// # });
647/// ```
648pub struct AlbumTracksIterator<C: LastFmEditClient> {
649 client: C,
650 album_name: String,
651 artist_name: String,
652 tracks: Option<Vec<Track>>,
653 index: usize,
654}
655
656#[async_trait(?Send)]
657impl<C: LastFmEditClient> AsyncPaginatedIterator<Track> for AlbumTracksIterator<C> {
658 async fn next(&mut self) -> Result<Option<Track>> {
659 // Load tracks if not already loaded
660 if self.tracks.is_none() {
661 // Use get_album_tracks_page instead of get_album_tracks to avoid infinite recursion
662 let tracks_page = self
663 .client
664 .get_album_tracks_page(&self.album_name, &self.artist_name, 1)
665 .await?;
666 log::debug!(
667 "Album '{}' by '{}' has {} tracks: {:?}",
668 self.album_name,
669 self.artist_name,
670 tracks_page.tracks.len(),
671 tracks_page
672 .tracks
673 .iter()
674 .map(|t| &t.name)
675 .collect::<Vec<_>>()
676 );
677
678 if tracks_page.tracks.is_empty() {
679 log::warn!(
680 "🚨 ZERO TRACKS FOUND for album '{}' by '{}' - investigating...",
681 self.album_name,
682 self.artist_name
683 );
684 log::debug!("Full TrackPage for empty album: has_next_page={}, page_number={}, total_pages={:?}",
685 tracks_page.has_next_page, tracks_page.page_number, tracks_page.total_pages);
686 }
687 self.tracks = Some(tracks_page.tracks);
688 }
689
690 // Return next track
691 if let Some(tracks) = &self.tracks {
692 if self.index < tracks.len() {
693 let track = tracks[self.index].clone();
694 self.index += 1;
695 Ok(Some(track))
696 } else {
697 Ok(None)
698 }
699 } else {
700 Ok(None)
701 }
702 }
703
704 fn current_page(&self) -> u32 {
705 // Album tracks don't have pages, so return 0
706 0
707 }
708}
709
710impl<C: LastFmEditClient> AlbumTracksIterator<C> {
711 /// Create a new album tracks iterator.
712 ///
713 /// This is typically called via [`LastFmEditClient::album_tracks`](crate::LastFmEditClient::album_tracks).
714 pub fn new(client: C, album_name: String, artist_name: String) -> Self {
715 Self {
716 client,
717 album_name,
718 artist_name,
719 tracks: None,
720 index: 0,
721 }
722 }
723}
724
725/// Iterator for searching tracks in the user's library.
726///
727/// This iterator provides paginated access to tracks that match a search query
728/// in the authenticated user's Last.fm library, using Last.fm's built-in search functionality.
729///
730/// # Examples
731///
732/// ```rust,no_run
733/// # use lastfm_edit::{LastFmEditClient, LastFmEditClientImpl, LastFmEditSession, AsyncPaginatedIterator};
734/// # tokio_test::block_on(async {
735/// # let test_session = LastFmEditSession::new("test".to_string(), vec!["sessionid=.test123".to_string()], Some("csrf".to_string()), "https://www.last.fm".to_string());
736/// let mut client = LastFmEditClientImpl::from_session(Box::new(http_client::native::NativeClient::new()), test_session);
737///
738/// let mut search_results = client.search_tracks("remaster");
739///
740/// // Get first 20 search results
741/// while let Some(track) = search_results.next().await? {
742/// println!("{} - {} (played {} times)", track.artist, track.name, track.playcount);
743/// }
744/// # Ok::<(), Box<dyn std::error::Error>>(())
745/// # });
746/// ```
747pub struct SearchTracksIterator<C: LastFmEditClient> {
748 client: C,
749 query: String,
750 current_page: u32,
751 has_more: bool,
752 buffer: Vec<Track>,
753 total_pages: Option<u32>,
754}
755
756#[async_trait(?Send)]
757impl<C: LastFmEditClient> AsyncPaginatedIterator<Track> for SearchTracksIterator<C> {
758 async fn next(&mut self) -> Result<Option<Track>> {
759 // If buffer is empty, try to load next page
760 if self.buffer.is_empty() {
761 if let Some(page) = self.next_page().await? {
762 self.buffer = page.tracks;
763 self.buffer.reverse(); // Reverse so we can pop from end efficiently
764 }
765 }
766
767 Ok(self.buffer.pop())
768 }
769
770 fn current_page(&self) -> u32 {
771 self.current_page.saturating_sub(1)
772 }
773
774 fn total_pages(&self) -> Option<u32> {
775 self.total_pages
776 }
777}
778
779impl<C: LastFmEditClient> SearchTracksIterator<C> {
780 /// Create a new search tracks iterator.
781 ///
782 /// This is typically called via [`LastFmEditClient::search_tracks`](crate::LastFmEditClient::search_tracks).
783 pub fn new(client: C, query: String) -> Self {
784 Self {
785 client,
786 query,
787 current_page: 1,
788 has_more: true,
789 buffer: Vec::new(),
790 total_pages: None,
791 }
792 }
793
794 /// Create a new search tracks iterator starting from a specific page.
795 ///
796 /// This is useful for implementing offset functionality efficiently by starting
797 /// at the appropriate page rather than iterating through all previous pages.
798 pub fn with_starting_page(client: C, query: String, starting_page: u32) -> Self {
799 let page = std::cmp::max(1, starting_page);
800 Self {
801 client,
802 query,
803 current_page: page,
804 has_more: true,
805 buffer: Vec::new(),
806 total_pages: None,
807 }
808 }
809
810 /// Fetch the next page of search results.
811 ///
812 /// This method handles pagination automatically and includes rate limiting
813 /// to be respectful to Last.fm's servers.
814 pub async fn next_page(&mut self) -> Result<Option<TrackPage>> {
815 if !self.has_more {
816 return Ok(None);
817 }
818
819 let page = self
820 .client
821 .search_tracks_page(&self.query, self.current_page)
822 .await?;
823
824 self.has_more = page.has_next_page;
825 self.current_page += 1;
826 self.total_pages = page.total_pages;
827
828 Ok(Some(page))
829 }
830
831 /// Get the total number of pages, if known.
832 ///
833 /// Returns `None` until at least one page has been fetched.
834 pub fn total_pages(&self) -> Option<u32> {
835 self.total_pages
836 }
837}
838
839/// Iterator for searching albums in the user's library.
840///
841/// This iterator provides paginated access to albums that match a search query
842/// in the authenticated user's Last.fm library, using Last.fm's built-in search functionality.
843///
844/// # Examples
845///
846/// ```rust,no_run
847/// # use lastfm_edit::{LastFmEditClient, LastFmEditClientImpl, LastFmEditSession, AsyncPaginatedIterator};
848/// # tokio_test::block_on(async {
849/// # let test_session = LastFmEditSession::new("test".to_string(), vec!["sessionid=.test123".to_string()], Some("csrf".to_string()), "https://www.last.fm".to_string());
850/// let mut client = LastFmEditClientImpl::from_session(Box::new(http_client::native::NativeClient::new()), test_session);
851///
852/// let mut search_results = client.search_albums("deluxe");
853///
854/// // Get first 10 search results
855/// let top_10 = search_results.take(10).await?;
856/// for album in top_10 {
857/// println!("{} - {} (played {} times)", album.artist, album.name, album.playcount);
858/// }
859/// # Ok::<(), Box<dyn std::error::Error>>(())
860/// # });
861/// ```
862pub struct SearchAlbumsIterator<C: LastFmEditClient> {
863 client: C,
864 query: String,
865 current_page: u32,
866 has_more: bool,
867 buffer: Vec<Album>,
868 total_pages: Option<u32>,
869}
870
871#[async_trait(?Send)]
872impl<C: LastFmEditClient> AsyncPaginatedIterator<Album> for SearchAlbumsIterator<C> {
873 async fn next(&mut self) -> Result<Option<Album>> {
874 // If buffer is empty, try to load next page
875 if self.buffer.is_empty() {
876 if let Some(page) = self.next_page().await? {
877 self.buffer = page.albums;
878 self.buffer.reverse(); // Reverse so we can pop from end efficiently
879 }
880 }
881
882 Ok(self.buffer.pop())
883 }
884
885 fn current_page(&self) -> u32 {
886 self.current_page.saturating_sub(1)
887 }
888
889 fn total_pages(&self) -> Option<u32> {
890 self.total_pages
891 }
892}
893
894impl<C: LastFmEditClient> SearchAlbumsIterator<C> {
895 /// Create a new search albums iterator.
896 ///
897 /// This is typically called via [`LastFmEditClient::search_albums`](crate::LastFmEditClient::search_albums).
898 pub fn new(client: C, query: String) -> Self {
899 Self {
900 client,
901 query,
902 current_page: 1,
903 has_more: true,
904 buffer: Vec::new(),
905 total_pages: None,
906 }
907 }
908
909 /// Create a new search albums iterator starting from a specific page.
910 ///
911 /// This is useful for implementing offset functionality efficiently by starting
912 /// at the appropriate page rather than iterating through all previous pages.
913 pub fn with_starting_page(client: C, query: String, starting_page: u32) -> Self {
914 let page = std::cmp::max(1, starting_page);
915 Self {
916 client,
917 query,
918 current_page: page,
919 has_more: true,
920 buffer: Vec::new(),
921 total_pages: None,
922 }
923 }
924
925 /// Fetch the next page of search results.
926 ///
927 /// This method handles pagination automatically and includes rate limiting
928 /// to be respectful to Last.fm's servers.
929 pub async fn next_page(&mut self) -> Result<Option<AlbumPage>> {
930 if !self.has_more {
931 return Ok(None);
932 }
933
934 let page = self
935 .client
936 .search_albums_page(&self.query, self.current_page)
937 .await?;
938
939 self.has_more = page.has_next_page;
940 self.current_page += 1;
941 self.total_pages = page.total_pages;
942
943 Ok(Some(page))
944 }
945
946 /// Get the total number of pages, if known.
947 ///
948 /// Returns `None` until at least one page has been fetched.
949 pub fn total_pages(&self) -> Option<u32> {
950 self.total_pages
951 }
952}
953
954// =============================================================================
955// ARTISTS ITERATOR
956// =============================================================================
957
958/// Iterator for browsing all artists in the user's library.
959///
960/// This iterator provides access to all artists in the authenticated user's Last.fm library,
961/// sorted by play count (highest first). The iterator loads artists as needed and handles
962/// rate limiting automatically to be respectful to Last.fm's servers.
963///
964/// # Examples
965///
966/// ```rust,no_run
967/// # use lastfm_edit::{LastFmEditClient, LastFmEditClientImpl, LastFmEditSession, AsyncPaginatedIterator};
968/// # tokio_test::block_on(async {
969/// # let test_session = LastFmEditSession::new("test".to_string(), vec!["sessionid=.test123".to_string()], Some("csrf".to_string()), "https://www.last.fm".to_string());
970/// let mut client = LastFmEditClientImpl::from_session(Box::new(http_client::native::NativeClient::new()), test_session);
971///
972/// let mut artists = client.artists();
973///
974/// // Get the top 10 artists
975/// let top_artists = artists.take(10).await?;
976/// for artist in top_artists {
977/// println!("{} ({} plays)", artist.name, artist.playcount);
978/// }
979/// # Ok::<(), Box<dyn std::error::Error>>(())
980/// # });
981/// ```
982pub struct ArtistsIterator<C: LastFmEditClient> {
983 client: C,
984 current_page: u32,
985 has_more: bool,
986 buffer: Vec<crate::Artist>,
987 total_pages: Option<u32>,
988}
989
990#[async_trait(?Send)]
991impl<C: LastFmEditClient> AsyncPaginatedIterator<crate::Artist> for ArtistsIterator<C> {
992 async fn next(&mut self) -> Result<Option<crate::Artist>> {
993 // If buffer is empty, try to load next page
994 if self.buffer.is_empty() {
995 if let Some(page) = self.next_page().await? {
996 self.buffer = page.artists;
997 self.buffer.reverse(); // Reverse so we can pop from end efficiently
998 }
999 }
1000
1001 Ok(self.buffer.pop())
1002 }
1003
1004 fn current_page(&self) -> u32 {
1005 self.current_page.saturating_sub(1)
1006 }
1007
1008 fn total_pages(&self) -> Option<u32> {
1009 self.total_pages
1010 }
1011}
1012
1013impl<C: LastFmEditClient> ArtistsIterator<C> {
1014 /// Create a new artists iterator.
1015 ///
1016 /// This iterator will start from page 1 and load all artists in the user's library.
1017 pub fn new(client: C) -> Self {
1018 Self {
1019 client,
1020 current_page: 1,
1021 has_more: true,
1022 buffer: Vec::new(),
1023 total_pages: None,
1024 }
1025 }
1026
1027 /// Create a new artists iterator starting from a specific page.
1028 ///
1029 /// This is useful for implementing offset functionality efficiently by starting
1030 /// at the appropriate page rather than iterating through all previous pages.
1031 pub fn with_starting_page(client: C, starting_page: u32) -> Self {
1032 let page = std::cmp::max(1, starting_page);
1033 Self {
1034 client,
1035 current_page: page,
1036 has_more: true,
1037 buffer: Vec::new(),
1038 total_pages: None,
1039 }
1040 }
1041
1042 /// Fetch the next page of artists.
1043 ///
1044 /// This method handles pagination automatically and includes rate limiting
1045 /// to be respectful to Last.fm's servers.
1046 pub async fn next_page(&mut self) -> Result<Option<crate::ArtistPage>> {
1047 if !self.has_more {
1048 return Ok(None);
1049 }
1050
1051 let page = self.client.get_artists_page(self.current_page).await?;
1052
1053 self.has_more = page.has_next_page;
1054 self.current_page += 1;
1055 self.total_pages = page.total_pages;
1056
1057 Ok(Some(page))
1058 }
1059
1060 /// Get the total number of pages, if known.
1061 ///
1062 /// Returns `None` until at least one page has been fetched.
1063 pub fn total_pages(&self) -> Option<u32> {
1064 self.total_pages
1065 }
1066}