Skip to main content

lastfm_edit/
trait.rs

1use crate::iterator::AsyncPaginatedIterator;
2use crate::types::{
3    Album, Artist, ArtistPage, ClientEvent, ClientEventReceiver, EditResponse, ExactScrobbleEdit,
4    LastFmEditSession, ScrobbleEdit, Track,
5};
6use crate::Result;
7use async_trait::async_trait;
8
9/// Low-level trait for individual Last.fm page fetches, search, and session management.
10///
11/// This trait abstracts single-request operations: fetching a page of data,
12/// performing a search query, and managing session/cancellation state.
13/// It serves as the foundation that higher-level traits like [`LastFmEditClient`]
14/// build upon.
15///
16/// # Mocking Support
17///
18/// When the `mock` feature is enabled, this crate provides `MockLastFmBaseClient`
19/// that implements this trait using the `mockall` library.
20#[cfg_attr(feature = "mock", mockall::automock)]
21#[async_trait(?Send)]
22pub trait LastFmBaseClient {
23    // =============================================================================
24    // PAGE FETCHING - Single page data access
25    // =============================================================================
26
27    /// Get a page of artists from the user's library.
28    async fn get_artists_page(&self, page: u32) -> Result<ArtistPage>;
29
30    /// Get a page of tracks from the user's library for the specified artist.
31    async fn get_artist_tracks_page(&self, artist: &str, page: u32) -> Result<crate::TrackPage>;
32
33    /// Get a page of albums from the user's library for the specified artist.
34    async fn get_artist_albums_page(&self, artist: &str, page: u32) -> Result<crate::AlbumPage>;
35
36    /// Get a page of tracks from a specific album in the user's library.
37    async fn get_album_tracks_page(
38        &self,
39        album_name: &str,
40        artist_name: &str,
41        page: u32,
42    ) -> Result<crate::TrackPage>;
43
44    /// Get a page of tracks from the user's recent listening history.
45    async fn get_recent_tracks_page(&self, page: u32) -> Result<crate::TrackPage>;
46
47    // =============================================================================
48    // SEARCH PAGES - Single page search results
49    // =============================================================================
50
51    /// Get a single page of track search results from the user's library.
52    ///
53    /// This performs a search using Last.fm's library search functionality,
54    /// returning one page of tracks that match the provided query string.
55    /// For iterator-based access, use [`LastFmEditClient::search_tracks`] instead.
56    ///
57    /// # Arguments
58    ///
59    /// * `query` - The search query (e.g., "remaster", "live", artist name, etc.)
60    /// * `page` - The page number to retrieve (1-based)
61    ///
62    /// # Returns
63    ///
64    /// Returns a `TrackPage` containing the search results with pagination information.
65    async fn search_tracks_page(&self, query: &str, page: u32) -> Result<crate::TrackPage>;
66
67    /// Get a single page of album search results from the user's library.
68    ///
69    /// This performs a search using Last.fm's library search functionality,
70    /// returning one page of albums that match the provided query string.
71    /// For iterator-based access, use [`LastFmEditClient::search_albums`] instead.
72    ///
73    /// # Arguments
74    ///
75    /// * `query` - The search query (e.g., "remaster", "deluxe", artist name, etc.)
76    /// * `page` - The page number to retrieve (1-based)
77    ///
78    /// # Returns
79    ///
80    /// Returns an `AlbumPage` containing the search results with pagination information.
81    async fn search_albums_page(&self, query: &str, page: u32) -> Result<crate::AlbumPage>;
82
83    /// Get a single page of artist search results from the user's library.
84    ///
85    /// This performs a search using Last.fm's library search functionality,
86    /// returning one page of artists that match the provided query string.
87    /// For iterator-based access, use [`LastFmEditClient::search_artists`] instead.
88    ///
89    /// # Arguments
90    ///
91    /// * `query` - The search query (e.g., artist name, partial match, etc.)
92    /// * `page` - The page number to retrieve (1-based)
93    ///
94    /// # Returns
95    ///
96    /// Returns an `ArtistPage` containing the search results with pagination information.
97    async fn search_artists_page(&self, query: &str, page: u32) -> Result<crate::ArtistPage>;
98
99    // =============================================================================
100    // INFRASTRUCTURE - Session, events, and authentication
101    // =============================================================================
102
103    /// Get the currently authenticated username.
104    fn username(&self) -> String;
105
106    /// Extract the current session state for persistence.
107    ///
108    /// This allows you to save the authentication state and restore it later
109    /// without requiring the user to log in again.
110    ///
111    /// # Returns
112    ///
113    /// Returns a [`LastFmEditSession`] that can be serialized and saved.
114    fn get_session(&self) -> LastFmEditSession;
115
116    /// Subscribe to internal client events.
117    ///
118    /// Returns a broadcast receiver that can be used to listen to events like rate limiting.
119    /// Multiple subscribers can listen simultaneously.
120    ///
121    /// # Example
122    /// ```rust,no_run
123    /// use lastfm_edit::{LastFmEditClientImpl, LastFmEditSession, ClientEvent};
124    ///
125    /// let http_client = http_client::native::NativeClient::new();
126    /// let test_session = LastFmEditSession::new("test".to_string(), vec!["sessionid=.test123".to_string()], Some("csrf".to_string()), "https://www.last.fm".to_string());
127    /// let client = LastFmEditClientImpl::from_session(Box::new(http_client), test_session);
128    /// let mut events = client.subscribe();
129    ///
130    /// // Listen for events in a background task
131    /// tokio::spawn(async move {
132    ///     while let Ok(event) = events.recv().await {
133    ///         match event {
134    ///             ClientEvent::RequestStarted { request } => {
135    ///                 println!("Request started: {}", request.short_description());
136    ///             }
137    ///             ClientEvent::RequestCompleted { request, status_code, duration_ms } => {
138    ///                 println!("Request completed: {} - {} ({} ms)", request.short_description(), status_code, duration_ms);
139    ///             }
140    ///             ClientEvent::RateLimited { delay_seconds, .. } => {
141    ///                 println!("Rate limited! Waiting {} seconds", delay_seconds);
142    ///             }
143    ///             ClientEvent::RateLimitEnded { total_rate_limit_duration_seconds, .. } => {
144    ///                 println!("Rate limiting ended after {} seconds", total_rate_limit_duration_seconds);
145    ///             }
146    ///             ClientEvent::Delaying { delay_ms, reason, .. } => {
147    ///                 println!("Delaying ({reason:?}) for {delay_ms}ms");
148    ///             }
149    ///             ClientEvent::EditAttempted { edit, success, .. } => {
150    ///                 println!("Edit attempt: '{}' -> '{}' - {}",
151    ///                          edit.track_name_original, edit.track_name,
152    ///                          if success { "Success" } else { "Failed" });
153    ///             }
154    ///             _ => {}
155    ///         }
156    ///     }
157    /// });
158    /// ```
159    fn subscribe(&self) -> ClientEventReceiver;
160
161    /// Get the latest client event without subscribing to future events.
162    ///
163    /// This returns the most recent event that occurred, or `None` if no events have occurred yet.
164    /// Unlike `subscribe()`, this provides instant access to the current state without waiting.
165    ///
166    /// # Example
167    /// ```rust,no_run
168    /// use lastfm_edit::{LastFmEditClientImpl, LastFmEditSession, ClientEvent};
169    ///
170    /// let http_client = http_client::native::NativeClient::new();
171    /// let test_session = LastFmEditSession::new("test".to_string(), vec!["sessionid=.test123".to_string()], Some("csrf".to_string()), "https://www.last.fm".to_string());
172    /// let client = LastFmEditClientImpl::from_session(Box::new(http_client), test_session);
173    ///
174    /// if let Some(ClientEvent::RateLimited { delay_seconds, .. }) = client.latest_event() {
175    ///     println!("Currently rate limited for {} seconds", delay_seconds);
176    /// }
177    /// ```
178    fn latest_event(&self) -> Option<ClientEvent>;
179
180    /// Validate if the current session is still working.
181    ///
182    /// This method makes a test request to a protected Last.fm settings page to verify
183    /// that the current session is still valid. If the session has expired or become
184    /// invalid, Last.fm will redirect to the login page.
185    ///
186    /// This is useful for checking session validity before attempting operations that
187    /// require authentication, especially after loading a previously saved session.
188    ///
189    /// # Returns
190    ///
191    /// Returns `true` if the session is valid and can be used for authenticated operations,
192    /// `false` if the session is invalid or expired.
193    async fn validate_session(&self) -> bool;
194
195    // =============================================================================
196    // READ HELPER
197    // =============================================================================
198
199    /// Find the most recent scrobble for a specific track.
200    async fn find_recent_scrobble_for_track(
201        &self,
202        track_name: &str,
203        artist_name: &str,
204        max_pages: u32,
205    ) -> Result<Option<Track>>;
206
207    // =============================================================================
208    // CANCELLATION - Cooperative cancellation for long-running operations
209    // =============================================================================
210
211    /// Request cooperative cancellation of ongoing operations (best-effort).
212    ///
213    /// Implementations should interrupt internal waits (retry backoff, operational delays)
214    /// and return `LastFmError::Io(ErrorKind::Interrupted)` where appropriate.
215    fn cancel(&self) {}
216
217    /// Clear the cancellation request so future operations can run again.
218    fn reset_cancel(&self) {}
219
220    /// Whether cancellation has been requested.
221    fn is_cancelled(&self) -> bool {
222        false
223    }
224}
225
226/// High-level trait for Last.fm client operations including iterators, discovery, and editing.
227///
228/// This trait builds on [`LastFmBaseClient`] to provide composite operations:
229/// iterator factories for paginated browsing, scrobble discovery, and editing workflows.
230///
231/// # Mocking Support
232///
233/// When the `mock` feature is enabled, this crate provides `MockLastFmEditClient`
234/// that implements this trait using the `mockall` library.
235///
236#[async_trait(?Send)]
237pub trait LastFmEditClient: LastFmBaseClient {
238    // =============================================================================
239    // CORE EDITING METHODS - Most important functionality
240    // =============================================================================
241
242    /// Edit scrobbles by discovering and updating all matching instances.
243    ///
244    /// This is the main editing method that automatically discovers all scrobble instances
245    /// that match the provided criteria and applies the specified changes to each one.
246    ///
247    /// # How it works
248    ///
249    /// 1. **Discovery**: Analyzes the `ScrobbleEdit` to determine what to search for:
250    ///    - If `track_name_original` is specified: finds all album variations of that track
251    ///    - If only `album_name_original` is specified: finds all tracks in that album
252    ///    - If neither is specified: finds all tracks by that artist
253    ///
254    /// 2. **Enrichment**: For each discovered scrobble, extracts complete metadata
255    ///    including album artist information from the user's library
256    ///
257    /// 3. **Editing**: Applies the requested changes to each discovered instance
258    ///
259    /// # Arguments
260    ///
261    /// * `edit` - A `ScrobbleEdit` specifying what to find and how to change it
262    ///
263    /// # Returns
264    ///
265    /// Returns an `EditResponse` containing results for all edited scrobbles, including:
266    /// - Overall success status
267    /// - Individual results for each scrobble instance
268    /// - Detailed error messages if any edits fail
269    ///
270    /// # Errors
271    ///
272    /// Returns `LastFmError::Parse` if no matching scrobbles are found, or other errors
273    /// for network/authentication issues.
274    ///
275    /// # Example
276    ///
277    /// ```rust,no_run
278    /// # use lastfm_edit::{LastFmEditClient, ScrobbleEdit, Result};
279    /// # async fn example(client: &dyn LastFmEditClient) -> Result<()> {
280    /// // Change track name for all instances of a track
281    /// let edit = ScrobbleEdit::from_track_and_artist("Old Track Name", "Artist")
282    ///     .with_track_name("New Track Name");
283    ///
284    /// let response = client.edit_scrobble(&edit).await?;
285    /// if response.success() {
286    ///     println!("Successfully edited {} scrobbles", response.total_edits());
287    /// }
288    /// # Ok(())
289    /// # }
290    /// ```
291    async fn edit_scrobble(&self, edit: &ScrobbleEdit) -> Result<EditResponse>;
292
293    /// Edit a single scrobble with complete information and retry logic.
294    ///
295    /// This method performs a single edit operation on a fully-specified scrobble.
296    /// Unlike [`edit_scrobble`], this method does not perform discovery, enrichment,
297    /// or multiple edits - it edits exactly one scrobble instance.
298    ///
299    /// # Key Differences from `edit_scrobble`
300    ///
301    /// - **No discovery**: Requires a fully-specified `ExactScrobbleEdit`
302    /// - **Single edit**: Only edits one scrobble instance
303    /// - **No enrichment**: All fields must be provided upfront
304    /// - **Retry logic**: Automatically retries on rate limiting
305    ///
306    /// # Arguments
307    ///
308    /// * `exact_edit` - A fully-specified edit with all required fields populated,
309    ///   including original metadata and timestamps
310    /// * `max_retries` - Maximum number of retry attempts for rate limiting.
311    ///   The method will wait with exponential backoff between retries.
312    ///
313    /// # Returns
314    ///
315    /// Returns an `EditResponse` with a single result indicating success or failure.
316    /// If max retries are exceeded due to rate limiting, returns a failed response
317    /// rather than an error.
318    ///
319    /// # Example
320    ///
321    /// ```rust,no_run
322    /// # use lastfm_edit::{LastFmEditClient, ExactScrobbleEdit, Result};
323    /// # async fn example(client: &dyn LastFmEditClient) -> Result<()> {
324    /// let exact_edit = ExactScrobbleEdit::new(
325    ///     "Original Track".to_string(),
326    ///     "Original Album".to_string(),
327    ///     "Artist".to_string(),
328    ///     "Artist".to_string(),
329    ///     "New Track Name".to_string(),
330    ///     "Original Album".to_string(),
331    ///     "Artist".to_string(),
332    ///     "Artist".to_string(),
333    ///     1640995200, // timestamp
334    ///     false
335    /// );
336    ///
337    /// let response = client.edit_scrobble_single(&exact_edit, 3).await?;
338    /// # Ok(())
339    /// # }
340    /// ```
341    async fn edit_scrobble_single(
342        &self,
343        exact_edit: &ExactScrobbleEdit,
344        max_retries: u32,
345    ) -> Result<EditResponse>;
346
347    /// Delete a scrobble by its identifying information.
348    ///
349    /// This method deletes a specific scrobble from the user's library using the
350    /// artist name, track name, and timestamp to uniquely identify it.
351    ///
352    /// # Arguments
353    ///
354    /// * `artist_name` - The artist name of the scrobble to delete
355    /// * `track_name` - The track name of the scrobble to delete
356    /// * `timestamp` - The unix timestamp of the scrobble to delete
357    ///
358    /// # Returns
359    ///
360    /// Returns `true` if the deletion was successful, `false` otherwise.
361    async fn delete_scrobble(
362        &self,
363        artist_name: &str,
364        track_name: &str,
365        timestamp: u64,
366    ) -> Result<bool>;
367
368    /// Create an incremental discovery iterator for scrobble editing.
369    ///
370    /// This returns the appropriate discovery iterator based on what fields are specified
371    /// in the ScrobbleEdit. The iterator yields `ExactScrobbleEdit` results incrementally,
372    /// which helps avoid rate limiting issues when discovering many scrobbles.
373    ///
374    /// Returns a `Box<dyn AsyncDiscoveryIterator<ExactScrobbleEdit>>` to handle the different
375    /// discovery strategies uniformly.
376    fn discover_scrobbles(
377        &self,
378        edit: ScrobbleEdit,
379    ) -> Box<dyn crate::AsyncDiscoveryIterator<crate::ExactScrobbleEdit>>;
380
381    // =============================================================================
382    // ITERATOR METHODS - Core library browsing functionality
383    // =============================================================================
384
385    /// Create an iterator for browsing all artists in the user's library.
386    fn artists(&self) -> Box<dyn AsyncPaginatedIterator<Artist>>;
387
388    /// Create an iterator for browsing an artist's tracks from the user's library.
389    fn artist_tracks(&self, artist: &str) -> Box<dyn AsyncPaginatedIterator<Track>>;
390
391    /// Create an iterator for browsing an artist's tracks directly using the paginated endpoint.
392    ///
393    /// This alternative approach uses
394    /// `/user/{username}/library/music/{artist}/+tracks` directly with
395    /// pagination, which is more efficient than the album-based approach since
396    /// it doesn't need to iterate through albums first. The downside of this
397    /// approach is that the tracks will not come with album information, which
398    /// will need to get looked up eventually in the process of making edits.
399    fn artist_tracks_direct(&self, artist: &str) -> Box<dyn AsyncPaginatedIterator<Track>>;
400
401    /// Create an iterator for browsing an artist's albums from the user's library.
402    fn artist_albums(&self, artist: &str) -> Box<dyn AsyncPaginatedIterator<Album>>;
403
404    /// Create an iterator for browsing tracks from a specific album.
405    fn album_tracks(
406        &self,
407        album_name: &str,
408        artist_name: &str,
409    ) -> Box<dyn AsyncPaginatedIterator<Track>>;
410
411    /// Create an iterator for browsing the user's recent tracks/scrobbles.
412    fn recent_tracks(&self) -> Box<dyn AsyncPaginatedIterator<Track>>;
413
414    /// Create an iterator for browsing the user's recent tracks starting from a specific page.
415    fn recent_tracks_from_page(&self, starting_page: u32)
416        -> Box<dyn AsyncPaginatedIterator<Track>>;
417
418    /// Create an iterator for searching tracks in the user's library.
419    ///
420    /// This returns an iterator that uses Last.fm's library search functionality
421    /// to find tracks matching the provided query string. The iterator handles
422    /// pagination automatically.
423    ///
424    /// # Arguments
425    ///
426    /// * `query` - The search query (e.g., "remaster", "live", artist name, etc.)
427    ///
428    /// # Returns
429    ///
430    /// Returns a `SearchTracksIterator` for streaming search results.
431    fn search_tracks(&self, query: &str) -> Box<dyn AsyncPaginatedIterator<Track>>;
432
433    /// Create an iterator for searching albums in the user's library.
434    ///
435    /// This returns an iterator that uses Last.fm's library search functionality
436    /// to find albums matching the provided query string. The iterator handles
437    /// pagination automatically.
438    ///
439    /// # Arguments
440    ///
441    /// * `query` - The search query (e.g., "remaster", "deluxe", artist name, etc.)
442    ///
443    /// # Returns
444    ///
445    /// Returns a `SearchAlbumsIterator` for streaming search results.
446    fn search_albums(&self, query: &str) -> Box<dyn AsyncPaginatedIterator<Album>>;
447
448    /// Create an iterator for searching artists in the user's library.
449    ///
450    /// This returns an iterator that uses Last.fm's library search functionality
451    /// to find artists matching the provided query string. The iterator handles
452    /// pagination automatically.
453    ///
454    /// # Arguments
455    ///
456    /// * `query` - The search query (e.g., artist name, partial match, etc.)
457    ///
458    /// # Returns
459    ///
460    /// Returns a `SearchArtistsIterator` for streaming search results.
461    fn search_artists(&self, query: &str) -> Box<dyn AsyncPaginatedIterator<Artist>>;
462
463    // =============================================================================
464    // CONVENIENCE METHODS - Higher-level helpers and shortcuts
465    // =============================================================================
466
467    /// Discover all scrobble edit variations based on the provided ScrobbleEdit template.
468    ///
469    /// This method analyzes what fields are specified in the input ScrobbleEdit and discovers
470    /// all relevant scrobble instances that match the criteria:
471    /// - If track_name_original is specified: discovers all album variations of that track
472    /// - If only album_name_original is specified: discovers all tracks in that album
473    /// - If neither is specified: discovers all tracks by that artist
474    ///
475    /// Returns fully-specified ExactScrobbleEdit instances with all metadata populated
476    /// from the user's library, ready for editing operations.
477    async fn discover_scrobble_edit_variations(
478        &self,
479        edit: &ScrobbleEdit,
480    ) -> Result<Vec<ExactScrobbleEdit>> {
481        // Use the incremental iterator and collect all results
482        let mut discovery_iterator = self.discover_scrobbles(edit.clone());
483        discovery_iterator.collect_all().await
484    }
485
486    /// Edit album metadata by updating scrobbles with new album name.
487    async fn edit_album(
488        &self,
489        old_album_name: &str,
490        new_album_name: &str,
491        artist_name: &str,
492    ) -> Result<EditResponse> {
493        log::debug!("Editing album '{old_album_name}' -> '{new_album_name}' by '{artist_name}'");
494
495        let edit = ScrobbleEdit::for_album(old_album_name, artist_name, artist_name)
496            .with_album_name(new_album_name);
497
498        self.edit_scrobble(&edit).await
499    }
500
501    /// Edit artist metadata by updating scrobbles with new artist name.
502    ///
503    /// This edits ALL tracks from the artist that are found in recent scrobbles.
504    async fn edit_artist(
505        &self,
506        old_artist_name: &str,
507        new_artist_name: &str,
508    ) -> Result<EditResponse> {
509        log::debug!("Editing artist '{old_artist_name}' -> '{new_artist_name}'");
510
511        let edit = ScrobbleEdit::for_artist(old_artist_name, new_artist_name);
512
513        self.edit_scrobble(&edit).await
514    }
515
516    /// Edit artist metadata for a specific track only.
517    ///
518    /// This edits only the specified track if found in recent scrobbles.
519    async fn edit_artist_for_track(
520        &self,
521        track_name: &str,
522        old_artist_name: &str,
523        new_artist_name: &str,
524    ) -> Result<EditResponse> {
525        log::debug!("Editing artist for track '{track_name}' from '{old_artist_name}' -> '{new_artist_name}'");
526
527        let edit = ScrobbleEdit::from_track_and_artist(track_name, old_artist_name)
528            .with_artist_name(new_artist_name);
529
530        self.edit_scrobble(&edit).await
531    }
532
533    /// Edit artist metadata for all tracks in a specific album.
534    ///
535    /// This edits ALL tracks from the specified album that are found in recent scrobbles.
536    async fn edit_artist_for_album(
537        &self,
538        album_name: &str,
539        old_artist_name: &str,
540        new_artist_name: &str,
541    ) -> Result<EditResponse> {
542        log::debug!("Editing artist for album '{album_name}' from '{old_artist_name}' -> '{new_artist_name}'");
543
544        let edit = ScrobbleEdit::for_album(album_name, old_artist_name, old_artist_name)
545            .with_artist_name(new_artist_name);
546
547        self.edit_scrobble(&edit).await
548    }
549}
550
551#[cfg(feature = "mock")]
552mockall::mock! {
553    pub LastFmEditClient {}
554
555    #[async_trait(?Send)]
556    impl LastFmBaseClient for LastFmEditClient {
557        async fn get_artists_page(&self, page: u32) -> Result<ArtistPage>;
558        async fn get_artist_tracks_page(&self, artist: &str, page: u32) -> Result<crate::TrackPage>;
559        async fn get_artist_albums_page(&self, artist: &str, page: u32) -> Result<crate::AlbumPage>;
560        async fn get_album_tracks_page(
561            &self,
562            album_name: &str,
563            artist_name: &str,
564            page: u32,
565        ) -> Result<crate::TrackPage>;
566        async fn get_recent_tracks_page(&self, page: u32) -> Result<crate::TrackPage>;
567        async fn search_tracks_page(&self, query: &str, page: u32) -> Result<crate::TrackPage>;
568        async fn search_albums_page(&self, query: &str, page: u32) -> Result<crate::AlbumPage>;
569        async fn search_artists_page(&self, query: &str, page: u32) -> Result<crate::ArtistPage>;
570        fn username(&self) -> String;
571        fn get_session(&self) -> LastFmEditSession;
572        fn subscribe(&self) -> ClientEventReceiver;
573        fn latest_event(&self) -> Option<ClientEvent>;
574        async fn validate_session(&self) -> bool;
575        async fn find_recent_scrobble_for_track(
576            &self,
577            track_name: &str,
578            artist_name: &str,
579            max_pages: u32,
580        ) -> Result<Option<Track>>;
581        fn cancel(&self);
582        fn reset_cancel(&self);
583        fn is_cancelled(&self) -> bool;
584    }
585
586    #[async_trait(?Send)]
587    impl LastFmEditClient for LastFmEditClient {
588        async fn edit_scrobble(&self, edit: &ScrobbleEdit) -> Result<EditResponse>;
589        async fn edit_scrobble_single(
590            &self,
591            exact_edit: &ExactScrobbleEdit,
592            max_retries: u32,
593        ) -> Result<EditResponse>;
594        async fn delete_scrobble(
595            &self,
596            artist_name: &str,
597            track_name: &str,
598            timestamp: u64,
599        ) -> Result<bool>;
600        fn discover_scrobbles(
601            &self,
602            edit: ScrobbleEdit,
603        ) -> Box<dyn crate::AsyncDiscoveryIterator<crate::ExactScrobbleEdit>>;
604        fn artists(&self) -> Box<dyn AsyncPaginatedIterator<Artist>>;
605        fn artist_tracks(&self, artist: &str) -> Box<dyn AsyncPaginatedIterator<Track>>;
606        fn artist_tracks_direct(&self, artist: &str) -> Box<dyn AsyncPaginatedIterator<Track>>;
607        fn artist_albums(&self, artist: &str) -> Box<dyn AsyncPaginatedIterator<Album>>;
608        fn album_tracks(
609            &self,
610            album_name: &str,
611            artist_name: &str,
612        ) -> Box<dyn AsyncPaginatedIterator<Track>>;
613        fn recent_tracks(&self) -> Box<dyn AsyncPaginatedIterator<Track>>;
614        fn recent_tracks_from_page(&self, starting_page: u32)
615            -> Box<dyn AsyncPaginatedIterator<Track>>;
616        fn search_tracks(&self, query: &str) -> Box<dyn AsyncPaginatedIterator<Track>>;
617        fn search_albums(&self, query: &str) -> Box<dyn AsyncPaginatedIterator<Album>>;
618        fn search_artists(&self, query: &str) -> Box<dyn AsyncPaginatedIterator<Artist>>;
619    }
620}