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