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