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#[cfg_attr(feature = "mock", mockall::automock)]
12#[async_trait(?Send)]
13pub trait AsyncPaginatedIterator<T> {
14 /// Fetch the next item from the iterator.
15 ///
16 /// This method automatically handles pagination, fetching new pages as needed.
17 /// Returns `None` when there are no more items available.
18 ///
19 /// # Returns
20 ///
21 /// - `Ok(Some(item))` - Next item in the sequence
22 /// - `Ok(None)` - No more items available
23 /// - `Err(...)` - Network or parsing error occurred
24 async fn next(&mut self) -> Result<Option<T>>;
25
26 /// Collect all remaining items into a Vec.
27 ///
28 /// **Warning**: This method will fetch ALL remaining pages, which could be
29 /// many thousands of items for large libraries. Use [`take`](Self::take) for
30 /// safer bounded collection.
31 async fn collect_all(&mut self) -> Result<Vec<T>> {
32 let mut items = Vec::new();
33 while let Some(item) = self.next().await? {
34 items.push(item);
35 }
36 Ok(items)
37 }
38
39 /// Take up to n items from the iterator.
40 ///
41 /// This is the recommended way to collect a bounded number of items
42 /// from potentially large datasets.
43 ///
44 /// # Arguments
45 ///
46 /// * `n` - Maximum number of items to collect
47 async fn take(&mut self, n: usize) -> Result<Vec<T>> {
48 let mut items = Vec::new();
49 for _ in 0..n {
50 match self.next().await? {
51 Some(item) => items.push(item),
52 None => break,
53 }
54 }
55 Ok(items)
56 }
57
58 /// Get the current page number (0-indexed).
59 ///
60 /// Returns the page number of the most recently fetched page.
61 fn current_page(&self) -> u32;
62
63 /// Get the total number of pages, if known.
64 ///
65 /// Returns `Some(n)` if the total page count is known, `None` otherwise.
66 /// This information may not be available until at least one page has been fetched.
67 fn total_pages(&self) -> Option<u32> {
68 None // Default implementation returns None
69 }
70}
71
72/// Iterator for browsing an artist's tracks from a user's library.
73///
74/// This iterator provides access to all tracks by a specific artist
75/// in the authenticated user's Last.fm library. Unlike the basic track listing,
76/// this iterator fetches tracks by iterating through the artist's albums first,
77/// which provides complete album information for each track.
78///
79/// The iterator loads albums and their tracks as needed and handles rate limiting
80/// automatically to be respectful to Last.fm's servers.
81pub struct ArtistTracksIterator<C: LastFmEditClient> {
82 client: C,
83 artist: String,
84 album_iterator: Option<ArtistAlbumsIterator<C>>,
85 current_album_tracks: Option<AlbumTracksIterator<C>>,
86 track_buffer: Vec<Track>,
87 finished: bool,
88}
89
90#[async_trait(?Send)]
91impl<C: LastFmEditClient + Clone> AsyncPaginatedIterator<Track> for ArtistTracksIterator<C> {
92 async fn next(&mut self) -> Result<Option<Track>> {
93 // If we're finished, return None
94 if self.finished {
95 return Ok(None);
96 }
97
98 // If track buffer is empty, try to get more tracks
99 while self.track_buffer.is_empty() {
100 // If we don't have a current album tracks iterator, get the next album
101 if self.current_album_tracks.is_none() {
102 // Initialize album iterator if needed
103 if self.album_iterator.is_none() {
104 self.album_iterator = Some(ArtistAlbumsIterator::new(
105 self.client.clone(),
106 self.artist.clone(),
107 ));
108 }
109
110 // Get next album
111 if let Some(ref mut album_iter) = self.album_iterator {
112 if let Some(album) = album_iter.next().await? {
113 log::debug!(
114 "Processing album '{}' for artist '{}'",
115 album.name,
116 self.artist
117 );
118 // Create album tracks iterator for this album
119 self.current_album_tracks = Some(AlbumTracksIterator::new(
120 self.client.clone(),
121 album.name.clone(),
122 self.artist.clone(),
123 ));
124 } else {
125 // No more albums, we're done
126 log::debug!("No more albums for artist '{}'", self.artist);
127 self.finished = true;
128 return Ok(None);
129 }
130 }
131 }
132
133 // Get tracks from current album
134 if let Some(ref mut album_tracks) = self.current_album_tracks {
135 if let Some(track) = album_tracks.next().await? {
136 self.track_buffer.push(track);
137 } else {
138 // This album is exhausted, move to next album
139 log::debug!(
140 "Finished processing current album for artist '{}'",
141 self.artist
142 );
143 self.current_album_tracks = None;
144 // Continue the loop to try getting the next album
145 }
146 }
147 }
148
149 // Return the next track from our buffer
150 Ok(self.track_buffer.pop())
151 }
152
153 fn current_page(&self) -> u32 {
154 // Since we're iterating through albums, return the album iterator's current page
155 if let Some(ref album_iter) = self.album_iterator {
156 album_iter.current_page()
157 } else {
158 0
159 }
160 }
161
162 fn total_pages(&self) -> Option<u32> {
163 // Since we're iterating through albums, return the album iterator's total pages
164 if let Some(ref album_iter) = self.album_iterator {
165 album_iter.total_pages()
166 } else {
167 None
168 }
169 }
170}
171
172impl<C: LastFmEditClient + Clone> ArtistTracksIterator<C> {
173 /// Create a new artist tracks iterator.
174 ///
175 /// This is typically called via [`LastFmEditClient::artist_tracks`](crate::LastFmEditClient::artist_tracks).
176 pub fn new(client: C, artist: String) -> Self {
177 Self {
178 client,
179 artist,
180 album_iterator: None,
181 current_album_tracks: None,
182 track_buffer: Vec::new(),
183 finished: false,
184 }
185 }
186}
187
188/// Iterator for browsing an artist's tracks directly using the paginated artist tracks endpoint.
189///
190/// This iterator provides access to all tracks by a specific artist
191/// in the authenticated user's Last.fm library by directly using the
192/// `/user/{username}/library/music/{artist}/+tracks` endpoint with pagination.
193/// This is more efficient than the album-based approach as it doesn't need to
194/// iterate through albums first.
195pub struct ArtistTracksDirectIterator<C: LastFmEditClient> {
196 client: C,
197 artist: String,
198 current_page: u32,
199 has_more: bool,
200 buffer: Vec<Track>,
201 total_pages: Option<u32>,
202 tracks_yielded: u32,
203}
204
205#[async_trait(?Send)]
206impl<C: LastFmEditClient> AsyncPaginatedIterator<Track> for ArtistTracksDirectIterator<C> {
207 async fn next(&mut self) -> Result<Option<Track>> {
208 // If buffer is empty, try to load next page
209 if self.buffer.is_empty() {
210 if let Some(page) = self.next_page().await? {
211 self.buffer = page.tracks;
212 self.buffer.reverse(); // Reverse so we can pop from end efficiently
213 }
214 }
215
216 if let Some(track) = self.buffer.pop() {
217 self.tracks_yielded += 1;
218 Ok(Some(track))
219 } else {
220 Ok(None)
221 }
222 }
223
224 fn current_page(&self) -> u32 {
225 self.current_page.saturating_sub(1)
226 }
227
228 fn total_pages(&self) -> Option<u32> {
229 self.total_pages
230 }
231}
232
233impl<C: LastFmEditClient> ArtistTracksDirectIterator<C> {
234 /// Create a new direct artist tracks iterator.
235 ///
236 /// This is typically called via [`LastFmEditClient::artist_tracks_direct`](crate::LastFmEditClient::artist_tracks_direct).
237 pub fn new(client: C, artist: String) -> Self {
238 Self {
239 client,
240 artist,
241 current_page: 1,
242 has_more: true,
243 buffer: Vec::new(),
244 total_pages: None,
245 tracks_yielded: 0,
246 }
247 }
248
249 /// Fetch the next page of tracks.
250 ///
251 /// This method handles pagination automatically and includes rate limiting.
252 pub async fn next_page(&mut self) -> Result<Option<TrackPage>> {
253 if !self.has_more {
254 return Ok(None);
255 }
256
257 log::debug!(
258 "Fetching page {} of {} tracks (yielded {} tracks so far)",
259 self.current_page,
260 self.artist,
261 self.tracks_yielded
262 );
263
264 let page = self
265 .client
266 .get_artist_tracks_page(&self.artist, self.current_page)
267 .await?;
268
269 self.has_more = page.has_next_page;
270 self.current_page += 1;
271 self.total_pages = page.total_pages;
272
273 Ok(Some(page))
274 }
275
276 /// Get the total number of pages, if known.
277 ///
278 /// Returns `None` until at least one page has been fetched.
279 pub fn total_pages(&self) -> Option<u32> {
280 self.total_pages
281 }
282}
283
284/// Iterator for browsing an artist's albums from a user's library.
285///
286/// This iterator provides paginated access to all albums by a specific artist
287/// in the authenticated user's Last.fm library, ordered by play count.
288pub struct ArtistAlbumsIterator<C: LastFmEditClient> {
289 client: C,
290 artist: String,
291 current_page: u32,
292 has_more: bool,
293 buffer: Vec<Album>,
294 total_pages: Option<u32>,
295}
296
297#[async_trait(?Send)]
298impl<C: LastFmEditClient> AsyncPaginatedIterator<Album> for ArtistAlbumsIterator<C> {
299 async fn next(&mut self) -> Result<Option<Album>> {
300 // If buffer is empty, try to load next page
301 if self.buffer.is_empty() {
302 if let Some(page) = self.next_page().await? {
303 self.buffer = page.albums;
304 self.buffer.reverse(); // Reverse so we can pop from end efficiently
305 }
306 }
307
308 Ok(self.buffer.pop())
309 }
310
311 fn current_page(&self) -> u32 {
312 self.current_page.saturating_sub(1)
313 }
314
315 fn total_pages(&self) -> Option<u32> {
316 self.total_pages
317 }
318}
319
320impl<C: LastFmEditClient> ArtistAlbumsIterator<C> {
321 /// Create a new artist albums iterator.
322 ///
323 /// This is typically called via [`LastFmEditClient::artist_albums`](crate::LastFmEditClient::artist_albums).
324 pub fn new(client: C, artist: String) -> Self {
325 Self {
326 client,
327 artist,
328 current_page: 1,
329 has_more: true,
330 buffer: Vec::new(),
331 total_pages: None,
332 }
333 }
334
335 /// Fetch the next page of albums.
336 ///
337 /// This method handles pagination automatically and includes rate limiting.
338 pub async fn next_page(&mut self) -> Result<Option<AlbumPage>> {
339 if !self.has_more {
340 return Ok(None);
341 }
342
343 let page = self
344 .client
345 .get_artist_albums_page(&self.artist, self.current_page)
346 .await?;
347
348 self.has_more = page.has_next_page;
349 self.current_page += 1;
350 self.total_pages = page.total_pages;
351
352 Ok(Some(page))
353 }
354
355 /// Get the total number of pages, if known.
356 ///
357 /// Returns `None` until at least one page has been fetched.
358 pub fn total_pages(&self) -> Option<u32> {
359 self.total_pages
360 }
361}
362
363/// Iterator for browsing a user's recent tracks/scrobbles.
364///
365/// This iterator provides access to the user's recent listening history with timestamps,
366/// which is essential for finding tracks that can be edited. It supports optional
367/// timestamp-based filtering to avoid reprocessing old data.
368pub struct RecentTracksIterator<C: LastFmEditClient> {
369 client: C,
370 current_page: u32,
371 has_more: bool,
372 buffer: Vec<Track>,
373 stop_at_timestamp: Option<u64>,
374}
375
376#[async_trait(?Send)]
377impl<C: LastFmEditClient> AsyncPaginatedIterator<Track> for RecentTracksIterator<C> {
378 async fn next(&mut self) -> Result<Option<Track>> {
379 // If buffer is empty, try to load next page
380 if self.buffer.is_empty() {
381 if !self.has_more {
382 return Ok(None);
383 }
384
385 let page = self
386 .client
387 .get_recent_tracks_page(self.current_page)
388 .await?;
389
390 if page.tracks.is_empty() {
391 self.has_more = false;
392 return Ok(None);
393 }
394
395 self.has_more = page.has_next_page;
396
397 // Check if we should stop based on timestamp
398 if let Some(stop_timestamp) = self.stop_at_timestamp {
399 let mut filtered_tracks = Vec::new();
400 for track in page.tracks {
401 if let Some(track_timestamp) = track.timestamp {
402 if track_timestamp <= stop_timestamp {
403 self.has_more = false;
404 break;
405 }
406 }
407 filtered_tracks.push(track);
408 }
409 self.buffer = filtered_tracks;
410 } else {
411 self.buffer = page.tracks;
412 }
413
414 self.buffer.reverse(); // Reverse so we can pop from end efficiently
415 self.current_page += 1;
416 }
417
418 Ok(self.buffer.pop())
419 }
420
421 fn current_page(&self) -> u32 {
422 self.current_page.saturating_sub(1)
423 }
424}
425
426impl<C: LastFmEditClient> RecentTracksIterator<C> {
427 /// Create a new recent tracks iterator starting from page 1.
428 ///
429 /// This is typically called via [`LastFmEditClient::recent_tracks`](crate::LastFmEditClient::recent_tracks).
430 pub fn new(client: C) -> Self {
431 Self::with_starting_page(client, 1)
432 }
433
434 /// Create a new recent tracks iterator starting from a specific page.
435 ///
436 /// This allows resuming pagination from an arbitrary page, useful for
437 /// continuing from where a previous iteration left off.
438 ///
439 /// # Arguments
440 ///
441 /// * `client` - The LastFmEditClient to use for API calls
442 /// * `starting_page` - The page number to start from (1-indexed)
443 pub fn with_starting_page(client: C, starting_page: u32) -> Self {
444 let page = std::cmp::max(1, starting_page);
445 Self {
446 client,
447 current_page: page,
448 has_more: true,
449 buffer: Vec::new(),
450 stop_at_timestamp: None,
451 }
452 }
453
454 /// Set a timestamp to stop iteration at.
455 ///
456 /// When this is set, the iterator will stop returning tracks once it encounters
457 /// a track with a timestamp less than or equal to the specified value. This is
458 /// useful for incremental processing to avoid reprocessing old data.
459 ///
460 /// # Arguments
461 ///
462 /// * `timestamp` - Unix timestamp to stop at
463 pub fn with_stop_timestamp(mut self, timestamp: u64) -> Self {
464 self.stop_at_timestamp = Some(timestamp);
465 self
466 }
467}
468
469/// Iterator for browsing tracks in a specific album from a user's library.
470///
471/// This iterator provides access to all tracks in a specific album by an artist
472/// in the authenticated user's Last.fm library. Unlike paginated iterators,
473/// this loads tracks once and iterates through them.
474pub struct AlbumTracksIterator<C: LastFmEditClient> {
475 client: C,
476 album_name: String,
477 artist_name: String,
478 tracks: Option<Vec<Track>>,
479 index: usize,
480}
481
482#[async_trait(?Send)]
483impl<C: LastFmEditClient> AsyncPaginatedIterator<Track> for AlbumTracksIterator<C> {
484 async fn next(&mut self) -> Result<Option<Track>> {
485 // Load tracks if not already loaded
486 if self.tracks.is_none() {
487 // Use get_album_tracks_page instead of get_album_tracks to avoid infinite recursion
488 let tracks_page = self
489 .client
490 .get_album_tracks_page(&self.album_name, &self.artist_name, 1)
491 .await?;
492 log::debug!(
493 "Album '{}' by '{}' has {} tracks: {:?}",
494 self.album_name,
495 self.artist_name,
496 tracks_page.tracks.len(),
497 tracks_page
498 .tracks
499 .iter()
500 .map(|t| &t.name)
501 .collect::<Vec<_>>()
502 );
503
504 if tracks_page.tracks.is_empty() {
505 log::warn!(
506 "🚨 ZERO TRACKS FOUND for album '{}' by '{}' - investigating...",
507 self.album_name,
508 self.artist_name
509 );
510 log::debug!("Full TrackPage for empty album: has_next_page={}, page_number={}, total_pages={:?}",
511 tracks_page.has_next_page, tracks_page.page_number, tracks_page.total_pages);
512 }
513 self.tracks = Some(tracks_page.tracks);
514 }
515
516 // Return next track
517 if let Some(tracks) = &self.tracks {
518 if self.index < tracks.len() {
519 let track = tracks[self.index].clone();
520 self.index += 1;
521 Ok(Some(track))
522 } else {
523 Ok(None)
524 }
525 } else {
526 Ok(None)
527 }
528 }
529
530 fn current_page(&self) -> u32 {
531 // Album tracks don't have pages, so return 0
532 0
533 }
534}
535
536impl<C: LastFmEditClient> AlbumTracksIterator<C> {
537 /// Create a new album tracks iterator.
538 ///
539 /// This is typically called via [`LastFmEditClient::album_tracks`](crate::LastFmEditClient::album_tracks).
540 pub fn new(client: C, album_name: String, artist_name: String) -> Self {
541 Self {
542 client,
543 album_name,
544 artist_name,
545 tracks: None,
546 index: 0,
547 }
548 }
549}
550
551/// Iterator for searching tracks in the user's library.
552///
553/// This iterator provides paginated access to tracks that match a search query
554/// in the authenticated user's Last.fm library, using Last.fm's built-in search functionality.
555pub struct SearchTracksIterator<C: LastFmEditClient> {
556 client: C,
557 query: String,
558 current_page: u32,
559 has_more: bool,
560 buffer: Vec<Track>,
561 total_pages: Option<u32>,
562}
563
564#[async_trait(?Send)]
565impl<C: LastFmEditClient> AsyncPaginatedIterator<Track> for SearchTracksIterator<C> {
566 async fn next(&mut self) -> Result<Option<Track>> {
567 // If buffer is empty, try to load next page
568 if self.buffer.is_empty() {
569 if let Some(page) = self.next_page().await? {
570 self.buffer = page.tracks;
571 self.buffer.reverse(); // Reverse so we can pop from end efficiently
572 }
573 }
574
575 Ok(self.buffer.pop())
576 }
577
578 fn current_page(&self) -> u32 {
579 self.current_page.saturating_sub(1)
580 }
581
582 fn total_pages(&self) -> Option<u32> {
583 self.total_pages
584 }
585}
586
587impl<C: LastFmEditClient> SearchTracksIterator<C> {
588 /// Create a new search tracks iterator.
589 ///
590 /// This is typically called via [`LastFmEditClient::search_tracks`](crate::LastFmEditClient::search_tracks).
591 pub fn new(client: C, query: String) -> Self {
592 Self {
593 client,
594 query,
595 current_page: 1,
596 has_more: true,
597 buffer: Vec::new(),
598 total_pages: None,
599 }
600 }
601
602 /// Create a new search tracks iterator starting from a specific page.
603 ///
604 /// This is useful for implementing offset functionality efficiently by starting
605 /// at the appropriate page rather than iterating through all previous pages.
606 pub fn with_starting_page(client: C, query: String, starting_page: u32) -> Self {
607 let page = std::cmp::max(1, starting_page);
608 Self {
609 client,
610 query,
611 current_page: page,
612 has_more: true,
613 buffer: Vec::new(),
614 total_pages: None,
615 }
616 }
617
618 /// Fetch the next page of search results.
619 ///
620 /// This method handles pagination automatically and includes rate limiting
621 /// to be respectful to Last.fm's servers.
622 pub async fn next_page(&mut self) -> Result<Option<TrackPage>> {
623 if !self.has_more {
624 return Ok(None);
625 }
626
627 let page = self
628 .client
629 .search_tracks_page(&self.query, self.current_page)
630 .await?;
631
632 self.has_more = page.has_next_page;
633 self.current_page += 1;
634 self.total_pages = page.total_pages;
635
636 Ok(Some(page))
637 }
638
639 /// Get the total number of pages, if known.
640 ///
641 /// Returns `None` until at least one page has been fetched.
642 pub fn total_pages(&self) -> Option<u32> {
643 self.total_pages
644 }
645}
646
647/// Iterator for searching albums in the user's library.
648///
649/// This iterator provides paginated access to albums that match a search query
650/// in the authenticated user's Last.fm library, using Last.fm's built-in search functionality.
651///
652/// # Examples
653pub struct SearchAlbumsIterator<C: LastFmEditClient> {
654 client: C,
655 query: String,
656 current_page: u32,
657 has_more: bool,
658 buffer: Vec<Album>,
659 total_pages: Option<u32>,
660}
661
662#[async_trait(?Send)]
663impl<C: LastFmEditClient> AsyncPaginatedIterator<Album> for SearchAlbumsIterator<C> {
664 async fn next(&mut self) -> Result<Option<Album>> {
665 // If buffer is empty, try to load next page
666 if self.buffer.is_empty() {
667 if let Some(page) = self.next_page().await? {
668 self.buffer = page.albums;
669 self.buffer.reverse(); // Reverse so we can pop from end efficiently
670 }
671 }
672
673 Ok(self.buffer.pop())
674 }
675
676 fn current_page(&self) -> u32 {
677 self.current_page.saturating_sub(1)
678 }
679
680 fn total_pages(&self) -> Option<u32> {
681 self.total_pages
682 }
683}
684
685impl<C: LastFmEditClient> SearchAlbumsIterator<C> {
686 /// Create a new search albums iterator.
687 ///
688 /// This is typically called via [`LastFmEditClient::search_albums`](crate::LastFmEditClient::search_albums).
689 pub fn new(client: C, query: String) -> Self {
690 Self {
691 client,
692 query,
693 current_page: 1,
694 has_more: true,
695 buffer: Vec::new(),
696 total_pages: None,
697 }
698 }
699
700 /// Create a new search albums iterator starting from a specific page.
701 ///
702 /// This is useful for implementing offset functionality efficiently by starting
703 /// at the appropriate page rather than iterating through all previous pages.
704 pub fn with_starting_page(client: C, query: String, starting_page: u32) -> Self {
705 let page = std::cmp::max(1, starting_page);
706 Self {
707 client,
708 query,
709 current_page: page,
710 has_more: true,
711 buffer: Vec::new(),
712 total_pages: None,
713 }
714 }
715
716 /// Fetch the next page of search results.
717 ///
718 /// This method handles pagination automatically and includes rate limiting
719 /// to be respectful to Last.fm's servers.
720 pub async fn next_page(&mut self) -> Result<Option<AlbumPage>> {
721 if !self.has_more {
722 return Ok(None);
723 }
724
725 let page = self
726 .client
727 .search_albums_page(&self.query, self.current_page)
728 .await?;
729
730 self.has_more = page.has_next_page;
731 self.current_page += 1;
732 self.total_pages = page.total_pages;
733
734 Ok(Some(page))
735 }
736
737 /// Get the total number of pages, if known.
738 ///
739 /// Returns `None` until at least one page has been fetched.
740 pub fn total_pages(&self) -> Option<u32> {
741 self.total_pages
742 }
743}
744
745/// Iterator for searching artists in the user's library.
746///
747/// This iterator provides paginated access to artists that match a search query
748/// in the authenticated user's Last.fm library, using Last.fm's built-in search functionality.
749pub struct SearchArtistsIterator<C: LastFmEditClient> {
750 client: C,
751 query: String,
752 current_page: u32,
753 has_more: bool,
754 buffer: Vec<crate::Artist>,
755 total_pages: Option<u32>,
756}
757
758#[async_trait(?Send)]
759impl<C: LastFmEditClient> AsyncPaginatedIterator<crate::Artist> for SearchArtistsIterator<C> {
760 async fn next(&mut self) -> Result<Option<crate::Artist>> {
761 // If buffer is empty, try to load next page
762 if self.buffer.is_empty() {
763 if let Some(page) = self.next_page().await? {
764 self.buffer = page.artists;
765 self.buffer.reverse(); // Reverse so we can pop from end efficiently
766 }
767 }
768
769 Ok(self.buffer.pop())
770 }
771
772 fn current_page(&self) -> u32 {
773 self.current_page.saturating_sub(1)
774 }
775
776 fn total_pages(&self) -> Option<u32> {
777 self.total_pages
778 }
779}
780
781impl<C: LastFmEditClient> SearchArtistsIterator<C> {
782 /// Create a new search artists iterator.
783 ///
784 /// This is typically called via [`LastFmEditClient::search_artists`](crate::LastFmEditClient::search_artists).
785 pub fn new(client: C, query: String) -> Self {
786 Self {
787 client,
788 query,
789 current_page: 1,
790 has_more: true,
791 buffer: Vec::new(),
792 total_pages: None,
793 }
794 }
795
796 /// Create a new search artists iterator starting from a specific page.
797 ///
798 /// This is useful for implementing offset functionality efficiently by starting
799 /// at the appropriate page rather than iterating through all previous pages.
800 pub fn with_starting_page(client: C, query: String, starting_page: u32) -> Self {
801 let page = std::cmp::max(1, starting_page);
802 Self {
803 client,
804 query,
805 current_page: page,
806 has_more: true,
807 buffer: Vec::new(),
808 total_pages: None,
809 }
810 }
811
812 /// Fetch the next page of search results.
813 ///
814 /// This method handles pagination automatically and includes rate limiting
815 /// to be respectful to Last.fm's servers.
816 pub async fn next_page(&mut self) -> Result<Option<crate::ArtistPage>> {
817 if !self.has_more {
818 return Ok(None);
819 }
820
821 let page = self
822 .client
823 .search_artists_page(&self.query, self.current_page)
824 .await?;
825
826 self.has_more = page.has_next_page;
827 self.current_page += 1;
828 self.total_pages = page.total_pages;
829
830 Ok(Some(page))
831 }
832
833 /// Get the total number of pages, if known.
834 ///
835 /// Returns `None` until at least one page has been fetched.
836 pub fn total_pages(&self) -> Option<u32> {
837 self.total_pages
838 }
839}
840
841// =============================================================================
842// ARTISTS ITERATOR
843// =============================================================================
844
845/// Iterator for browsing all artists in the user's library.
846///
847/// This iterator provides access to all artists in the authenticated user's Last.fm library,
848/// sorted by play count (highest first). The iterator loads artists as needed and handles
849/// rate limiting automatically to be respectful to Last.fm's servers.
850pub struct ArtistsIterator<C: LastFmEditClient> {
851 client: C,
852 current_page: u32,
853 has_more: bool,
854 buffer: Vec<crate::Artist>,
855 total_pages: Option<u32>,
856}
857
858#[async_trait(?Send)]
859impl<C: LastFmEditClient> AsyncPaginatedIterator<crate::Artist> for ArtistsIterator<C> {
860 async fn next(&mut self) -> Result<Option<crate::Artist>> {
861 // If buffer is empty, try to load next page
862 if self.buffer.is_empty() {
863 if let Some(page) = self.next_page().await? {
864 self.buffer = page.artists;
865 self.buffer.reverse(); // Reverse so we can pop from end efficiently
866 }
867 }
868
869 Ok(self.buffer.pop())
870 }
871
872 fn current_page(&self) -> u32 {
873 self.current_page.saturating_sub(1)
874 }
875
876 fn total_pages(&self) -> Option<u32> {
877 self.total_pages
878 }
879}
880
881impl<C: LastFmEditClient> ArtistsIterator<C> {
882 /// Create a new artists iterator.
883 ///
884 /// This iterator will start from page 1 and load all artists in the user's library.
885 pub fn new(client: C) -> Self {
886 Self {
887 client,
888 current_page: 1,
889 has_more: true,
890 buffer: Vec::new(),
891 total_pages: None,
892 }
893 }
894
895 /// Create a new artists iterator starting from a specific page.
896 ///
897 /// This is useful for implementing offset functionality efficiently by starting
898 /// at the appropriate page rather than iterating through all previous pages.
899 pub fn with_starting_page(client: C, starting_page: u32) -> Self {
900 let page = std::cmp::max(1, starting_page);
901 Self {
902 client,
903 current_page: page,
904 has_more: true,
905 buffer: Vec::new(),
906 total_pages: None,
907 }
908 }
909
910 /// Fetch the next page of artists.
911 ///
912 /// This method handles pagination automatically and includes rate limiting
913 /// to be respectful to Last.fm's servers.
914 pub async fn next_page(&mut self) -> Result<Option<crate::ArtistPage>> {
915 if !self.has_more {
916 return Ok(None);
917 }
918
919 let page = self.client.get_artists_page(self.current_page).await?;
920
921 self.has_more = page.has_next_page;
922 self.current_page += 1;
923 self.total_pages = page.total_pages;
924
925 Ok(Some(page))
926 }
927
928 /// Get the total number of pages, if known.
929 ///
930 /// Returns `None` until at least one page has been fetched.
931 pub fn total_pages(&self) -> Option<u32> {
932 self.total_pages
933 }
934}