1use crate::edit_analysis;
2use crate::headers;
3use crate::login::extract_cookies_from_response;
4use crate::parsing::LastFmParser;
5use crate::r#trait::LastFmEditClient;
6use crate::retry;
7use crate::types::{
8 AlbumPage, ClientConfig, ClientEvent, ClientEventReceiver, EditResponse, ExactScrobbleEdit,
9 LastFmEditSession, LastFmError, OperationalDelayConfig, RateLimitConfig, RateLimitType,
10 RequestInfo, RetryConfig, ScrobbleEdit, SharedEventBroadcaster, SingleEditResponse, Track,
11 TrackPage,
12};
13use crate::Result;
14use async_trait::async_trait;
15use http_client::{HttpClient, Request, Response};
16use http_types::{Method, Url};
17use scraper::{Html, Selector};
18use std::sync::{Arc, Mutex};
19
20#[derive(Clone)]
21pub struct LastFmEditClientImpl {
22 client: Arc<dyn HttpClient + Send + Sync>,
23 session: Arc<Mutex<LastFmEditSession>>,
24 parser: LastFmParser,
25 broadcaster: Arc<SharedEventBroadcaster>,
26 config: ClientConfig,
27}
28
29impl LastFmEditClientImpl {
30 fn lastfm_encode(&self, input: &str) -> String {
32 urlencoding::encode(input).to_string()
33 }
34
35 pub fn from_session(
36 client: Box<dyn HttpClient + Send + Sync>,
37 session: LastFmEditSession,
38 ) -> Self {
39 Self::from_session_with_arc(Arc::from(client), session)
40 }
41
42 fn from_session_with_arc(
43 client: Arc<dyn HttpClient + Send + Sync>,
44 session: LastFmEditSession,
45 ) -> Self {
46 Self::from_session_with_broadcaster_arc(
47 client,
48 session,
49 Arc::new(SharedEventBroadcaster::new()),
50 )
51 }
52
53 pub fn from_session_with_rate_limit_patterns(
54 client: Box<dyn HttpClient + Send + Sync>,
55 session: LastFmEditSession,
56 rate_limit_patterns: Vec<String>,
57 ) -> Self {
58 let config = ClientConfig::default()
59 .with_rate_limit_config(RateLimitConfig::default().with_patterns(rate_limit_patterns));
60 Self::from_session_with_client_config(client, session, config)
61 }
62
63 pub async fn login_with_credentials(
64 client: Box<dyn HttpClient + Send + Sync>,
65 username: &str,
66 password: &str,
67 ) -> Result<Self> {
68 let client_arc: Arc<dyn HttpClient + Send + Sync> = Arc::from(client);
69 let login_manager =
70 crate::login::LoginManager::new(client_arc.clone(), "https://www.last.fm".to_string());
71 let session = login_manager.login(username, password).await?;
72 Ok(Self::from_session_with_arc(client_arc, session))
73 }
74
75 pub fn from_session_with_client_config(
76 client: Box<dyn HttpClient + Send + Sync>,
77 session: LastFmEditSession,
78 config: ClientConfig,
79 ) -> Self {
80 Self::from_session_with_client_config_arc(Arc::from(client), session, config)
81 }
82
83 pub async fn login_with_credentials_and_client_config(
84 client: Box<dyn HttpClient + Send + Sync>,
85 username: &str,
86 password: &str,
87 config: ClientConfig,
88 ) -> Result<Self> {
89 let client_arc: Arc<dyn HttpClient + Send + Sync> = Arc::from(client);
90 let login_manager =
91 crate::login::LoginManager::new(client_arc.clone(), "https://www.last.fm".to_string());
92 let session = login_manager.login(username, password).await?;
93 Ok(Self::from_session_with_client_config_arc(
94 client_arc, session, config,
95 ))
96 }
97
98 pub fn from_session_with_config(
99 client: Box<dyn HttpClient + Send + Sync>,
100 session: LastFmEditSession,
101 retry_config: RetryConfig,
102 rate_limit_config: RateLimitConfig,
103 ) -> Self {
104 Self::from_session_with_config_arc(
105 Arc::from(client),
106 session,
107 retry_config,
108 rate_limit_config,
109 )
110 }
111
112 pub async fn login_with_credentials_and_config(
113 client: Box<dyn HttpClient + Send + Sync>,
114 username: &str,
115 password: &str,
116 retry_config: RetryConfig,
117 rate_limit_config: RateLimitConfig,
118 ) -> Result<Self> {
119 let client_arc: Arc<dyn HttpClient + Send + Sync> = Arc::from(client);
120 let login_manager =
121 crate::login::LoginManager::new(client_arc.clone(), "https://www.last.fm".to_string());
122 let session = login_manager.login(username, password).await?;
123 Ok(Self::from_session_with_config_arc(
124 client_arc,
125 session,
126 retry_config,
127 rate_limit_config,
128 ))
129 }
130
131 fn from_session_with_broadcaster(
132 client: Box<dyn HttpClient + Send + Sync>,
133 session: LastFmEditSession,
134 broadcaster: Arc<SharedEventBroadcaster>,
135 ) -> Self {
136 Self::from_session_with_broadcaster_arc(Arc::from(client), session, broadcaster)
137 }
138
139 fn from_session_with_client_config_arc(
140 client: Arc<dyn HttpClient + Send + Sync>,
141 session: LastFmEditSession,
142 config: ClientConfig,
143 ) -> Self {
144 Self::from_session_with_client_config_and_broadcaster_arc(
145 client,
146 session,
147 config,
148 Arc::new(SharedEventBroadcaster::new()),
149 )
150 }
151
152 fn from_session_with_config_arc(
153 client: Arc<dyn HttpClient + Send + Sync>,
154 session: LastFmEditSession,
155 retry_config: RetryConfig,
156 rate_limit_config: RateLimitConfig,
157 ) -> Self {
158 let config = ClientConfig {
159 retry: retry_config,
160 rate_limit: rate_limit_config,
161 operational_delays: OperationalDelayConfig::default(),
162 };
163 Self::from_session_with_client_config_arc(client, session, config)
164 }
165
166 fn from_session_with_broadcaster_arc(
167 client: Arc<dyn HttpClient + Send + Sync>,
168 session: LastFmEditSession,
169 broadcaster: Arc<SharedEventBroadcaster>,
170 ) -> Self {
171 Self::from_session_with_client_config_and_broadcaster_arc(
172 client,
173 session,
174 ClientConfig::default(),
175 broadcaster,
176 )
177 }
178
179 fn from_session_with_client_config_and_broadcaster_arc(
180 client: Arc<dyn HttpClient + Send + Sync>,
181 session: LastFmEditSession,
182 config: ClientConfig,
183 broadcaster: Arc<SharedEventBroadcaster>,
184 ) -> Self {
185 Self {
186 client,
187 session: Arc::new(Mutex::new(session)),
188 parser: LastFmParser::new(),
189 broadcaster,
190 config,
191 }
192 }
193
194 pub fn get_session(&self) -> LastFmEditSession {
195 self.session.lock().unwrap().clone()
196 }
197
198 pub fn with_shared_broadcaster(&self, client: Box<dyn HttpClient + Send + Sync>) -> Self {
199 let session = self.get_session();
200 Self::from_session_with_broadcaster(client, session, self.broadcaster.clone())
201 }
202
203 pub fn username(&self) -> String {
204 self.session.lock().unwrap().username.clone()
205 }
206
207 pub async fn validate_session(&self) -> bool {
208 let test_url = {
209 let session = self.session.lock().unwrap();
210 format!(
211 "{}/settings/subscription/automatic-edits/tracks",
212 session.base_url
213 )
214 };
215
216 let mut request = Request::new(Method::Get, test_url.parse::<Url>().unwrap());
217
218 {
219 let session = self.session.lock().unwrap();
220 headers::add_cookies(&mut request, &session.cookies);
221 }
222
223 headers::add_get_headers(&mut request, false, None);
224
225 match self.client.send(request).await {
226 Ok(response) => {
227 if response.status() == 302 || response.status() == 301 {
228 if let Some(location) = response.header("location") {
229 if let Some(redirect_url) = location.get(0) {
230 let redirect_url_str = redirect_url.as_str();
231 let is_valid = !redirect_url_str.contains("/login");
232
233 return is_valid;
234 }
235 }
236 }
237 true
238 }
239 Err(_e) => false,
240 }
241 }
242
243 pub async fn delete_scrobble(
244 &self,
245 artist_name: &str,
246 track_name: &str,
247 timestamp: u64,
248 ) -> Result<bool> {
249 let config = RetryConfig {
250 max_retries: 3,
251 base_delay: 5,
252 max_delay: 300,
253 enabled: true,
254 };
255
256 let artist_name = artist_name.to_string();
257 let track_name = track_name.to_string();
258 let client = self.clone();
259
260 match retry::retry_with_backoff(
261 config,
262 "Delete scrobble",
263 || client.delete_scrobble_impl(&artist_name, &track_name, timestamp),
264 |delay, rate_limit_timestamp, operation_name| {
265 self.broadcast_event(ClientEvent::RateLimited {
266 delay_seconds: delay,
267 request: None,
268 rate_limit_type: RateLimitType::ResponsePattern,
269 rate_limit_timestamp,
270 });
271 log::debug!("{operation_name} rate limited, waiting {delay} seconds");
272 },
273 |total_duration, _operation_name| {
274 self.broadcast_event(ClientEvent::RateLimitEnded {
275 request: crate::types::RequestInfo::from_url_and_method(
276 &format!("delete_scrobble/{artist_name}/{track_name}/{timestamp}"),
277 "POST",
278 ),
279 rate_limit_type: RateLimitType::ResponsePattern,
280 total_rate_limit_duration_seconds: total_duration,
281 });
282 },
283 )
284 .await
285 {
286 Ok(retry_result) => Ok(retry_result.result),
287 Err(_) => Ok(false),
288 }
289 }
290
291 async fn delete_scrobble_impl(
292 &self,
293 artist_name: &str,
294 track_name: &str,
295 timestamp: u64,
296 ) -> Result<bool> {
297 let delete_url = {
298 let session = self.session.lock().unwrap();
299 format!(
300 "{}/user/{}/library/delete",
301 session.base_url, session.username
302 )
303 };
304
305 log::debug!("Getting fresh CSRF token for delete");
306 let library_url = {
307 let session = self.session.lock().unwrap();
308 format!("{}/user/{}/library", session.base_url, session.username)
309 };
310
311 let mut response = self.get(&library_url).await?;
312 let content = response
313 .body_string()
314 .await
315 .map_err(|e| LastFmError::Http(e.to_string()))?;
316
317 let document = Html::parse_document(&content);
318 let fresh_csrf_token = self.extract_csrf_token(&document)?;
319
320 log::debug!("Submitting delete request with fresh token");
321
322 let mut request = Request::new(Method::Post, delete_url.parse::<Url>().unwrap());
323
324 let referer_url = {
325 let session = self.session.lock().unwrap();
326 headers::add_cookies(&mut request, &session.cookies);
327 format!("{}/user/{}", session.base_url, session.username)
328 };
329
330 headers::add_edit_headers(&mut request, &referer_url);
331
332 let form_data = [
333 ("csrfmiddlewaretoken", fresh_csrf_token.as_str()),
334 ("artist_name", artist_name),
335 ("track_name", track_name),
336 ("timestamp", ×tamp.to_string()),
337 ("ajax", "1"),
338 ];
339
340 let form_string: String = form_data
341 .iter()
342 .map(|(k, v)| format!("{}={}", urlencoding::encode(k), urlencoding::encode(v)))
343 .collect::<Vec<_>>()
344 .join("&");
345
346 request.set_body(form_string);
347
348 log::debug!(
349 "Deleting scrobble: '{track_name}' by '{artist_name}' with timestamp {timestamp}"
350 );
351
352 let request_info = RequestInfo::from_url_and_method(&delete_url, "POST");
353 let request_start = std::time::Instant::now();
354
355 self.broadcast_event(ClientEvent::RequestStarted {
356 request: request_info.clone(),
357 });
358
359 let mut response = self
360 .client
361 .send(request)
362 .await
363 .map_err(|e| LastFmError::Http(e.to_string()))?;
364
365 self.broadcast_event(ClientEvent::RequestCompleted {
366 request: request_info.clone(),
367 status_code: response.status().into(),
368 duration_ms: request_start.elapsed().as_millis() as u64,
369 });
370
371 log::debug!("Delete response status: {}", response.status());
372
373 let response_text = response
374 .body_string()
375 .await
376 .map_err(|e| LastFmError::Http(e.to_string()))?;
377
378 let success = response.status().is_success();
379
380 if success {
381 log::debug!("Successfully deleted scrobble");
382 } else {
383 log::debug!("Delete failed with response: {response_text}");
384 }
385
386 Ok(success)
387 }
388
389 pub fn subscribe(&self) -> ClientEventReceiver {
390 self.broadcaster.subscribe()
391 }
392
393 pub fn latest_event(&self) -> Option<ClientEvent> {
394 self.broadcaster.latest_event()
395 }
396
397 fn broadcast_event(&self, event: ClientEvent) {
398 self.broadcaster.broadcast_event(event);
399 }
400
401 pub async fn get_recent_scrobbles(&self, page: u32) -> Result<Vec<Track>> {
402 let url = {
403 let session = self.session.lock().unwrap();
404 format!(
405 "{}/user/{}/library?page={}",
406 session.base_url, session.username, page
407 )
408 };
409
410 log::debug!("Fetching recent scrobbles page {page}");
411 let mut response = self.get(&url).await?;
412 let content = response
413 .body_string()
414 .await
415 .map_err(|e| LastFmError::Http(e.to_string()))?;
416
417 log::debug!(
418 "Recent scrobbles response: {} status, {} chars",
419 response.status(),
420 content.len()
421 );
422
423 let document = Html::parse_document(&content);
424 self.parser.parse_recent_scrobbles(&document)
425 }
426
427 pub async fn get_recent_tracks_page(&self, page: u32) -> Result<TrackPage> {
428 let tracks = self.get_recent_scrobbles(page).await?;
429
430 let has_next_page = !tracks.is_empty();
431
432 Ok(TrackPage {
433 tracks,
434 page_number: page,
435 has_next_page,
436 total_pages: None,
437 })
438 }
439
440 pub async fn find_recent_scrobble_for_track(
441 &self,
442 track_name: &str,
443 artist_name: &str,
444 max_pages: u32,
445 ) -> Result<Option<Track>> {
446 log::debug!("Searching for recent scrobble: '{track_name}' by '{artist_name}'");
447
448 for page in 1..=max_pages {
449 let scrobbles = self.get_recent_scrobbles(page).await?;
450
451 for scrobble in scrobbles {
452 if scrobble.name == track_name && scrobble.artist == artist_name {
453 log::debug!(
454 "Found recent scrobble: '{}' with timestamp {:?}",
455 scrobble.name,
456 scrobble.timestamp
457 );
458 return Ok(Some(scrobble));
459 }
460 }
461 }
462
463 log::debug!(
464 "No recent scrobble found for '{track_name}' by '{artist_name}' in {max_pages} pages"
465 );
466 Ok(None)
467 }
468
469 pub async fn edit_scrobble(&self, edit: &ScrobbleEdit) -> Result<EditResponse> {
470 let discovered_edits = self.discover_scrobble_edit_variations(edit).await?;
471
472 if discovered_edits.is_empty() {
473 let context = match (&edit.track_name_original, &edit.album_name_original) {
474 (Some(track_name), _) => {
475 format!("track '{}' by '{}'", track_name, edit.artist_name_original)
476 }
477 (None, Some(album_name)) => {
478 format!("album '{}' by '{}'", album_name, edit.artist_name_original)
479 }
480 (None, None) => format!("artist '{}'", edit.artist_name_original),
481 };
482 return Err(LastFmError::Parse(format!(
483 "No scrobbles found for {context}. Make sure the names are correct and that you have scrobbled recently."
484 )));
485 }
486
487 log::info!(
488 "Discovered {} scrobble instances to edit",
489 discovered_edits.len()
490 );
491
492 let mut all_results = Vec::new();
493
494 for (index, discovered_edit) in discovered_edits.iter().enumerate() {
495 log::debug!(
496 "Processing scrobble {}/{}: '{}' from '{}'",
497 index + 1,
498 discovered_edits.len(),
499 discovered_edit.track_name_original,
500 discovered_edit.album_name_original
501 );
502
503 let mut modified_exact_edit = discovered_edit.clone();
504
505 if let Some(new_track_name) = &edit.track_name {
506 modified_exact_edit.track_name = new_track_name.clone();
507 }
508 if let Some(new_album_name) = &edit.album_name {
509 modified_exact_edit.album_name = new_album_name.clone();
510 }
511 modified_exact_edit.artist_name = edit.artist_name.clone();
512 if let Some(new_album_artist_name) = &edit.album_artist_name {
513 modified_exact_edit.album_artist_name = new_album_artist_name.clone();
514 }
515 modified_exact_edit.edit_all = edit.edit_all;
516
517 let album_info = format!(
518 "{} by {}",
519 modified_exact_edit.album_name_original,
520 modified_exact_edit.album_artist_name_original
521 );
522
523 let single_response = self.edit_scrobble_single(&modified_exact_edit, 3).await?;
524 let success = single_response.success();
525 let message = single_response.message();
526
527 all_results.push(SingleEditResponse {
528 success,
529 message,
530 album_info: Some(album_info),
531 exact_scrobble_edit: modified_exact_edit.clone(),
532 });
533
534 if index < discovered_edits.len() - 1
535 && self.config.operational_delays.edit_delay_ms > 0
536 {
537 tokio::time::sleep(std::time::Duration::from_millis(
538 self.config.operational_delays.edit_delay_ms,
539 ))
540 .await;
541 }
542 }
543
544 Ok(EditResponse::from_results(all_results))
545 }
546
547 pub async fn edit_scrobble_single(
548 &self,
549 exact_edit: &ExactScrobbleEdit,
550 max_retries: u32,
551 ) -> Result<EditResponse> {
552 let config = RetryConfig {
553 max_retries,
554 base_delay: 5,
555 max_delay: 300,
556 enabled: true,
557 };
558
559 let edit_clone = exact_edit.clone();
560 let client = self.clone();
561
562 match retry::retry_with_backoff(
563 config,
564 "Edit scrobble",
565 || client.edit_scrobble_impl(&edit_clone),
566 |delay, rate_limit_timestamp, operation_name| {
567 self.broadcast_event(ClientEvent::RateLimited {
568 delay_seconds: delay,
569 request: None, rate_limit_type: RateLimitType::ResponsePattern,
571 rate_limit_timestamp,
572 });
573 log::debug!("{operation_name} rate limited, waiting {delay} seconds");
574 },
575 |total_duration, _operation_name| {
576 self.broadcast_event(ClientEvent::RateLimitEnded {
577 request: crate::types::RequestInfo::from_url_and_method(
578 &format!(
579 "edit_scrobble/{}/{}",
580 edit_clone.artist_name, edit_clone.track_name
581 ),
582 "POST",
583 ),
584 rate_limit_type: RateLimitType::ResponsePattern,
585 total_rate_limit_duration_seconds: total_duration,
586 });
587 },
588 )
589 .await
590 {
591 Ok(retry_result) => Ok(EditResponse::single(
592 retry_result.result,
593 None,
594 None,
595 exact_edit.clone(),
596 )),
597 Err(LastFmError::RateLimit { .. }) => Ok(EditResponse::single(
598 false,
599 Some(format!("Rate limit exceeded after {max_retries} retries")),
600 None,
601 exact_edit.clone(),
602 )),
603 Err(other_error) => Ok(EditResponse::single(
604 false,
605 Some(other_error.to_string()),
606 None,
607 exact_edit.clone(),
608 )),
609 }
610 }
611
612 async fn edit_scrobble_impl(&self, exact_edit: &ExactScrobbleEdit) -> Result<bool> {
613 let start_time = std::time::Instant::now();
614 let result = self.edit_scrobble_impl_internal(exact_edit).await;
615 let duration_ms = start_time.elapsed().as_millis() as u64;
616
617 match &result {
618 Ok(success) => {
619 self.broadcast_event(ClientEvent::EditAttempted {
620 edit: exact_edit.clone(),
621 success: *success,
622 error_message: None,
623 duration_ms,
624 });
625 }
626 Err(error) => {
627 self.broadcast_event(ClientEvent::EditAttempted {
628 edit: exact_edit.clone(),
629 success: false,
630 error_message: Some(error.to_string()),
631 duration_ms,
632 });
633 }
634 }
635
636 result
637 }
638
639 async fn edit_scrobble_impl_internal(&self, exact_edit: &ExactScrobbleEdit) -> Result<bool> {
640 let edit_url = {
641 let session = self.session.lock().unwrap();
642 format!(
643 "{}/user/{}/library/edit?edited-variation=library-track-scrobble",
644 session.base_url, session.username
645 )
646 };
647
648 log::debug!("Getting fresh CSRF token for edit");
649 let form_html = self.get_edit_form_html(&edit_url).await?;
650
651 let form_document = Html::parse_document(&form_html);
652 let fresh_csrf_token = self.extract_csrf_token(&form_document)?;
653
654 log::debug!("Submitting edit with fresh token");
655
656 let form_data = exact_edit.build_form_data(&fresh_csrf_token);
657
658 log::debug!(
659 "Editing scrobble: '{}' -> '{}'",
660 exact_edit.track_name_original,
661 exact_edit.track_name
662 );
663 {
664 let session = self.session.lock().unwrap();
665 log::trace!("Session cookies count: {}", session.cookies.len());
666 }
667
668 let mut request = Request::new(Method::Post, edit_url.parse::<Url>().unwrap());
669
670 let referer_url = {
671 let session = self.session.lock().unwrap();
672 headers::add_cookies(&mut request, &session.cookies);
673 format!("{}/user/{}/library", session.base_url, session.username)
674 };
675
676 headers::add_edit_headers(&mut request, &referer_url);
677
678 let form_string: String = form_data
679 .iter()
680 .map(|(k, v)| format!("{}={}", urlencoding::encode(k), urlencoding::encode(v)))
681 .collect::<Vec<_>>()
682 .join("&");
683
684 request.set_body(form_string);
685
686 let request_info = RequestInfo::from_url_and_method(&edit_url, "POST");
687 let request_start = std::time::Instant::now();
688
689 self.broadcast_event(ClientEvent::RequestStarted {
690 request: request_info.clone(),
691 });
692
693 let mut response = self
694 .client
695 .send(request)
696 .await
697 .map_err(|e| LastFmError::Http(e.to_string()))?;
698
699 self.broadcast_event(ClientEvent::RequestCompleted {
700 request: request_info.clone(),
701 status_code: response.status().into(),
702 duration_ms: request_start.elapsed().as_millis() as u64,
703 });
704
705 log::debug!("Edit response status: {}", response.status());
706
707 let response_text = response
708 .body_string()
709 .await
710 .map_err(|e| LastFmError::Http(e.to_string()))?;
711
712 let analysis = edit_analysis::analyze_edit_response(&response_text, response.status());
713
714 Ok(analysis.success)
715 }
716
717 async fn get_edit_form_html(&self, edit_url: &str) -> Result<String> {
718 let mut form_response = self.get(edit_url).await?;
719 let form_html = form_response
720 .body_string()
721 .await
722 .map_err(|e| LastFmError::Http(e.to_string()))?;
723
724 log::debug!("Edit form response status: {}", form_response.status());
725 Ok(form_html)
726 }
727
728 pub async fn load_edit_form_values_internal(
729 &self,
730 track_name: &str,
731 artist_name: &str,
732 ) -> Result<Vec<ExactScrobbleEdit>> {
733 log::debug!("Loading edit form values for '{track_name}' by '{artist_name}'");
734
735 let base_track_url = {
736 let session = self.session.lock().unwrap();
737 format!(
738 "{}/user/{}/library/music/+noredirect/{}/_/{}",
739 session.base_url,
740 session.username,
741 urlencoding::encode(artist_name),
742 urlencoding::encode(track_name)
743 )
744 };
745
746 log::debug!("Fetching track page: {base_track_url}");
747
748 let mut response = self.get(&base_track_url).await?;
749 let html = response
750 .body_string()
751 .await
752 .map_err(|e| crate::LastFmError::Http(e.to_string()))?;
753
754 let document = Html::parse_document(&html);
755
756 let mut all_scrobble_edits = Vec::new();
757 let mut unique_albums = std::collections::HashSet::new();
758 let max_pages = 5;
759
760 let page_edits = self.extract_scrobble_edits_from_page(
761 &document,
762 track_name,
763 artist_name,
764 &mut unique_albums,
765 )?;
766 all_scrobble_edits.extend(page_edits);
767
768 log::debug!(
769 "Page 1: found {} unique album variations",
770 all_scrobble_edits.len()
771 );
772
773 let pagination_selector = Selector::parse(".pagination .pagination-next").unwrap();
774 let mut has_next_page = document.select(&pagination_selector).next().is_some();
775 let mut page = 2;
776
777 while has_next_page && page <= max_pages {
778 let page_url = {
779 let session = self.session.lock().unwrap();
780 format!(
781 "{}/user/{}/library/music/{}/_/{}?page={page}",
782 session.base_url,
783 session.username,
784 urlencoding::encode(artist_name),
785 urlencoding::encode(track_name)
786 )
787 };
788
789 log::debug!("Fetching page {page} for additional album variations");
790
791 let mut response = self.get(&page_url).await?;
792 let html = response
793 .body_string()
794 .await
795 .map_err(|e| crate::LastFmError::Http(e.to_string()))?;
796
797 let document = Html::parse_document(&html);
798
799 let page_edits = self.extract_scrobble_edits_from_page(
800 &document,
801 track_name,
802 artist_name,
803 &mut unique_albums,
804 )?;
805
806 let initial_count = all_scrobble_edits.len();
807 all_scrobble_edits.extend(page_edits);
808 let found_new_unique_albums = all_scrobble_edits.len() > initial_count;
809
810 has_next_page = document.select(&pagination_selector).next().is_some();
811
812 log::debug!(
813 "Page {page}: found {} total unique albums ({})",
814 all_scrobble_edits.len(),
815 if found_new_unique_albums {
816 "new albums found"
817 } else {
818 "no new unique albums"
819 }
820 );
821
822 page += 1;
823 }
824
825 if all_scrobble_edits.is_empty() {
826 return Err(crate::LastFmError::Parse(format!(
827 "No scrobble forms found for track '{track_name}' by '{artist_name}'"
828 )));
829 }
830
831 log::debug!(
832 "Final result: found {} unique album variations for '{track_name}' by '{artist_name}'",
833 all_scrobble_edits.len(),
834 );
835
836 Ok(all_scrobble_edits)
837 }
838
839 fn extract_scrobble_edits_from_page(
840 &self,
841 document: &Html,
842 expected_track: &str,
843 expected_artist: &str,
844 unique_albums: &mut std::collections::HashSet<(String, String)>,
845 ) -> Result<Vec<ExactScrobbleEdit>> {
846 let mut scrobble_edits = Vec::new();
847 let table_selector =
848 Selector::parse("table.chartlist:not(.chartlist__placeholder)").unwrap();
849 let table = document.select(&table_selector).next().ok_or_else(|| {
850 crate::LastFmError::Parse("No chartlist table found on track page".to_string())
851 })?;
852
853 let row_selector = Selector::parse("tr").unwrap();
854 for row in table.select(&row_selector) {
855 let count_bar_link_selector = Selector::parse(".chartlist-count-bar-link").unwrap();
856 if row.select(&count_bar_link_selector).next().is_some() {
857 log::debug!("Found count bar link, skipping aggregated row");
858 continue;
859 }
860
861 let form_selector = Selector::parse("form[data-edit-scrobble]").unwrap();
862 if let Some(form) = row.select(&form_selector).next() {
863 let extract_form_value = |name: &str| -> Option<String> {
864 let selector = Selector::parse(&format!("input[name='{name}']")).unwrap();
865 form.select(&selector)
866 .next()
867 .and_then(|input| input.value().attr("value"))
868 .map(|s| s.to_string())
869 };
870
871 let form_track = extract_form_value("track_name").unwrap_or_default();
872 let form_artist = extract_form_value("artist_name").unwrap_or_default();
873 let form_album = extract_form_value("album_name").unwrap_or_default();
874 let form_album_artist =
875 extract_form_value("album_artist_name").unwrap_or_else(|| form_artist.clone());
876 let form_timestamp = extract_form_value("timestamp").unwrap_or_default();
877
878 if form_track == expected_track && form_artist == expected_artist {
879 let album_key = (form_album.clone(), form_album_artist.clone());
880 if unique_albums.insert(album_key) {
881 let timestamp = if form_timestamp.is_empty() {
882 None
883 } else {
884 form_timestamp.parse::<u64>().ok()
885 };
886
887 if let Some(timestamp) = timestamp {
888 let scrobble_edit = ExactScrobbleEdit::new(
889 form_track.clone(),
890 form_album.clone(),
891 form_artist.clone(),
892 form_album_artist.clone(),
893 form_track,
894 form_album,
895 form_artist,
896 form_album_artist,
897 timestamp,
898 true,
899 );
900 scrobble_edits.push(scrobble_edit);
901 } else {
902 log::warn!(
903 "â ī¸ Skipping form without valid timestamp: '{form_album}' by '{form_album_artist}'"
904 );
905 }
906 }
907 }
908 }
909 }
910
911 Ok(scrobble_edits)
912 }
913
914 pub async fn get_artist_tracks_page(&self, artist: &str, page: u32) -> Result<TrackPage> {
915 let url = {
916 let session = self.session.lock().unwrap();
917 format!(
918 "{}/user/{}/library/music/{}/+tracks?page={}&ajax=true",
919 session.base_url,
920 session.username,
921 urlencoding::encode(artist),
922 page
923 )
924 };
925
926 log::debug!("Fetching tracks page {page} for artist: {artist}");
927 let mut response = self.get(&url).await?;
928 let content = response
929 .body_string()
930 .await
931 .map_err(|e| LastFmError::Http(e.to_string()))?;
932
933 log::debug!(
934 "AJAX response: {} status, {} chars",
935 response.status(),
936 content.len()
937 );
938
939 log::debug!("Parsing HTML response from AJAX endpoint");
940 let document = Html::parse_document(&content);
941 self.parser.parse_tracks_page(&document, page, artist, None)
942 }
943
944 pub fn extract_tracks_from_document(
945 &self,
946 document: &Html,
947 artist: &str,
948 album: Option<&str>,
949 ) -> Result<Vec<Track>> {
950 self.parser
951 .extract_tracks_from_document(document, artist, album)
952 }
953
954 pub fn parse_tracks_page(
955 &self,
956 document: &Html,
957 page_number: u32,
958 artist: &str,
959 album: Option<&str>,
960 ) -> Result<TrackPage> {
961 self.parser
962 .parse_tracks_page(document, page_number, artist, album)
963 }
964
965 fn extract_csrf_token(&self, document: &Html) -> Result<String> {
966 let csrf_selector = Selector::parse("input[name=\"csrfmiddlewaretoken\"]").unwrap();
967
968 document
969 .select(&csrf_selector)
970 .next()
971 .and_then(|input| input.value().attr("value"))
972 .map(|token| token.to_string())
973 .ok_or(LastFmError::CsrfNotFound)
974 }
975
976 pub async fn get(&self, url: &str) -> Result<Response> {
977 self.get_with_retry(url).await
978 }
979
980 async fn get_with_retry(&self, url: &str) -> Result<Response> {
981 let config = self.config.retry.clone();
982
983 let url_string = url.to_string();
984 let client = self.clone();
985
986 let retry_result = retry::retry_with_backoff(
987 config,
988 &format!("GET {url}"),
989 || async {
990 let mut response = client.get_with_redirects(&url_string, 0).await?;
991
992 let body = client
993 .extract_response_body(&url_string, &mut response)
994 .await?;
995
996 if response.status().is_success() && client.is_rate_limit_response(&body) {
997 log::debug!("Response body contains rate limit patterns");
998 return Err(LastFmError::RateLimit { retry_after: 60 });
999 }
1000
1001 let mut new_response = http_types::Response::new(response.status());
1002 for (name, values) in response.iter() {
1003 for value in values {
1004 let _ = new_response.insert_header(name.clone(), value.clone());
1005 }
1006 }
1007 new_response.set_body(body);
1008
1009 Ok(new_response)
1010 },
1011 |delay, rate_limit_timestamp, operation_name| {
1012 self.broadcast_event(ClientEvent::RateLimited {
1013 delay_seconds: delay,
1014 request: None, rate_limit_type: RateLimitType::ResponsePattern,
1016 rate_limit_timestamp,
1017 });
1018 log::debug!("{operation_name} rate limited, waiting {delay} seconds");
1019 },
1020 |total_duration, _operation_name| {
1021 self.broadcast_event(ClientEvent::RateLimitEnded {
1022 request: crate::types::RequestInfo::from_url_and_method(&url_string, "GET"),
1023 rate_limit_type: RateLimitType::ResponsePattern,
1024 total_rate_limit_duration_seconds: total_duration,
1025 });
1026 },
1027 )
1028 .await?;
1029
1030 Ok(retry_result.result)
1031 }
1032
1033 async fn get_with_redirects(&self, url: &str, redirect_count: u32) -> Result<Response> {
1034 if redirect_count > 5 {
1035 return Err(LastFmError::Http("Too many redirects".to_string()));
1036 }
1037
1038 let mut request = Request::new(Method::Get, url.parse::<Url>().unwrap());
1039
1040 {
1041 let session = self.session.lock().unwrap();
1042 headers::add_cookies(&mut request, &session.cookies);
1043 if session.cookies.is_empty() && url.contains("page=") {
1044 log::debug!("No cookies available for paginated request!");
1045 }
1046 }
1047
1048 let is_ajax = url.contains("ajax=true");
1049 let referer_url = if url.contains("page=") {
1050 Some(url.split('?').next().unwrap_or(url))
1051 } else {
1052 None
1053 };
1054
1055 headers::add_get_headers(&mut request, is_ajax, referer_url);
1056
1057 let request_info = RequestInfo::from_url_and_method(url, "GET");
1058 let request_start = std::time::Instant::now();
1059
1060 self.broadcast_event(ClientEvent::RequestStarted {
1061 request: request_info.clone(),
1062 });
1063
1064 let response = self
1065 .client
1066 .send(request)
1067 .await
1068 .map_err(|e| LastFmError::Http(e.to_string()))?;
1069
1070 self.broadcast_event(ClientEvent::RequestCompleted {
1071 request: request_info.clone(),
1072 status_code: response.status().into(),
1073 duration_ms: request_start.elapsed().as_millis() as u64,
1074 });
1075
1076 self.extract_cookies(&response);
1077
1078 if response.status() == 302 || response.status() == 301 {
1079 if let Some(location) = response.header("location") {
1080 if let Some(redirect_url) = location.get(0) {
1081 let redirect_url_str = redirect_url.as_str();
1082 if url.contains("page=") {
1083 log::debug!("Following redirect from {url} to {redirect_url_str}");
1084
1085 if redirect_url_str.contains("/login") {
1086 log::debug!("Redirect to login page - authentication failed for paginated request");
1087 return Err(LastFmError::Auth(
1088 "Session expired or invalid for paginated request".to_string(),
1089 ));
1090 }
1091 }
1092
1093 let full_redirect_url = if redirect_url_str.starts_with('/') {
1094 let base_url = self.session.lock().unwrap().base_url.clone();
1095 format!("{base_url}{redirect_url_str}")
1096 } else if redirect_url_str.starts_with("http") {
1097 redirect_url_str.to_string()
1098 } else {
1099 let base_url = url
1100 .rsplit('/')
1101 .skip(1)
1102 .collect::<Vec<_>>()
1103 .into_iter()
1104 .rev()
1105 .collect::<Vec<_>>()
1106 .join("/");
1107 format!("{base_url}/{redirect_url_str}")
1108 };
1109
1110 return Box::pin(
1111 self.get_with_redirects(&full_redirect_url, redirect_count + 1),
1112 )
1113 .await;
1114 }
1115 }
1116 }
1117
1118 if self.config.rate_limit.detect_by_status && response.status() == 429 {
1119 let retry_after = response
1120 .header("retry-after")
1121 .and_then(|h| h.get(0))
1122 .and_then(|v| v.as_str().parse::<u64>().ok())
1123 .unwrap_or(60);
1124 self.broadcast_event(ClientEvent::RateLimited {
1125 delay_seconds: retry_after,
1126 request: Some(request_info.clone()),
1127 rate_limit_type: RateLimitType::Http429,
1128 rate_limit_timestamp: std::time::SystemTime::now()
1129 .duration_since(std::time::UNIX_EPOCH)
1130 .unwrap_or_default()
1131 .as_secs(),
1132 });
1133 return Err(LastFmError::RateLimit { retry_after });
1134 }
1135
1136 if self.config.rate_limit.detect_by_status && response.status() == 403 {
1137 log::debug!("Got 403 response, checking if it's a rate limit");
1138 {
1139 let session = self.session.lock().unwrap();
1140 if !session.cookies.is_empty() {
1141 log::debug!("403 on authenticated request - likely rate limit");
1142 self.broadcast_event(ClientEvent::RateLimited {
1143 delay_seconds: 60,
1144 request: Some(request_info.clone()),
1145 rate_limit_type: RateLimitType::Http403,
1146 rate_limit_timestamp: std::time::SystemTime::now()
1147 .duration_since(std::time::UNIX_EPOCH)
1148 .unwrap_or_default()
1149 .as_secs(),
1150 });
1151 return Err(LastFmError::RateLimit { retry_after: 60 });
1152 }
1153 }
1154 }
1155
1156 Ok(response)
1157 }
1158
1159 fn is_rate_limit_response(&self, response_body: &str) -> bool {
1160 let rate_limit_config = &self.config.rate_limit;
1161
1162 if !rate_limit_config.detect_by_patterns && rate_limit_config.custom_patterns.is_empty() {
1163 return false;
1164 }
1165
1166 let body_lower = response_body.to_lowercase();
1167
1168 for pattern in &rate_limit_config.custom_patterns {
1169 if body_lower.contains(&pattern.to_lowercase()) {
1170 log::debug!("Rate limit detected (custom pattern: '{pattern}')");
1171 return true;
1172 }
1173 }
1174
1175 if rate_limit_config.detect_by_patterns {
1176 for pattern in &rate_limit_config.patterns {
1177 let pattern_lower = pattern.to_lowercase();
1178 if body_lower.contains(&pattern_lower) {
1179 log::debug!("Rate limit detected (pattern: '{pattern}')");
1180 return true;
1181 }
1182 }
1183 }
1184
1185 false
1186 }
1187
1188 fn extract_cookies(&self, response: &Response) {
1189 let mut session = self.session.lock().unwrap();
1190 extract_cookies_from_response(response, &mut session.cookies);
1191 }
1192
1193 async fn extract_response_body(&self, _url: &str, response: &mut Response) -> Result<String> {
1194 let body = response
1195 .body_string()
1196 .await
1197 .map_err(|e| LastFmError::Http(e.to_string()))?;
1198
1199 Ok(body)
1200 }
1201
1202 pub async fn get_artists_page(&self, page: u32) -> Result<crate::ArtistPage> {
1203 let url = {
1204 let session = self.session.lock().unwrap();
1205 format!(
1206 "{}/user/{}/library/artists?page={}",
1207 session.base_url, session.username, page
1208 )
1209 };
1210
1211 log::debug!("Fetching artists page {page}");
1212 let mut response = self.get(&url).await?;
1213 let content = response
1214 .body_string()
1215 .await
1216 .map_err(|e| LastFmError::Http(e.to_string()))?;
1217
1218 log::debug!(
1219 "Artist library response: {} status, {} chars",
1220 response.status(),
1221 content.len()
1222 );
1223
1224 log::debug!("Parsing HTML response from artist library endpoint");
1225 let document = Html::parse_document(&content);
1226 self.parser.parse_artists_page(&document, page)
1227 }
1228
1229 pub async fn get_artist_albums_page(&self, artist: &str, page: u32) -> Result<AlbumPage> {
1230 let url = {
1231 let session = self.session.lock().unwrap();
1232 format!(
1233 "{}/user/{}/library/music/{}/+albums?page={}&ajax=true",
1234 session.base_url,
1235 session.username,
1236 urlencoding::encode(artist),
1237 page
1238 )
1239 };
1240
1241 log::debug!("Fetching albums page {page} for artist: {artist}");
1242 let mut response = self.get(&url).await?;
1243 let content = response
1244 .body_string()
1245 .await
1246 .map_err(|e| LastFmError::Http(e.to_string()))?;
1247
1248 log::debug!(
1249 "AJAX response: {} status, {} chars",
1250 response.status(),
1251 content.len()
1252 );
1253
1254 log::debug!("Parsing HTML response from AJAX endpoint");
1255 let document = Html::parse_document(&content);
1256 self.parser.parse_albums_page(&document, page, artist)
1257 }
1258
1259 pub async fn get_album_tracks_page(
1260 &self,
1261 album_name: &str,
1262 artist_name: &str,
1263 page: u32,
1264 ) -> Result<TrackPage> {
1265 let url = {
1266 let session = self.session.lock().unwrap();
1267 format!(
1268 "{}/user/{}/library/music/{}/{}?page={}&ajax=true",
1269 session.base_url,
1270 session.username,
1271 self.lastfm_encode(artist_name),
1272 self.lastfm_encode(album_name),
1273 page
1274 )
1275 };
1276
1277 log::debug!("Fetching tracks page {page} for album '{album_name}' by '{artist_name}'");
1278 log::debug!("đ Album URL: {url}");
1279
1280 let mut response = self.get(&url).await?;
1281 let content = response
1282 .body_string()
1283 .await
1284 .map_err(|e| LastFmError::Http(e.to_string()))?;
1285
1286 log::debug!(
1287 "AJAX response: {} status, {} chars",
1288 response.status(),
1289 content.len()
1290 );
1291
1292 log::debug!("Parsing HTML response from AJAX endpoint");
1293 let document = Html::parse_document(&content);
1294 let result =
1295 self.parser
1296 .parse_tracks_page(&document, page, artist_name, Some(album_name))?;
1297
1298 if result.tracks.is_empty() {
1300 if content.contains("404") || content.contains("Not Found") {
1301 log::warn!("đ¨ 404 ERROR for album '{album_name}' by '{artist_name}': {url}");
1302 } else if content.contains("no tracks") || content.contains("no music") {
1303 log::debug!("âšī¸ Album '{album_name}' by '{artist_name}' explicitly has no tracks in user's library");
1304 } else {
1305 log::warn!(
1306 "đ¨ UNKNOWN EMPTY RESPONSE for album '{album_name}' by '{artist_name}': {url}"
1307 );
1308 log::debug!("đ Response length: {} chars", content.len());
1309 log::debug!(
1310 "đ Response preview (first 200 chars): {}",
1311 &content.chars().take(200).collect::<String>()
1312 );
1313 }
1314 } else {
1315 log::debug!(
1316 "â
SUCCESS: Album '{album_name}' by '{artist_name}' returned {} tracks",
1317 result.tracks.len()
1318 );
1319 }
1320
1321 Ok(result)
1322 }
1323
1324 pub async fn search_tracks_page(&self, query: &str, page: u32) -> Result<TrackPage> {
1325 let url = {
1326 let session = self.session.lock().unwrap();
1327 format!(
1328 "{}/user/{}/library/tracks/search?page={}&query={}&ajax=1",
1329 session.base_url,
1330 session.username,
1331 page,
1332 urlencoding::encode(query)
1333 )
1334 };
1335
1336 log::debug!("Searching tracks for query '{query}' on page {page}");
1337 let mut response = self.get(&url).await?;
1338 let content = response
1339 .body_string()
1340 .await
1341 .map_err(|e| LastFmError::Http(e.to_string()))?;
1342
1343 log::debug!(
1344 "Track search response: {} status, {} chars",
1345 response.status(),
1346 content.len()
1347 );
1348
1349 let document = Html::parse_document(&content);
1350 let tracks = self.parser.parse_track_search_results(&document)?;
1351
1352 let (has_next_page, total_pages) = self.parser.parse_pagination(&document, page)?;
1355
1356 Ok(TrackPage {
1357 tracks,
1358 page_number: page,
1359 has_next_page,
1360 total_pages,
1361 })
1362 }
1363
1364 pub async fn search_albums_page(&self, query: &str, page: u32) -> Result<AlbumPage> {
1365 let url = {
1366 let session = self.session.lock().unwrap();
1367 format!(
1368 "{}/user/{}/library/albums/search?page={}&query={}&ajax=1",
1369 session.base_url,
1370 session.username,
1371 page,
1372 urlencoding::encode(query)
1373 )
1374 };
1375
1376 log::debug!("Searching albums for query '{query}' on page {page}");
1377 let mut response = self.get(&url).await?;
1378 let content = response
1379 .body_string()
1380 .await
1381 .map_err(|e| LastFmError::Http(e.to_string()))?;
1382
1383 log::debug!(
1384 "Album search response: {} status, {} chars",
1385 response.status(),
1386 content.len()
1387 );
1388
1389 let document = Html::parse_document(&content);
1390 let albums = self.parser.parse_album_search_results(&document)?;
1391
1392 let (has_next_page, total_pages) = self.parser.parse_pagination(&document, page)?;
1394
1395 Ok(AlbumPage {
1396 albums,
1397 page_number: page,
1398 has_next_page,
1399 total_pages,
1400 })
1401 }
1402
1403 pub fn inner_client(&self) -> Arc<dyn HttpClient + Send + Sync> {
1405 self.client.clone()
1406 }
1407}
1408
1409#[async_trait(?Send)]
1410impl LastFmEditClient for LastFmEditClientImpl {
1411 fn username(&self) -> String {
1412 self.username()
1413 }
1414
1415 async fn find_recent_scrobble_for_track(
1416 &self,
1417 track_name: &str,
1418 artist_name: &str,
1419 max_pages: u32,
1420 ) -> Result<Option<Track>> {
1421 self.find_recent_scrobble_for_track(track_name, artist_name, max_pages)
1422 .await
1423 }
1424
1425 async fn edit_scrobble(&self, edit: &ScrobbleEdit) -> Result<EditResponse> {
1426 self.edit_scrobble(edit).await
1427 }
1428
1429 async fn edit_scrobble_single(
1430 &self,
1431 exact_edit: &ExactScrobbleEdit,
1432 max_retries: u32,
1433 ) -> Result<EditResponse> {
1434 self.edit_scrobble_single(exact_edit, max_retries).await
1435 }
1436
1437 fn get_session(&self) -> LastFmEditSession {
1438 self.get_session()
1439 }
1440
1441 fn subscribe(&self) -> ClientEventReceiver {
1442 self.subscribe()
1443 }
1444
1445 fn latest_event(&self) -> Option<ClientEvent> {
1446 self.latest_event()
1447 }
1448
1449 fn discover_scrobbles(
1450 &self,
1451 edit: ScrobbleEdit,
1452 ) -> Box<dyn crate::AsyncDiscoveryIterator<crate::ExactScrobbleEdit>> {
1453 let track_name = edit.track_name_original.clone();
1454 let album_name = edit.album_name_original.clone();
1455
1456 match (&track_name, &album_name) {
1457 (Some(track_name), Some(album_name)) => Box::new(crate::ExactMatchDiscovery::new(
1458 self.clone(),
1459 edit,
1460 track_name.clone(),
1461 album_name.clone(),
1462 )),
1463
1464 (Some(track_name), None) => Box::new(crate::TrackVariationsDiscovery::new(
1465 self.clone(),
1466 edit,
1467 track_name.clone(),
1468 )),
1469
1470 (None, Some(album_name)) => Box::new(crate::AlbumTracksDiscovery::new(
1471 self.clone(),
1472 edit,
1473 album_name.clone(),
1474 )),
1475
1476 (None, None) => Box::new(crate::ArtistTracksDiscovery::new(self.clone(), edit)),
1477 }
1478 }
1479
1480 async fn get_artists_page(&self, page: u32) -> Result<crate::ArtistPage> {
1481 self.get_artists_page(page).await
1482 }
1483
1484 async fn get_artist_tracks_page(&self, artist: &str, page: u32) -> Result<TrackPage> {
1485 self.get_artist_tracks_page(artist, page).await
1486 }
1487
1488 async fn get_artist_albums_page(&self, artist: &str, page: u32) -> Result<AlbumPage> {
1489 self.get_artist_albums_page(artist, page).await
1490 }
1491
1492 async fn get_album_tracks_page(
1493 &self,
1494 album_name: &str,
1495 artist_name: &str,
1496 page: u32,
1497 ) -> Result<TrackPage> {
1498 self.get_album_tracks_page(album_name, artist_name, page)
1499 .await
1500 }
1501
1502 async fn get_recent_tracks_page(&self, page: u32) -> Result<TrackPage> {
1503 self.get_recent_tracks_page(page).await
1504 }
1505
1506 fn artists(&self) -> Box<dyn crate::AsyncPaginatedIterator<crate::Artist>> {
1507 Box::new(crate::iterator::ArtistsIterator::new(self.clone()))
1508 }
1509
1510 fn artist_tracks(&self, artist: &str) -> Box<dyn crate::AsyncPaginatedIterator<Track>> {
1511 Box::new(crate::ArtistTracksIterator::new(
1512 self.clone(),
1513 artist.to_string(),
1514 ))
1515 }
1516
1517 fn artist_tracks_direct(&self, artist: &str) -> Box<dyn crate::AsyncPaginatedIterator<Track>> {
1518 Box::new(crate::iterator::ArtistTracksDirectIterator::new(
1519 self.clone(),
1520 artist.to_string(),
1521 ))
1522 }
1523
1524 fn artist_albums(&self, artist: &str) -> Box<dyn crate::AsyncPaginatedIterator<crate::Album>> {
1525 Box::new(crate::ArtistAlbumsIterator::new(
1526 self.clone(),
1527 artist.to_string(),
1528 ))
1529 }
1530
1531 fn album_tracks(
1532 &self,
1533 album_name: &str,
1534 artist_name: &str,
1535 ) -> Box<dyn crate::AsyncPaginatedIterator<Track>> {
1536 Box::new(crate::AlbumTracksIterator::new(
1537 self.clone(),
1538 album_name.to_string(),
1539 artist_name.to_string(),
1540 ))
1541 }
1542
1543 fn recent_tracks(&self) -> Box<dyn crate::AsyncPaginatedIterator<Track>> {
1544 Box::new(crate::RecentTracksIterator::new(self.clone()))
1545 }
1546
1547 fn recent_tracks_from_page(
1548 &self,
1549 starting_page: u32,
1550 ) -> Box<dyn crate::AsyncPaginatedIterator<Track>> {
1551 Box::new(crate::RecentTracksIterator::with_starting_page(
1552 self.clone(),
1553 starting_page,
1554 ))
1555 }
1556
1557 fn search_tracks(&self, query: &str) -> Box<dyn crate::AsyncPaginatedIterator<Track>> {
1558 Box::new(crate::SearchTracksIterator::new(
1559 self.clone(),
1560 query.to_string(),
1561 ))
1562 }
1563
1564 fn search_albums(&self, query: &str) -> Box<dyn crate::AsyncPaginatedIterator<crate::Album>> {
1565 Box::new(crate::SearchAlbumsIterator::new(
1566 self.clone(),
1567 query.to_string(),
1568 ))
1569 }
1570
1571 async fn search_tracks_page(&self, query: &str, page: u32) -> Result<crate::TrackPage> {
1572 self.search_tracks_page(query, page).await
1573 }
1574
1575 async fn search_albums_page(&self, query: &str, page: u32) -> Result<crate::AlbumPage> {
1576 self.search_albums_page(query, page).await
1577 }
1578
1579 async fn validate_session(&self) -> bool {
1580 self.validate_session().await
1581 }
1582
1583 async fn delete_scrobble(
1584 &self,
1585 artist_name: &str,
1586 track_name: &str,
1587 timestamp: u64,
1588 ) -> Result<bool> {
1589 self.delete_scrobble(artist_name, track_name, timestamp)
1590 .await
1591 }
1592}