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