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 // Create album tracks iterator for this album
184 self.current_album_tracks = Some(AlbumTracksIterator::new(
185 self.client.clone(),
186 album.name.clone(),
187 self.artist.clone(),
188 ));
189 } else {
190 // No more albums, we're done
191 self.finished = true;
192 return Ok(None);
193 }
194 }
195 }
196
197 // Get tracks from current album
198 if let Some(ref mut album_tracks) = self.current_album_tracks {
199 if let Some(track) = album_tracks.next().await? {
200 self.track_buffer.push(track);
201 } else {
202 // This album is exhausted, move to next album
203 self.current_album_tracks = None;
204 // Continue the loop to try getting the next album
205 }
206 }
207 }
208
209 // Return the next track from our buffer
210 Ok(self.track_buffer.pop())
211 }
212
213 fn current_page(&self) -> u32 {
214 // Since we're iterating through albums, return the album iterator's current page
215 if let Some(ref album_iter) = self.album_iterator {
216 album_iter.current_page()
217 } else {
218 0
219 }
220 }
221
222 fn total_pages(&self) -> Option<u32> {
223 // Since we're iterating through albums, return the album iterator's total pages
224 if let Some(ref album_iter) = self.album_iterator {
225 album_iter.total_pages()
226 } else {
227 None
228 }
229 }
230}
231
232impl<C: LastFmEditClient + Clone> ArtistTracksIterator<C> {
233 /// Create a new artist tracks iterator.
234 ///
235 /// This is typically called via [`LastFmEditClient::artist_tracks`](crate::LastFmEditClient::artist_tracks).
236 pub fn new(client: C, artist: String) -> Self {
237 Self {
238 client,
239 artist,
240 album_iterator: None,
241 current_album_tracks: None,
242 track_buffer: Vec::new(),
243 finished: false,
244 }
245 }
246}
247
248/// Iterator for browsing an artist's albums from a user's library.
249///
250/// This iterator provides paginated access to all albums by a specific artist
251/// in the authenticated user's Last.fm library, ordered by play count.
252///
253/// # Examples
254///
255/// ```rust,no_run
256/// # use lastfm_edit::{LastFmEditClient, LastFmEditClientImpl, LastFmEditSession, AsyncPaginatedIterator};
257/// # tokio_test::block_on(async {
258/// # let test_session = LastFmEditSession::new("test".to_string(), vec!["sessionid=.test123".to_string()], Some("csrf".to_string()), "https://www.last.fm".to_string());
259/// let mut client = LastFmEditClientImpl::from_session(Box::new(http_client::native::NativeClient::new()), test_session);
260///
261/// let mut albums = client.artist_albums("Pink Floyd");
262///
263/// // Get all albums (be careful with large discographies!)
264/// while let Some(album) = albums.next().await? {
265/// println!("{} (played {} times)", album.name, album.playcount);
266/// }
267/// # Ok::<(), Box<dyn std::error::Error>>(())
268/// # });
269/// ```
270pub struct ArtistAlbumsIterator<C: LastFmEditClient> {
271 client: C,
272 artist: String,
273 current_page: u32,
274 has_more: bool,
275 buffer: Vec<Album>,
276 total_pages: Option<u32>,
277}
278
279#[async_trait(?Send)]
280impl<C: LastFmEditClient> AsyncPaginatedIterator<Album> for ArtistAlbumsIterator<C> {
281 async fn next(&mut self) -> Result<Option<Album>> {
282 // If buffer is empty, try to load next page
283 if self.buffer.is_empty() {
284 if let Some(page) = self.next_page().await? {
285 self.buffer = page.albums;
286 self.buffer.reverse(); // Reverse so we can pop from end efficiently
287 }
288 }
289
290 Ok(self.buffer.pop())
291 }
292
293 fn current_page(&self) -> u32 {
294 self.current_page.saturating_sub(1)
295 }
296
297 fn total_pages(&self) -> Option<u32> {
298 self.total_pages
299 }
300}
301
302impl<C: LastFmEditClient> ArtistAlbumsIterator<C> {
303 /// Create a new artist albums iterator.
304 ///
305 /// This is typically called via [`LastFmEditClient::artist_albums`](crate::LastFmEditClient::artist_albums).
306 pub fn new(client: C, artist: String) -> Self {
307 Self {
308 client,
309 artist,
310 current_page: 1,
311 has_more: true,
312 buffer: Vec::new(),
313 total_pages: None,
314 }
315 }
316
317 /// Fetch the next page of albums.
318 ///
319 /// This method handles pagination automatically and includes rate limiting.
320 pub async fn next_page(&mut self) -> Result<Option<AlbumPage>> {
321 if !self.has_more {
322 return Ok(None);
323 }
324
325 let page = self
326 .client
327 .get_artist_albums_page(&self.artist, self.current_page)
328 .await?;
329
330 self.has_more = page.has_next_page;
331 self.current_page += 1;
332 self.total_pages = page.total_pages;
333
334 Ok(Some(page))
335 }
336
337 /// Get the total number of pages, if known.
338 ///
339 /// Returns `None` until at least one page has been fetched.
340 pub fn total_pages(&self) -> Option<u32> {
341 self.total_pages
342 }
343}
344
345/// Iterator for browsing a user's recent tracks/scrobbles.
346///
347/// This iterator provides access to the user's recent listening history with timestamps,
348/// which is essential for finding tracks that can be edited. It supports optional
349/// timestamp-based filtering to avoid reprocessing old data.
350///
351/// # Examples
352///
353/// ```rust,no_run
354/// # use lastfm_edit::{LastFmEditClient, LastFmEditClientImpl, LastFmEditSession, AsyncPaginatedIterator};
355/// # tokio_test::block_on(async {
356/// # let test_session = LastFmEditSession::new("test".to_string(), vec!["sessionid=.test123".to_string()], Some("csrf".to_string()), "https://www.last.fm".to_string());
357/// let mut client = LastFmEditClientImpl::from_session(Box::new(http_client::native::NativeClient::new()), test_session);
358///
359/// // Get recent tracks with timestamps
360/// let mut recent = client.recent_tracks();
361/// while let Some(track) = recent.next().await? {
362/// if let Some(timestamp) = track.timestamp {
363/// println!("{} - {} ({})", track.artist, track.name, timestamp);
364/// }
365/// }
366///
367/// // Or stop at a specific timestamp to avoid reprocessing
368/// let last_processed = 1640995200;
369/// let mut recent = lastfm_edit::RecentTracksIterator::new(client).with_stop_timestamp(last_processed);
370/// let new_tracks = recent.collect_all().await?;
371/// # Ok::<(), Box<dyn std::error::Error>>(())
372/// # });
373/// ```
374pub struct RecentTracksIterator<C: LastFmEditClient> {
375 client: C,
376 current_page: u32,
377 has_more: bool,
378 buffer: Vec<Track>,
379 stop_at_timestamp: Option<u64>,
380}
381
382#[async_trait(?Send)]
383impl<C: LastFmEditClient> AsyncPaginatedIterator<Track> for RecentTracksIterator<C> {
384 async fn next(&mut self) -> Result<Option<Track>> {
385 // If buffer is empty, try to load next page
386 if self.buffer.is_empty() {
387 if !self.has_more {
388 return Ok(None);
389 }
390
391 let tracks = self.client.get_recent_scrobbles(self.current_page).await?;
392
393 if tracks.is_empty() {
394 self.has_more = false;
395 return Ok(None);
396 }
397
398 // Check if we should stop based on timestamp
399 if let Some(stop_timestamp) = self.stop_at_timestamp {
400 let mut filtered_tracks = Vec::new();
401 for track in tracks {
402 if let Some(track_timestamp) = track.timestamp {
403 if track_timestamp <= stop_timestamp {
404 self.has_more = false;
405 break;
406 }
407 }
408 filtered_tracks.push(track);
409 }
410 self.buffer = filtered_tracks;
411 } else {
412 self.buffer = tracks;
413 }
414
415 self.buffer.reverse(); // Reverse so we can pop from end efficiently
416 self.current_page += 1;
417 }
418
419 Ok(self.buffer.pop())
420 }
421
422 fn current_page(&self) -> u32 {
423 self.current_page.saturating_sub(1)
424 }
425}
426
427impl<C: LastFmEditClient> RecentTracksIterator<C> {
428 /// Create a new recent tracks iterator starting from page 1.
429 ///
430 /// This is typically called via [`LastFmEditClient::recent_tracks`](crate::LastFmEditClient::recent_tracks).
431 pub fn new(client: C) -> Self {
432 Self::with_starting_page(client, 1)
433 }
434
435 /// Create a new recent tracks iterator starting from a specific page.
436 ///
437 /// This allows resuming pagination from an arbitrary page, useful for
438 /// continuing from where a previous iteration left off.
439 ///
440 /// # Arguments
441 ///
442 /// * `client` - The LastFmEditClient to use for API calls
443 /// * `starting_page` - The page number to start from (1-indexed)
444 ///
445 /// # Examples
446 ///
447 /// ```rust,no_run
448 /// # use lastfm_edit::{LastFmEditClient, LastFmEditClientImpl, LastFmEditSession, AsyncPaginatedIterator};
449 /// # tokio_test::block_on(async {
450 /// # let test_session = LastFmEditSession::new("test".to_string(), vec!["sessionid=.test123".to_string()], Some("csrf".to_string()), "https://www.last.fm".to_string());
451 /// let mut client = LastFmEditClientImpl::from_session(Box::new(http_client::native::NativeClient::new()), test_session);
452 ///
453 /// // Start from page 5
454 /// let mut recent = client.recent_tracks_from_page(5);
455 /// let tracks = recent.take(10).await?;
456 /// # Ok::<(), Box<dyn std::error::Error>>(())
457 /// # });
458 /// ```
459 pub fn with_starting_page(client: C, starting_page: u32) -> Self {
460 let page = std::cmp::max(1, starting_page);
461 Self {
462 client,
463 current_page: page,
464 has_more: true,
465 buffer: Vec::new(),
466 stop_at_timestamp: None,
467 }
468 }
469
470 /// Set a timestamp to stop iteration at.
471 ///
472 /// When this is set, the iterator will stop returning tracks once it encounters
473 /// a track with a timestamp less than or equal to the specified value. This is
474 /// useful for incremental processing to avoid reprocessing old data.
475 ///
476 /// # Arguments
477 ///
478 /// * `timestamp` - Unix timestamp to stop at
479 ///
480 /// # Examples
481 ///
482 /// ```rust,no_run
483 /// # use lastfm_edit::{LastFmEditClient, LastFmEditClientImpl, LastFmEditSession, AsyncPaginatedIterator};
484 /// # tokio_test::block_on(async {
485 /// # let test_session = LastFmEditSession::new("test".to_string(), vec!["sessionid=.test123".to_string()], Some("csrf".to_string()), "https://www.last.fm".to_string());
486 /// let mut client = LastFmEditClientImpl::from_session(Box::new(http_client::native::NativeClient::new()), test_session);
487 /// let last_processed = 1640995200; // Some previous timestamp
488 ///
489 /// let mut recent = lastfm_edit::RecentTracksIterator::new(client).with_stop_timestamp(last_processed);
490 /// let new_tracks = recent.collect_all().await?; // Only gets new tracks
491 /// # Ok::<(), Box<dyn std::error::Error>>(())
492 /// # });
493 /// ```
494 pub fn with_stop_timestamp(mut self, timestamp: u64) -> Self {
495 self.stop_at_timestamp = Some(timestamp);
496 self
497 }
498}
499
500/// Iterator for browsing tracks in a specific album from a user's library.
501///
502/// This iterator provides access to all tracks in a specific album by an artist
503/// in the authenticated user's Last.fm library. Unlike paginated iterators,
504/// this loads tracks once and iterates through them.
505///
506/// # Examples
507///
508/// ```rust,no_run
509/// # use lastfm_edit::{LastFmEditClient, LastFmEditClientImpl, LastFmEditSession, AsyncPaginatedIterator};
510/// # tokio_test::block_on(async {
511/// # let test_session = LastFmEditSession::new("test".to_string(), vec!["sessionid=.test123".to_string()], Some("csrf".to_string()), "https://www.last.fm".to_string());
512/// let mut client = LastFmEditClientImpl::from_session(Box::new(http_client::native::NativeClient::new()), test_session);
513///
514/// let mut tracks = client.album_tracks("The Dark Side of the Moon", "Pink Floyd");
515///
516/// // Get all tracks in the album
517/// while let Some(track) = tracks.next().await? {
518/// println!("{} - {}", track.name, track.artist);
519/// }
520/// # Ok::<(), Box<dyn std::error::Error>>(())
521/// # });
522/// ```
523pub struct AlbumTracksIterator<C: LastFmEditClient> {
524 client: C,
525 album_name: String,
526 artist_name: String,
527 tracks: Option<Vec<Track>>,
528 index: usize,
529}
530
531#[async_trait(?Send)]
532impl<C: LastFmEditClient> AsyncPaginatedIterator<Track> for AlbumTracksIterator<C> {
533 async fn next(&mut self) -> Result<Option<Track>> {
534 // Load tracks if not already loaded
535 if self.tracks.is_none() {
536 // Use get_album_tracks_page instead of get_album_tracks to avoid infinite recursion
537 let tracks_page = self
538 .client
539 .get_album_tracks_page(&self.album_name, &self.artist_name, 1)
540 .await?;
541 self.tracks = Some(tracks_page.tracks);
542 }
543
544 // Return next track
545 if let Some(tracks) = &self.tracks {
546 if self.index < tracks.len() {
547 let track = tracks[self.index].clone();
548 self.index += 1;
549 Ok(Some(track))
550 } else {
551 Ok(None)
552 }
553 } else {
554 Ok(None)
555 }
556 }
557
558 fn current_page(&self) -> u32 {
559 // Album tracks don't have pages, so return 0
560 0
561 }
562}
563
564impl<C: LastFmEditClient> AlbumTracksIterator<C> {
565 /// Create a new album tracks iterator.
566 ///
567 /// This is typically called via [`LastFmEditClient::album_tracks`](crate::LastFmEditClient::album_tracks).
568 pub fn new(client: C, album_name: String, artist_name: String) -> Self {
569 Self {
570 client,
571 album_name,
572 artist_name,
573 tracks: None,
574 index: 0,
575 }
576 }
577}
578
579/// Iterator for searching tracks in the user's library.
580///
581/// This iterator provides paginated access to tracks that match a search query
582/// in the authenticated user's Last.fm library, using Last.fm's built-in search functionality.
583///
584/// # Examples
585///
586/// ```rust,no_run
587/// # use lastfm_edit::{LastFmEditClient, LastFmEditClientImpl, LastFmEditSession, AsyncPaginatedIterator};
588/// # tokio_test::block_on(async {
589/// # let test_session = LastFmEditSession::new("test".to_string(), vec!["sessionid=.test123".to_string()], Some("csrf".to_string()), "https://www.last.fm".to_string());
590/// let mut client = LastFmEditClientImpl::from_session(Box::new(http_client::native::NativeClient::new()), test_session);
591///
592/// let mut search_results = client.search_tracks("remaster");
593///
594/// // Get first 20 search results
595/// while let Some(track) = search_results.next().await? {
596/// println!("{} - {} (played {} times)", track.artist, track.name, track.playcount);
597/// }
598/// # Ok::<(), Box<dyn std::error::Error>>(())
599/// # });
600/// ```
601pub struct SearchTracksIterator<C: LastFmEditClient> {
602 client: C,
603 query: String,
604 current_page: u32,
605 has_more: bool,
606 buffer: Vec<Track>,
607 total_pages: Option<u32>,
608}
609
610#[async_trait(?Send)]
611impl<C: LastFmEditClient> AsyncPaginatedIterator<Track> for SearchTracksIterator<C> {
612 async fn next(&mut self) -> Result<Option<Track>> {
613 // If buffer is empty, try to load next page
614 if self.buffer.is_empty() {
615 if let Some(page) = self.next_page().await? {
616 self.buffer = page.tracks;
617 self.buffer.reverse(); // Reverse so we can pop from end efficiently
618 }
619 }
620
621 Ok(self.buffer.pop())
622 }
623
624 fn current_page(&self) -> u32 {
625 self.current_page.saturating_sub(1)
626 }
627
628 fn total_pages(&self) -> Option<u32> {
629 self.total_pages
630 }
631}
632
633impl<C: LastFmEditClient> SearchTracksIterator<C> {
634 /// Create a new search tracks iterator.
635 ///
636 /// This is typically called via [`LastFmEditClient::search_tracks`](crate::LastFmEditClient::search_tracks).
637 pub fn new(client: C, query: String) -> Self {
638 Self {
639 client,
640 query,
641 current_page: 1,
642 has_more: true,
643 buffer: Vec::new(),
644 total_pages: None,
645 }
646 }
647
648 /// Create a new search tracks iterator starting from a specific page.
649 ///
650 /// This is useful for implementing offset functionality efficiently by starting
651 /// at the appropriate page rather than iterating through all previous pages.
652 pub fn with_starting_page(client: C, query: String, starting_page: u32) -> Self {
653 let page = std::cmp::max(1, starting_page);
654 Self {
655 client,
656 query,
657 current_page: page,
658 has_more: true,
659 buffer: Vec::new(),
660 total_pages: None,
661 }
662 }
663
664 /// Fetch the next page of search results.
665 ///
666 /// This method handles pagination automatically and includes rate limiting
667 /// to be respectful to Last.fm's servers.
668 pub async fn next_page(&mut self) -> Result<Option<TrackPage>> {
669 if !self.has_more {
670 return Ok(None);
671 }
672
673 let page = self
674 .client
675 .search_tracks_page(&self.query, self.current_page)
676 .await?;
677
678 self.has_more = page.has_next_page;
679 self.current_page += 1;
680 self.total_pages = page.total_pages;
681
682 Ok(Some(page))
683 }
684
685 /// Get the total number of pages, if known.
686 ///
687 /// Returns `None` until at least one page has been fetched.
688 pub fn total_pages(&self) -> Option<u32> {
689 self.total_pages
690 }
691}
692
693/// Iterator for searching albums in the user's library.
694///
695/// This iterator provides paginated access to albums that match a search query
696/// in the authenticated user's Last.fm library, using Last.fm's built-in search functionality.
697///
698/// # Examples
699///
700/// ```rust,no_run
701/// # use lastfm_edit::{LastFmEditClient, LastFmEditClientImpl, LastFmEditSession, AsyncPaginatedIterator};
702/// # tokio_test::block_on(async {
703/// # let test_session = LastFmEditSession::new("test".to_string(), vec!["sessionid=.test123".to_string()], Some("csrf".to_string()), "https://www.last.fm".to_string());
704/// let mut client = LastFmEditClientImpl::from_session(Box::new(http_client::native::NativeClient::new()), test_session);
705///
706/// let mut search_results = client.search_albums("deluxe");
707///
708/// // Get first 10 search results
709/// let top_10 = search_results.take(10).await?;
710/// for album in top_10 {
711/// println!("{} - {} (played {} times)", album.artist, album.name, album.playcount);
712/// }
713/// # Ok::<(), Box<dyn std::error::Error>>(())
714/// # });
715/// ```
716pub struct SearchAlbumsIterator<C: LastFmEditClient> {
717 client: C,
718 query: String,
719 current_page: u32,
720 has_more: bool,
721 buffer: Vec<Album>,
722 total_pages: Option<u32>,
723}
724
725#[async_trait(?Send)]
726impl<C: LastFmEditClient> AsyncPaginatedIterator<Album> for SearchAlbumsIterator<C> {
727 async fn next(&mut self) -> Result<Option<Album>> {
728 // If buffer is empty, try to load next page
729 if self.buffer.is_empty() {
730 if let Some(page) = self.next_page().await? {
731 self.buffer = page.albums;
732 self.buffer.reverse(); // Reverse so we can pop from end efficiently
733 }
734 }
735
736 Ok(self.buffer.pop())
737 }
738
739 fn current_page(&self) -> u32 {
740 self.current_page.saturating_sub(1)
741 }
742
743 fn total_pages(&self) -> Option<u32> {
744 self.total_pages
745 }
746}
747
748impl<C: LastFmEditClient> SearchAlbumsIterator<C> {
749 /// Create a new search albums iterator.
750 ///
751 /// This is typically called via [`LastFmEditClient::search_albums`](crate::LastFmEditClient::search_albums).
752 pub fn new(client: C, query: String) -> Self {
753 Self {
754 client,
755 query,
756 current_page: 1,
757 has_more: true,
758 buffer: Vec::new(),
759 total_pages: None,
760 }
761 }
762
763 /// Create a new search albums iterator starting from a specific page.
764 ///
765 /// This is useful for implementing offset functionality efficiently by starting
766 /// at the appropriate page rather than iterating through all previous pages.
767 pub fn with_starting_page(client: C, query: String, starting_page: u32) -> Self {
768 let page = std::cmp::max(1, starting_page);
769 Self {
770 client,
771 query,
772 current_page: page,
773 has_more: true,
774 buffer: Vec::new(),
775 total_pages: None,
776 }
777 }
778
779 /// Fetch the next page of search results.
780 ///
781 /// This method handles pagination automatically and includes rate limiting
782 /// to be respectful to Last.fm's servers.
783 pub async fn next_page(&mut self) -> Result<Option<AlbumPage>> {
784 if !self.has_more {
785 return Ok(None);
786 }
787
788 let page = self
789 .client
790 .search_albums_page(&self.query, self.current_page)
791 .await?;
792
793 self.has_more = page.has_next_page;
794 self.current_page += 1;
795 self.total_pages = page.total_pages;
796
797 Ok(Some(page))
798 }
799
800 /// Get the total number of pages, if known.
801 ///
802 /// Returns `None` until at least one page has been fetched.
803 pub fn total_pages(&self) -> Option<u32> {
804 self.total_pages
805 }
806}