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 page = self
517 .client
518 .get_recent_tracks_page(self.current_page)
519 .await?;
520
521 if page.tracks.is_empty() {
522 self.has_more = false;
523 return Ok(None);
524 }
525
526 self.has_more = page.has_next_page;
527
528 // Check if we should stop based on timestamp
529 if let Some(stop_timestamp) = self.stop_at_timestamp {
530 let mut filtered_tracks = Vec::new();
531 for track in page.tracks {
532 if let Some(track_timestamp) = track.timestamp {
533 if track_timestamp <= stop_timestamp {
534 self.has_more = false;
535 break;
536 }
537 }
538 filtered_tracks.push(track);
539 }
540 self.buffer = filtered_tracks;
541 } else {
542 self.buffer = page.tracks;
543 }
544
545 self.buffer.reverse(); // Reverse so we can pop from end efficiently
546 self.current_page += 1;
547 }
548
549 Ok(self.buffer.pop())
550 }
551
552 fn current_page(&self) -> u32 {
553 self.current_page.saturating_sub(1)
554 }
555}
556
557impl<C: LastFmEditClient> RecentTracksIterator<C> {
558 /// Create a new recent tracks iterator starting from page 1.
559 ///
560 /// This is typically called via [`LastFmEditClient::recent_tracks`](crate::LastFmEditClient::recent_tracks).
561 pub fn new(client: C) -> Self {
562 Self::with_starting_page(client, 1)
563 }
564
565 /// Create a new recent tracks iterator starting from a specific page.
566 ///
567 /// This allows resuming pagination from an arbitrary page, useful for
568 /// continuing from where a previous iteration left off.
569 ///
570 /// # Arguments
571 ///
572 /// * `client` - The LastFmEditClient to use for API calls
573 /// * `starting_page` - The page number to start from (1-indexed)
574 ///
575 /// # Examples
576 ///
577 /// ```rust,no_run
578 /// # use lastfm_edit::{LastFmEditClient, LastFmEditClientImpl, LastFmEditSession, AsyncPaginatedIterator};
579 /// # tokio_test::block_on(async {
580 /// # let test_session = LastFmEditSession::new("test".to_string(), vec!["sessionid=.test123".to_string()], Some("csrf".to_string()), "https://www.last.fm".to_string());
581 /// let mut client = LastFmEditClientImpl::from_session(Box::new(http_client::native::NativeClient::new()), test_session);
582 ///
583 /// // Start from page 5
584 /// let mut recent = client.recent_tracks_from_page(5);
585 /// let tracks = recent.take(10).await?;
586 /// # Ok::<(), Box<dyn std::error::Error>>(())
587 /// # });
588 /// ```
589 pub fn with_starting_page(client: C, starting_page: u32) -> Self {
590 let page = std::cmp::max(1, starting_page);
591 Self {
592 client,
593 current_page: page,
594 has_more: true,
595 buffer: Vec::new(),
596 stop_at_timestamp: None,
597 }
598 }
599
600 /// Set a timestamp to stop iteration at.
601 ///
602 /// When this is set, the iterator will stop returning tracks once it encounters
603 /// a track with a timestamp less than or equal to the specified value. This is
604 /// useful for incremental processing to avoid reprocessing old data.
605 ///
606 /// # Arguments
607 ///
608 /// * `timestamp` - Unix timestamp to stop at
609 ///
610 /// # Examples
611 ///
612 /// ```rust,no_run
613 /// # use lastfm_edit::{LastFmEditClient, LastFmEditClientImpl, LastFmEditSession, AsyncPaginatedIterator};
614 /// # tokio_test::block_on(async {
615 /// # let test_session = LastFmEditSession::new("test".to_string(), vec!["sessionid=.test123".to_string()], Some("csrf".to_string()), "https://www.last.fm".to_string());
616 /// let mut client = LastFmEditClientImpl::from_session(Box::new(http_client::native::NativeClient::new()), test_session);
617 /// let last_processed = 1640995200; // Some previous timestamp
618 ///
619 /// let mut recent = lastfm_edit::RecentTracksIterator::new(client).with_stop_timestamp(last_processed);
620 /// let new_tracks = recent.collect_all().await?; // Only gets new tracks
621 /// # Ok::<(), Box<dyn std::error::Error>>(())
622 /// # });
623 /// ```
624 pub fn with_stop_timestamp(mut self, timestamp: u64) -> Self {
625 self.stop_at_timestamp = Some(timestamp);
626 self
627 }
628}
629
630/// Iterator for browsing tracks in a specific album from a user's library.
631///
632/// This iterator provides access to all tracks in a specific album by an artist
633/// in the authenticated user's Last.fm library. Unlike paginated iterators,
634/// this loads tracks once and iterates through them.
635///
636/// # Examples
637///
638/// ```rust,no_run
639/// # use lastfm_edit::{LastFmEditClient, LastFmEditClientImpl, LastFmEditSession, AsyncPaginatedIterator};
640/// # tokio_test::block_on(async {
641/// # let test_session = LastFmEditSession::new("test".to_string(), vec!["sessionid=.test123".to_string()], Some("csrf".to_string()), "https://www.last.fm".to_string());
642/// let mut client = LastFmEditClientImpl::from_session(Box::new(http_client::native::NativeClient::new()), test_session);
643///
644/// let mut tracks = client.album_tracks("The Dark Side of the Moon", "Pink Floyd");
645///
646/// // Get all tracks in the album
647/// while let Some(track) = tracks.next().await? {
648/// println!("{} - {}", track.name, track.artist);
649/// }
650/// # Ok::<(), Box<dyn std::error::Error>>(())
651/// # });
652/// ```
653pub struct AlbumTracksIterator<C: LastFmEditClient> {
654 client: C,
655 album_name: String,
656 artist_name: String,
657 tracks: Option<Vec<Track>>,
658 index: usize,
659}
660
661#[async_trait(?Send)]
662impl<C: LastFmEditClient> AsyncPaginatedIterator<Track> for AlbumTracksIterator<C> {
663 async fn next(&mut self) -> Result<Option<Track>> {
664 // Load tracks if not already loaded
665 if self.tracks.is_none() {
666 // Use get_album_tracks_page instead of get_album_tracks to avoid infinite recursion
667 let tracks_page = self
668 .client
669 .get_album_tracks_page(&self.album_name, &self.artist_name, 1)
670 .await?;
671 log::debug!(
672 "Album '{}' by '{}' has {} tracks: {:?}",
673 self.album_name,
674 self.artist_name,
675 tracks_page.tracks.len(),
676 tracks_page
677 .tracks
678 .iter()
679 .map(|t| &t.name)
680 .collect::<Vec<_>>()
681 );
682
683 if tracks_page.tracks.is_empty() {
684 log::warn!(
685 "🚨 ZERO TRACKS FOUND for album '{}' by '{}' - investigating...",
686 self.album_name,
687 self.artist_name
688 );
689 log::debug!("Full TrackPage for empty album: has_next_page={}, page_number={}, total_pages={:?}",
690 tracks_page.has_next_page, tracks_page.page_number, tracks_page.total_pages);
691 }
692 self.tracks = Some(tracks_page.tracks);
693 }
694
695 // Return next track
696 if let Some(tracks) = &self.tracks {
697 if self.index < tracks.len() {
698 let track = tracks[self.index].clone();
699 self.index += 1;
700 Ok(Some(track))
701 } else {
702 Ok(None)
703 }
704 } else {
705 Ok(None)
706 }
707 }
708
709 fn current_page(&self) -> u32 {
710 // Album tracks don't have pages, so return 0
711 0
712 }
713}
714
715impl<C: LastFmEditClient> AlbumTracksIterator<C> {
716 /// Create a new album tracks iterator.
717 ///
718 /// This is typically called via [`LastFmEditClient::album_tracks`](crate::LastFmEditClient::album_tracks).
719 pub fn new(client: C, album_name: String, artist_name: String) -> Self {
720 Self {
721 client,
722 album_name,
723 artist_name,
724 tracks: None,
725 index: 0,
726 }
727 }
728}
729
730/// Iterator for searching tracks in the user's library.
731///
732/// This iterator provides paginated access to tracks that match a search query
733/// in the authenticated user's Last.fm library, using Last.fm's built-in search functionality.
734///
735/// # Examples
736///
737/// ```rust,no_run
738/// # use lastfm_edit::{LastFmEditClient, LastFmEditClientImpl, LastFmEditSession, AsyncPaginatedIterator};
739/// # tokio_test::block_on(async {
740/// # let test_session = LastFmEditSession::new("test".to_string(), vec!["sessionid=.test123".to_string()], Some("csrf".to_string()), "https://www.last.fm".to_string());
741/// let mut client = LastFmEditClientImpl::from_session(Box::new(http_client::native::NativeClient::new()), test_session);
742///
743/// let mut search_results = client.search_tracks("remaster");
744///
745/// // Get first 20 search results
746/// while let Some(track) = search_results.next().await? {
747/// println!("{} - {} (played {} times)", track.artist, track.name, track.playcount);
748/// }
749/// # Ok::<(), Box<dyn std::error::Error>>(())
750/// # });
751/// ```
752pub struct SearchTracksIterator<C: LastFmEditClient> {
753 client: C,
754 query: String,
755 current_page: u32,
756 has_more: bool,
757 buffer: Vec<Track>,
758 total_pages: Option<u32>,
759}
760
761#[async_trait(?Send)]
762impl<C: LastFmEditClient> AsyncPaginatedIterator<Track> for SearchTracksIterator<C> {
763 async fn next(&mut self) -> Result<Option<Track>> {
764 // If buffer is empty, try to load next page
765 if self.buffer.is_empty() {
766 if let Some(page) = self.next_page().await? {
767 self.buffer = page.tracks;
768 self.buffer.reverse(); // Reverse so we can pop from end efficiently
769 }
770 }
771
772 Ok(self.buffer.pop())
773 }
774
775 fn current_page(&self) -> u32 {
776 self.current_page.saturating_sub(1)
777 }
778
779 fn total_pages(&self) -> Option<u32> {
780 self.total_pages
781 }
782}
783
784impl<C: LastFmEditClient> SearchTracksIterator<C> {
785 /// Create a new search tracks iterator.
786 ///
787 /// This is typically called via [`LastFmEditClient::search_tracks`](crate::LastFmEditClient::search_tracks).
788 pub fn new(client: C, query: String) -> Self {
789 Self {
790 client,
791 query,
792 current_page: 1,
793 has_more: true,
794 buffer: Vec::new(),
795 total_pages: None,
796 }
797 }
798
799 /// Create a new search tracks iterator starting from a specific page.
800 ///
801 /// This is useful for implementing offset functionality efficiently by starting
802 /// at the appropriate page rather than iterating through all previous pages.
803 pub fn with_starting_page(client: C, query: String, starting_page: u32) -> Self {
804 let page = std::cmp::max(1, starting_page);
805 Self {
806 client,
807 query,
808 current_page: page,
809 has_more: true,
810 buffer: Vec::new(),
811 total_pages: None,
812 }
813 }
814
815 /// Fetch the next page of search results.
816 ///
817 /// This method handles pagination automatically and includes rate limiting
818 /// to be respectful to Last.fm's servers.
819 pub async fn next_page(&mut self) -> Result<Option<TrackPage>> {
820 if !self.has_more {
821 return Ok(None);
822 }
823
824 let page = self
825 .client
826 .search_tracks_page(&self.query, self.current_page)
827 .await?;
828
829 self.has_more = page.has_next_page;
830 self.current_page += 1;
831 self.total_pages = page.total_pages;
832
833 Ok(Some(page))
834 }
835
836 /// Get the total number of pages, if known.
837 ///
838 /// Returns `None` until at least one page has been fetched.
839 pub fn total_pages(&self) -> Option<u32> {
840 self.total_pages
841 }
842}
843
844/// Iterator for searching albums in the user's library.
845///
846/// This iterator provides paginated access to albums that match a search query
847/// in the authenticated user's Last.fm library, using Last.fm's built-in search functionality.
848///
849/// # Examples
850///
851/// ```rust,no_run
852/// # use lastfm_edit::{LastFmEditClient, LastFmEditClientImpl, LastFmEditSession, AsyncPaginatedIterator};
853/// # tokio_test::block_on(async {
854/// # let test_session = LastFmEditSession::new("test".to_string(), vec!["sessionid=.test123".to_string()], Some("csrf".to_string()), "https://www.last.fm".to_string());
855/// let mut client = LastFmEditClientImpl::from_session(Box::new(http_client::native::NativeClient::new()), test_session);
856///
857/// let mut search_results = client.search_albums("deluxe");
858///
859/// // Get first 10 search results
860/// let top_10 = search_results.take(10).await?;
861/// for album in top_10 {
862/// println!("{} - {} (played {} times)", album.artist, album.name, album.playcount);
863/// }
864/// # Ok::<(), Box<dyn std::error::Error>>(())
865/// # });
866/// ```
867pub struct SearchAlbumsIterator<C: LastFmEditClient> {
868 client: C,
869 query: String,
870 current_page: u32,
871 has_more: bool,
872 buffer: Vec<Album>,
873 total_pages: Option<u32>,
874}
875
876#[async_trait(?Send)]
877impl<C: LastFmEditClient> AsyncPaginatedIterator<Album> for SearchAlbumsIterator<C> {
878 async fn next(&mut self) -> Result<Option<Album>> {
879 // If buffer is empty, try to load next page
880 if self.buffer.is_empty() {
881 if let Some(page) = self.next_page().await? {
882 self.buffer = page.albums;
883 self.buffer.reverse(); // Reverse so we can pop from end efficiently
884 }
885 }
886
887 Ok(self.buffer.pop())
888 }
889
890 fn current_page(&self) -> u32 {
891 self.current_page.saturating_sub(1)
892 }
893
894 fn total_pages(&self) -> Option<u32> {
895 self.total_pages
896 }
897}
898
899impl<C: LastFmEditClient> SearchAlbumsIterator<C> {
900 /// Create a new search albums iterator.
901 ///
902 /// This is typically called via [`LastFmEditClient::search_albums`](crate::LastFmEditClient::search_albums).
903 pub fn new(client: C, query: String) -> Self {
904 Self {
905 client,
906 query,
907 current_page: 1,
908 has_more: true,
909 buffer: Vec::new(),
910 total_pages: None,
911 }
912 }
913
914 /// Create a new search albums iterator starting from a specific page.
915 ///
916 /// This is useful for implementing offset functionality efficiently by starting
917 /// at the appropriate page rather than iterating through all previous pages.
918 pub fn with_starting_page(client: C, query: String, starting_page: u32) -> Self {
919 let page = std::cmp::max(1, starting_page);
920 Self {
921 client,
922 query,
923 current_page: page,
924 has_more: true,
925 buffer: Vec::new(),
926 total_pages: None,
927 }
928 }
929
930 /// Fetch the next page of search results.
931 ///
932 /// This method handles pagination automatically and includes rate limiting
933 /// to be respectful to Last.fm's servers.
934 pub async fn next_page(&mut self) -> Result<Option<AlbumPage>> {
935 if !self.has_more {
936 return Ok(None);
937 }
938
939 let page = self
940 .client
941 .search_albums_page(&self.query, self.current_page)
942 .await?;
943
944 self.has_more = page.has_next_page;
945 self.current_page += 1;
946 self.total_pages = page.total_pages;
947
948 Ok(Some(page))
949 }
950
951 /// Get the total number of pages, if known.
952 ///
953 /// Returns `None` until at least one page has been fetched.
954 pub fn total_pages(&self) -> Option<u32> {
955 self.total_pages
956 }
957}
958
959// =============================================================================
960// ARTISTS ITERATOR
961// =============================================================================
962
963/// Iterator for browsing all artists in the user's library.
964///
965/// This iterator provides access to all artists in the authenticated user's Last.fm library,
966/// sorted by play count (highest first). The iterator loads artists as needed and handles
967/// rate limiting automatically to be respectful to Last.fm's servers.
968///
969/// # Examples
970///
971/// ```rust,no_run
972/// # use lastfm_edit::{LastFmEditClient, LastFmEditClientImpl, LastFmEditSession, AsyncPaginatedIterator};
973/// # tokio_test::block_on(async {
974/// # let test_session = LastFmEditSession::new("test".to_string(), vec!["sessionid=.test123".to_string()], Some("csrf".to_string()), "https://www.last.fm".to_string());
975/// let mut client = LastFmEditClientImpl::from_session(Box::new(http_client::native::NativeClient::new()), test_session);
976///
977/// let mut artists = client.artists();
978///
979/// // Get the top 10 artists
980/// let top_artists = artists.take(10).await?;
981/// for artist in top_artists {
982/// println!("{} ({} plays)", artist.name, artist.playcount);
983/// }
984/// # Ok::<(), Box<dyn std::error::Error>>(())
985/// # });
986/// ```
987pub struct ArtistsIterator<C: LastFmEditClient> {
988 client: C,
989 current_page: u32,
990 has_more: bool,
991 buffer: Vec<crate::Artist>,
992 total_pages: Option<u32>,
993}
994
995#[async_trait(?Send)]
996impl<C: LastFmEditClient> AsyncPaginatedIterator<crate::Artist> for ArtistsIterator<C> {
997 async fn next(&mut self) -> Result<Option<crate::Artist>> {
998 // If buffer is empty, try to load next page
999 if self.buffer.is_empty() {
1000 if let Some(page) = self.next_page().await? {
1001 self.buffer = page.artists;
1002 self.buffer.reverse(); // Reverse so we can pop from end efficiently
1003 }
1004 }
1005
1006 Ok(self.buffer.pop())
1007 }
1008
1009 fn current_page(&self) -> u32 {
1010 self.current_page.saturating_sub(1)
1011 }
1012
1013 fn total_pages(&self) -> Option<u32> {
1014 self.total_pages
1015 }
1016}
1017
1018impl<C: LastFmEditClient> ArtistsIterator<C> {
1019 /// Create a new artists iterator.
1020 ///
1021 /// This iterator will start from page 1 and load all artists in the user's library.
1022 pub fn new(client: C) -> Self {
1023 Self {
1024 client,
1025 current_page: 1,
1026 has_more: true,
1027 buffer: Vec::new(),
1028 total_pages: None,
1029 }
1030 }
1031
1032 /// Create a new artists iterator starting from a specific page.
1033 ///
1034 /// This is useful for implementing offset functionality efficiently by starting
1035 /// at the appropriate page rather than iterating through all previous pages.
1036 pub fn with_starting_page(client: C, starting_page: u32) -> Self {
1037 let page = std::cmp::max(1, starting_page);
1038 Self {
1039 client,
1040 current_page: page,
1041 has_more: true,
1042 buffer: Vec::new(),
1043 total_pages: None,
1044 }
1045 }
1046
1047 /// Fetch the next page of artists.
1048 ///
1049 /// This method handles pagination automatically and includes rate limiting
1050 /// to be respectful to Last.fm's servers.
1051 pub async fn next_page(&mut self) -> Result<Option<crate::ArtistPage>> {
1052 if !self.has_more {
1053 return Ok(None);
1054 }
1055
1056 let page = self.client.get_artists_page(self.current_page).await?;
1057
1058 self.has_more = page.has_next_page;
1059 self.current_page += 1;
1060 self.total_pages = page.total_pages;
1061
1062 Ok(Some(page))
1063 }
1064
1065 /// Get the total number of pages, if known.
1066 ///
1067 /// Returns `None` until at least one page has been fetched.
1068 pub fn total_pages(&self) -> Option<u32> {
1069 self.total_pages
1070 }
1071}