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