1use crate::api::LastFmApiClient;
2use crate::r#trait::LastFmBaseClient;
3use crate::{Album, AlbumPage, Result, Track, TrackPage};
4
5use async_trait::async_trait;
6
7#[cfg_attr(feature = "mock", mockall::automock)]
13#[async_trait(?Send)]
14pub trait AsyncPaginatedIterator<T> {
15 async fn next(&mut self) -> Result<Option<T>>;
26
27 async fn collect_all(&mut self) -> Result<Vec<T>> {
33 let mut items = Vec::new();
34 while let Some(item) = self.next().await? {
35 items.push(item);
36 }
37 Ok(items)
38 }
39
40 async fn take(&mut self, n: usize) -> Result<Vec<T>> {
49 let mut items = Vec::new();
50 for _ in 0..n {
51 match self.next().await? {
52 Some(item) => items.push(item),
53 None => break,
54 }
55 }
56 Ok(items)
57 }
58
59 fn current_page(&self) -> u32;
63
64 fn total_pages(&self) -> Option<u32> {
69 None }
71}
72
73pub struct ArtistTracksIterator<C: LastFmBaseClient> {
83 client: C,
84 artist: String,
85 album_iterator: Option<ArtistAlbumsIterator<C>>,
86 current_album_tracks: Option<AlbumTracksIterator<C>>,
87 track_buffer: Vec<Track>,
88 finished: bool,
89}
90
91#[async_trait(?Send)]
92impl<C: LastFmBaseClient + Clone> AsyncPaginatedIterator<Track> for ArtistTracksIterator<C> {
93 async fn next(&mut self) -> Result<Option<Track>> {
94 if self.finished {
96 return Ok(None);
97 }
98
99 while self.track_buffer.is_empty() {
101 if self.current_album_tracks.is_none() {
103 if self.album_iterator.is_none() {
105 self.album_iterator = Some(ArtistAlbumsIterator::new(
106 self.client.clone(),
107 self.artist.clone(),
108 ));
109 }
110
111 if let Some(ref mut album_iter) = self.album_iterator {
113 if let Some(album) = album_iter.next().await? {
114 log::debug!(
115 "Processing album '{}' for artist '{}'",
116 album.name,
117 self.artist
118 );
119 self.current_album_tracks = Some(AlbumTracksIterator::new(
121 self.client.clone(),
122 album.name.clone(),
123 self.artist.clone(),
124 ));
125 } else {
126 log::debug!("No more albums for artist '{}'", self.artist);
128 self.finished = true;
129 return Ok(None);
130 }
131 }
132 }
133
134 if let Some(ref mut album_tracks) = self.current_album_tracks {
136 if let Some(track) = album_tracks.next().await? {
137 self.track_buffer.push(track);
138 } else {
139 log::debug!(
141 "Finished processing current album for artist '{}'",
142 self.artist
143 );
144 self.current_album_tracks = None;
145 }
147 }
148 }
149
150 Ok(self.track_buffer.pop())
152 }
153
154 fn current_page(&self) -> u32 {
155 if let Some(ref album_iter) = self.album_iterator {
157 album_iter.current_page()
158 } else {
159 0
160 }
161 }
162
163 fn total_pages(&self) -> Option<u32> {
164 if let Some(ref album_iter) = self.album_iterator {
166 album_iter.total_pages()
167 } else {
168 None
169 }
170 }
171}
172
173impl<C: LastFmBaseClient + Clone> ArtistTracksIterator<C> {
174 pub fn new(client: C, artist: String) -> Self {
178 Self {
179 client,
180 artist,
181 album_iterator: None,
182 current_album_tracks: None,
183 track_buffer: Vec::new(),
184 finished: false,
185 }
186 }
187}
188
189pub struct ArtistTracksDirectIterator<C: LastFmBaseClient> {
197 client: C,
198 artist: String,
199 current_page: u32,
200 has_more: bool,
201 buffer: Vec<Track>,
202 total_pages: Option<u32>,
203 tracks_yielded: u32,
204}
205
206#[async_trait(?Send)]
207impl<C: LastFmBaseClient> AsyncPaginatedIterator<Track> for ArtistTracksDirectIterator<C> {
208 async fn next(&mut self) -> Result<Option<Track>> {
209 if self.buffer.is_empty() {
211 if let Some(page) = self.next_page().await? {
212 self.buffer = page.tracks;
213 self.buffer.reverse(); }
215 }
216
217 if let Some(track) = self.buffer.pop() {
218 self.tracks_yielded += 1;
219 Ok(Some(track))
220 } else {
221 Ok(None)
222 }
223 }
224
225 fn current_page(&self) -> u32 {
226 self.current_page.saturating_sub(1)
227 }
228
229 fn total_pages(&self) -> Option<u32> {
230 self.total_pages
231 }
232}
233
234impl<C: LastFmBaseClient> ArtistTracksDirectIterator<C> {
235 pub fn new(client: C, artist: String) -> Self {
239 Self {
240 client,
241 artist,
242 current_page: 1,
243 has_more: true,
244 buffer: Vec::new(),
245 total_pages: None,
246 tracks_yielded: 0,
247 }
248 }
249
250 pub async fn next_page(&mut self) -> Result<Option<TrackPage>> {
254 if !self.has_more {
255 return Ok(None);
256 }
257
258 log::debug!(
259 "Fetching page {} of {} tracks (yielded {} tracks so far)",
260 self.current_page,
261 self.artist,
262 self.tracks_yielded
263 );
264
265 let page = self
266 .client
267 .get_artist_tracks_page(&self.artist, self.current_page)
268 .await?;
269
270 self.has_more = page.has_next_page;
271 self.current_page += 1;
272 self.total_pages = page.total_pages;
273
274 Ok(Some(page))
275 }
276
277 pub fn total_pages(&self) -> Option<u32> {
281 self.total_pages
282 }
283}
284
285pub struct ArtistAlbumsIterator<C: LastFmBaseClient> {
290 client: C,
291 artist: String,
292 current_page: u32,
293 has_more: bool,
294 buffer: Vec<Album>,
295 total_pages: Option<u32>,
296}
297
298#[async_trait(?Send)]
299impl<C: LastFmBaseClient> AsyncPaginatedIterator<Album> for ArtistAlbumsIterator<C> {
300 async fn next(&mut self) -> Result<Option<Album>> {
301 if self.buffer.is_empty() {
303 if let Some(page) = self.next_page().await? {
304 self.buffer = page.albums;
305 self.buffer.reverse(); }
307 }
308
309 Ok(self.buffer.pop())
310 }
311
312 fn current_page(&self) -> u32 {
313 self.current_page.saturating_sub(1)
314 }
315
316 fn total_pages(&self) -> Option<u32> {
317 self.total_pages
318 }
319}
320
321impl<C: LastFmBaseClient> ArtistAlbumsIterator<C> {
322 pub fn new(client: C, artist: String) -> Self {
326 Self {
327 client,
328 artist,
329 current_page: 1,
330 has_more: true,
331 buffer: Vec::new(),
332 total_pages: None,
333 }
334 }
335
336 pub async fn next_page(&mut self) -> Result<Option<AlbumPage>> {
340 if !self.has_more {
341 return Ok(None);
342 }
343
344 let page = self
345 .client
346 .get_artist_albums_page(&self.artist, self.current_page)
347 .await?;
348
349 self.has_more = page.has_next_page;
350 self.current_page += 1;
351 self.total_pages = page.total_pages;
352
353 Ok(Some(page))
354 }
355
356 pub fn total_pages(&self) -> Option<u32> {
360 self.total_pages
361 }
362}
363
364pub struct RecentTracksIterator<C: LastFmBaseClient> {
370 client: C,
371 current_page: u32,
372 has_more: bool,
373 buffer: Vec<Track>,
374 stop_at_timestamp: Option<u64>,
375}
376
377#[async_trait(?Send)]
378impl<C: LastFmBaseClient> AsyncPaginatedIterator<Track> for RecentTracksIterator<C> {
379 async fn next(&mut self) -> Result<Option<Track>> {
380 if self.buffer.is_empty() {
382 if !self.has_more {
383 return Ok(None);
384 }
385
386 let page = self
387 .client
388 .get_recent_tracks_page(self.current_page)
389 .await?;
390
391 if page.tracks.is_empty() {
392 self.has_more = false;
393 return Ok(None);
394 }
395
396 self.has_more = page.has_next_page;
397
398 if let Some(stop_timestamp) = self.stop_at_timestamp {
400 let mut filtered_tracks = Vec::new();
401 for track in page.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 = page.tracks;
413 }
414
415 self.buffer.reverse(); 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: LastFmBaseClient> RecentTracksIterator<C> {
428 pub fn new(client: C) -> Self {
432 Self::with_starting_page(client, 1)
433 }
434
435 pub fn with_starting_page(client: C, starting_page: u32) -> Self {
445 let page = std::cmp::max(1, starting_page);
446 Self {
447 client,
448 current_page: page,
449 has_more: true,
450 buffer: Vec::new(),
451 stop_at_timestamp: None,
452 }
453 }
454
455 pub fn with_stop_timestamp(mut self, timestamp: u64) -> Self {
465 self.stop_at_timestamp = Some(timestamp);
466 self
467 }
468}
469
470pub struct ApiRecentTracksIterator<C: LastFmApiClient> {
476 client: C,
477 current_page: u32,
478 has_more: bool,
479 buffer: Vec<Track>,
480 stop_at_timestamp: Option<u64>,
481 total_pages: Option<u32>,
482}
483
484#[async_trait(?Send)]
485impl<C: LastFmApiClient> AsyncPaginatedIterator<Track> for ApiRecentTracksIterator<C> {
486 async fn next(&mut self) -> Result<Option<Track>> {
487 if self.buffer.is_empty() {
488 if !self.has_more {
489 return Ok(None);
490 }
491
492 let page = self
493 .client
494 .api_get_recent_tracks_page(self.current_page)
495 .await?;
496
497 if page.tracks.is_empty() {
498 self.has_more = false;
499 return Ok(None);
500 }
501
502 self.has_more = page.has_next_page;
503 self.total_pages = page.total_pages;
504
505 if let Some(stop_timestamp) = self.stop_at_timestamp {
506 let mut filtered_tracks = Vec::new();
507 for track in page.tracks {
508 if let Some(track_timestamp) = track.timestamp {
509 if track_timestamp <= stop_timestamp {
510 self.has_more = false;
511 break;
512 }
513 }
514 filtered_tracks.push(track);
515 }
516 self.buffer = filtered_tracks;
517 } else {
518 self.buffer = page.tracks;
519 }
520
521 self.buffer.reverse();
522 self.current_page += 1;
523 }
524
525 Ok(self.buffer.pop())
526 }
527
528 fn current_page(&self) -> u32 {
529 self.current_page.saturating_sub(1)
530 }
531
532 fn total_pages(&self) -> Option<u32> {
533 self.total_pages
534 }
535}
536
537impl<C: LastFmApiClient> ApiRecentTracksIterator<C> {
538 pub fn new(client: C) -> Self {
539 Self::with_starting_page(client, 1)
540 }
541
542 pub fn with_starting_page(client: C, starting_page: u32) -> Self {
543 let page = std::cmp::max(1, starting_page);
544 Self {
545 client,
546 current_page: page,
547 has_more: true,
548 buffer: Vec::new(),
549 stop_at_timestamp: None,
550 total_pages: None,
551 }
552 }
553
554 pub fn with_stop_timestamp(mut self, timestamp: u64) -> Self {
555 self.stop_at_timestamp = Some(timestamp);
556 self
557 }
558}
559
560pub struct AlbumTracksIterator<C: LastFmBaseClient> {
566 client: C,
567 album_name: String,
568 artist_name: String,
569 tracks: Option<Vec<Track>>,
570 index: usize,
571}
572
573#[async_trait(?Send)]
574impl<C: LastFmBaseClient> AsyncPaginatedIterator<Track> for AlbumTracksIterator<C> {
575 async fn next(&mut self) -> Result<Option<Track>> {
576 if self.tracks.is_none() {
578 let tracks_page = self
580 .client
581 .get_album_tracks_page(&self.album_name, &self.artist_name, 1)
582 .await?;
583 log::debug!(
584 "Album '{}' by '{}' has {} tracks: {:?}",
585 self.album_name,
586 self.artist_name,
587 tracks_page.tracks.len(),
588 tracks_page
589 .tracks
590 .iter()
591 .map(|t| &t.name)
592 .collect::<Vec<_>>()
593 );
594
595 if tracks_page.tracks.is_empty() {
596 log::warn!(
597 "🚨 ZERO TRACKS FOUND for album '{}' by '{}' - investigating...",
598 self.album_name,
599 self.artist_name
600 );
601 log::debug!("Full TrackPage for empty album: has_next_page={}, page_number={}, total_pages={:?}",
602 tracks_page.has_next_page, tracks_page.page_number, tracks_page.total_pages);
603 }
604 self.tracks = Some(tracks_page.tracks);
605 }
606
607 if let Some(tracks) = &self.tracks {
609 if self.index < tracks.len() {
610 let track = tracks[self.index].clone();
611 self.index += 1;
612 Ok(Some(track))
613 } else {
614 Ok(None)
615 }
616 } else {
617 Ok(None)
618 }
619 }
620
621 fn current_page(&self) -> u32 {
622 0
624 }
625}
626
627impl<C: LastFmBaseClient> AlbumTracksIterator<C> {
628 pub fn new(client: C, album_name: String, artist_name: String) -> Self {
632 Self {
633 client,
634 album_name,
635 artist_name,
636 tracks: None,
637 index: 0,
638 }
639 }
640}
641
642pub struct SearchTracksIterator<C: LastFmBaseClient> {
647 client: C,
648 query: String,
649 current_page: u32,
650 has_more: bool,
651 buffer: Vec<Track>,
652 total_pages: Option<u32>,
653}
654
655#[async_trait(?Send)]
656impl<C: LastFmBaseClient> AsyncPaginatedIterator<Track> for SearchTracksIterator<C> {
657 async fn next(&mut self) -> Result<Option<Track>> {
658 if self.buffer.is_empty() {
660 if let Some(page) = self.next_page().await? {
661 self.buffer = page.tracks;
662 self.buffer.reverse(); }
664 }
665
666 Ok(self.buffer.pop())
667 }
668
669 fn current_page(&self) -> u32 {
670 self.current_page.saturating_sub(1)
671 }
672
673 fn total_pages(&self) -> Option<u32> {
674 self.total_pages
675 }
676}
677
678impl<C: LastFmBaseClient> SearchTracksIterator<C> {
679 pub fn new(client: C, query: String) -> Self {
683 Self {
684 client,
685 query,
686 current_page: 1,
687 has_more: true,
688 buffer: Vec::new(),
689 total_pages: None,
690 }
691 }
692
693 pub fn with_starting_page(client: C, query: String, starting_page: u32) -> Self {
698 let page = std::cmp::max(1, starting_page);
699 Self {
700 client,
701 query,
702 current_page: page,
703 has_more: true,
704 buffer: Vec::new(),
705 total_pages: None,
706 }
707 }
708
709 pub async fn next_page(&mut self) -> Result<Option<TrackPage>> {
714 if !self.has_more {
715 return Ok(None);
716 }
717
718 let page = self
719 .client
720 .search_tracks_page(&self.query, self.current_page)
721 .await?;
722
723 self.has_more = page.has_next_page;
724 self.current_page += 1;
725 self.total_pages = page.total_pages;
726
727 Ok(Some(page))
728 }
729
730 pub fn total_pages(&self) -> Option<u32> {
734 self.total_pages
735 }
736}
737
738pub struct SearchAlbumsIterator<C: LastFmBaseClient> {
745 client: C,
746 query: String,
747 current_page: u32,
748 has_more: bool,
749 buffer: Vec<Album>,
750 total_pages: Option<u32>,
751}
752
753#[async_trait(?Send)]
754impl<C: LastFmBaseClient> AsyncPaginatedIterator<Album> for SearchAlbumsIterator<C> {
755 async fn next(&mut self) -> Result<Option<Album>> {
756 if self.buffer.is_empty() {
758 if let Some(page) = self.next_page().await? {
759 self.buffer = page.albums;
760 self.buffer.reverse(); }
762 }
763
764 Ok(self.buffer.pop())
765 }
766
767 fn current_page(&self) -> u32 {
768 self.current_page.saturating_sub(1)
769 }
770
771 fn total_pages(&self) -> Option<u32> {
772 self.total_pages
773 }
774}
775
776impl<C: LastFmBaseClient> SearchAlbumsIterator<C> {
777 pub fn new(client: C, query: String) -> Self {
781 Self {
782 client,
783 query,
784 current_page: 1,
785 has_more: true,
786 buffer: Vec::new(),
787 total_pages: None,
788 }
789 }
790
791 pub fn with_starting_page(client: C, query: String, starting_page: u32) -> Self {
796 let page = std::cmp::max(1, starting_page);
797 Self {
798 client,
799 query,
800 current_page: page,
801 has_more: true,
802 buffer: Vec::new(),
803 total_pages: None,
804 }
805 }
806
807 pub async fn next_page(&mut self) -> Result<Option<AlbumPage>> {
812 if !self.has_more {
813 return Ok(None);
814 }
815
816 let page = self
817 .client
818 .search_albums_page(&self.query, self.current_page)
819 .await?;
820
821 self.has_more = page.has_next_page;
822 self.current_page += 1;
823 self.total_pages = page.total_pages;
824
825 Ok(Some(page))
826 }
827
828 pub fn total_pages(&self) -> Option<u32> {
832 self.total_pages
833 }
834}
835
836pub struct SearchArtistsIterator<C: LastFmBaseClient> {
841 client: C,
842 query: String,
843 current_page: u32,
844 has_more: bool,
845 buffer: Vec<crate::Artist>,
846 total_pages: Option<u32>,
847}
848
849#[async_trait(?Send)]
850impl<C: LastFmBaseClient> AsyncPaginatedIterator<crate::Artist> for SearchArtistsIterator<C> {
851 async fn next(&mut self) -> Result<Option<crate::Artist>> {
852 if self.buffer.is_empty() {
854 if let Some(page) = self.next_page().await? {
855 self.buffer = page.artists;
856 self.buffer.reverse(); }
858 }
859
860 Ok(self.buffer.pop())
861 }
862
863 fn current_page(&self) -> u32 {
864 self.current_page.saturating_sub(1)
865 }
866
867 fn total_pages(&self) -> Option<u32> {
868 self.total_pages
869 }
870}
871
872impl<C: LastFmBaseClient> SearchArtistsIterator<C> {
873 pub fn new(client: C, query: String) -> Self {
877 Self {
878 client,
879 query,
880 current_page: 1,
881 has_more: true,
882 buffer: Vec::new(),
883 total_pages: None,
884 }
885 }
886
887 pub fn with_starting_page(client: C, query: String, starting_page: u32) -> Self {
892 let page = std::cmp::max(1, starting_page);
893 Self {
894 client,
895 query,
896 current_page: page,
897 has_more: true,
898 buffer: Vec::new(),
899 total_pages: None,
900 }
901 }
902
903 pub async fn next_page(&mut self) -> Result<Option<crate::ArtistPage>> {
908 if !self.has_more {
909 return Ok(None);
910 }
911
912 let page = self
913 .client
914 .search_artists_page(&self.query, self.current_page)
915 .await?;
916
917 self.has_more = page.has_next_page;
918 self.current_page += 1;
919 self.total_pages = page.total_pages;
920
921 Ok(Some(page))
922 }
923
924 pub fn total_pages(&self) -> Option<u32> {
928 self.total_pages
929 }
930}
931
932pub struct ArtistsIterator<C: LastFmBaseClient> {
942 client: C,
943 current_page: u32,
944 has_more: bool,
945 buffer: Vec<crate::Artist>,
946 total_pages: Option<u32>,
947}
948
949#[async_trait(?Send)]
950impl<C: LastFmBaseClient> AsyncPaginatedIterator<crate::Artist> for ArtistsIterator<C> {
951 async fn next(&mut self) -> Result<Option<crate::Artist>> {
952 if self.buffer.is_empty() {
954 if let Some(page) = self.next_page().await? {
955 self.buffer = page.artists;
956 self.buffer.reverse(); }
958 }
959
960 Ok(self.buffer.pop())
961 }
962
963 fn current_page(&self) -> u32 {
964 self.current_page.saturating_sub(1)
965 }
966
967 fn total_pages(&self) -> Option<u32> {
968 self.total_pages
969 }
970}
971
972impl<C: LastFmBaseClient> ArtistsIterator<C> {
973 pub fn new(client: C) -> Self {
977 Self {
978 client,
979 current_page: 1,
980 has_more: true,
981 buffer: Vec::new(),
982 total_pages: None,
983 }
984 }
985
986 pub fn with_starting_page(client: C, starting_page: u32) -> Self {
991 let page = std::cmp::max(1, starting_page);
992 Self {
993 client,
994 current_page: page,
995 has_more: true,
996 buffer: Vec::new(),
997 total_pages: None,
998 }
999 }
1000
1001 pub async fn next_page(&mut self) -> Result<Option<crate::ArtistPage>> {
1006 if !self.has_more {
1007 return Ok(None);
1008 }
1009
1010 let page = self.client.get_artists_page(self.current_page).await?;
1011
1012 self.has_more = page.has_next_page;
1013 self.current_page += 1;
1014 self.total_pages = page.total_pages;
1015
1016 Ok(Some(page))
1017 }
1018
1019 pub fn total_pages(&self) -> Option<u32> {
1023 self.total_pages
1024 }
1025}