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}