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