1use crate::edit::{ExactScrobbleEdit, SingleEditResponse};
2use crate::edit_analysis;
3use crate::events::{
4 ClientEvent, ClientEventReceiver, RateLimitType, RequestInfo, SharedEventBroadcaster,
5};
6use crate::headers;
7use crate::login::extract_cookies_from_response;
8use crate::parsing::LastFmParser;
9use crate::r#trait::LastFmEditClient;
10use crate::retry::{self, RetryConfig};
11use crate::session::LastFmEditSession;
12use crate::{AlbumPage, EditResponse, LastFmError, Result, ScrobbleEdit, Track, TrackPage};
13use async_trait::async_trait;
14use http_client::{HttpClient, Request, Response};
15use http_types::{Method, Url};
16use scraper::{Html, Selector};
17use std::sync::{Arc, Mutex};
18
19#[derive(Clone)]
25pub struct LastFmEditClientImpl {
26 client: Arc<dyn HttpClient + Send + Sync>,
27 session: Arc<Mutex<LastFmEditSession>>,
28 rate_limit_patterns: Vec<String>,
29 parser: LastFmParser,
30 broadcaster: Arc<SharedEventBroadcaster>,
31}
32
33impl LastFmEditClientImpl {
34 pub fn from_session(
45 client: Box<dyn HttpClient + Send + Sync>,
46 session: LastFmEditSession,
47 ) -> Self {
48 Self::from_session_with_arc(Arc::from(client), session)
49 }
50
51 fn from_session_with_arc(
55 client: Arc<dyn HttpClient + Send + Sync>,
56 session: LastFmEditSession,
57 ) -> Self {
58 Self::from_session_with_broadcaster_arc(
59 client,
60 session,
61 Arc::new(SharedEventBroadcaster::new()),
62 )
63 }
64
65 pub fn from_session_with_rate_limit_patterns(
75 client: Box<dyn HttpClient + Send + Sync>,
76 session: LastFmEditSession,
77 rate_limit_patterns: Vec<String>,
78 ) -> Self {
79 Self {
80 client: Arc::from(client),
81 session: Arc::new(Mutex::new(session)),
82 rate_limit_patterns,
83 parser: LastFmParser::new(),
84 broadcaster: Arc::new(SharedEventBroadcaster::new()),
85 }
86 }
87
88 pub async fn login_with_credentials(
103 client: Box<dyn HttpClient + Send + Sync>,
104 username: &str,
105 password: &str,
106 ) -> Result<Self> {
107 let client_arc: Arc<dyn HttpClient + Send + Sync> = Arc::from(client);
108 let login_manager =
109 crate::login::LoginManager::new(client_arc.clone(), "https://www.last.fm".to_string());
110 let session = login_manager.login(username, password).await?;
111 Ok(Self::from_session_with_arc(client_arc, session))
112 }
113
114 fn from_session_with_broadcaster(
129 client: Box<dyn HttpClient + Send + Sync>,
130 session: LastFmEditSession,
131 broadcaster: Arc<SharedEventBroadcaster>,
132 ) -> Self {
133 Self::from_session_with_broadcaster_arc(Arc::from(client), session, broadcaster)
134 }
135
136 fn from_session_with_broadcaster_arc(
138 client: Arc<dyn HttpClient + Send + Sync>,
139 session: LastFmEditSession,
140 broadcaster: Arc<SharedEventBroadcaster>,
141 ) -> Self {
142 Self {
143 client,
144 session: Arc::new(Mutex::new(session)),
145 rate_limit_patterns: vec![
146 "you've tried to log in too many times".to_string(),
147 "you're requesting too many pages".to_string(),
148 "slow down".to_string(),
149 "too fast".to_string(),
150 "rate limit".to_string(),
151 "throttled".to_string(),
152 "temporarily blocked".to_string(),
153 "temporarily restricted".to_string(),
154 "captcha".to_string(),
155 "verify you're human".to_string(),
156 "prove you're not a robot".to_string(),
157 "security check".to_string(),
158 "service temporarily unavailable".to_string(),
159 "quota exceeded".to_string(),
160 "limit exceeded".to_string(),
161 "daily limit".to_string(),
162 ],
163 parser: LastFmParser::new(),
164 broadcaster,
165 }
166 }
167
168 pub fn get_session(&self) -> LastFmEditSession {
170 self.session.lock().unwrap().clone()
171 }
172
173 pub fn restore_session(&self, session: LastFmEditSession) {
175 *self.session.lock().unwrap() = session;
176 }
177
178 pub fn with_shared_broadcaster(&self, client: Box<dyn HttpClient + Send + Sync>) -> Self {
192 let session = self.get_session();
193 Self::from_session_with_broadcaster(client, session, self.broadcaster.clone())
194 }
195
196 pub fn username(&self) -> String {
200 self.session.lock().unwrap().username.clone()
201 }
202
203 pub async fn validate_session(&self) -> bool {
204 let test_url = {
205 let session = self.session.lock().unwrap();
206 format!(
207 "{}/settings/subscription/automatic-edits/tracks",
208 session.base_url
209 )
210 };
211
212 let mut request = Request::new(Method::Get, test_url.parse::<Url>().unwrap());
213
214 {
215 let session = self.session.lock().unwrap();
216 headers::add_cookies(&mut request, &session.cookies);
217 }
218
219 headers::add_get_headers(&mut request, false, None);
220
221 match self.client.send(request).await {
222 Ok(response) => {
223 if response.status() == 302 || response.status() == 301 {
225 if let Some(location) = response.header("location") {
226 if let Some(redirect_url) = location.get(0) {
227 let redirect_url_str = redirect_url.as_str();
228 let is_valid = !redirect_url_str.contains("/login");
229
230 return is_valid;
231 }
232 }
233 }
234 true
235 }
236 Err(_e) => false,
237 }
238 }
239
240 pub fn subscribe(&self) -> ClientEventReceiver {
242 self.broadcaster.subscribe()
243 }
244
245 pub fn latest_event(&self) -> Option<ClientEvent> {
247 self.broadcaster.latest_event()
248 }
249
250 fn broadcast_event(&self, event: ClientEvent) {
254 self.broadcaster.broadcast_event(event);
255 }
256
257 pub async fn get_recent_scrobbles(&self, page: u32) -> Result<Vec<Track>> {
260 let url = {
261 let session = self.session.lock().unwrap();
262 format!(
263 "{}/user/{}/library?page={}",
264 session.base_url, session.username, page
265 )
266 };
267
268 log::debug!("Fetching recent scrobbles page {page}");
269 let mut response = self.get(&url).await?;
270 let content = response
271 .body_string()
272 .await
273 .map_err(|e| LastFmError::Http(e.to_string()))?;
274
275 log::debug!(
276 "Recent scrobbles response: {} status, {} chars",
277 response.status(),
278 content.len()
279 );
280
281 let document = Html::parse_document(&content);
282 self.parser.parse_recent_scrobbles(&document)
283 }
284
285 pub async fn get_recent_tracks_page(&self, page: u32) -> Result<TrackPage> {
287 let tracks = self.get_recent_scrobbles(page).await?;
288
289 let has_next_page = !tracks.is_empty(); Ok(TrackPage {
294 tracks,
295 page_number: page,
296 has_next_page,
297 total_pages: None, })
299 }
300
301 pub async fn find_recent_scrobble_for_track(
304 &self,
305 track_name: &str,
306 artist_name: &str,
307 max_pages: u32,
308 ) -> Result<Option<Track>> {
309 log::debug!("Searching for recent scrobble: '{track_name}' by '{artist_name}'");
310
311 for page in 1..=max_pages {
312 let scrobbles = self.get_recent_scrobbles(page).await?;
313
314 for scrobble in scrobbles {
315 if scrobble.name == track_name && scrobble.artist == artist_name {
316 log::debug!(
317 "Found recent scrobble: '{}' with timestamp {:?}",
318 scrobble.name,
319 scrobble.timestamp
320 );
321 return Ok(Some(scrobble));
322 }
323 }
324 }
325
326 log::debug!(
327 "No recent scrobble found for '{track_name}' by '{artist_name}' in {max_pages} pages"
328 );
329 Ok(None)
330 }
331
332 pub async fn edit_scrobble(&self, edit: &ScrobbleEdit) -> Result<EditResponse> {
333 let discovered_edits = self.discover_scrobble_edit_variations(edit).await?;
335
336 if discovered_edits.is_empty() {
337 let context = match (&edit.track_name_original, &edit.album_name_original) {
338 (Some(track_name), _) => {
339 format!("track '{}' by '{}'", track_name, edit.artist_name_original)
340 }
341 (None, Some(album_name)) => {
342 format!("album '{}' by '{}'", album_name, edit.artist_name_original)
343 }
344 (None, None) => format!("artist '{}'", edit.artist_name_original),
345 };
346 return Err(LastFmError::Parse(format!(
347 "No scrobbles found for {context}. Make sure the names are correct and that you have scrobbled recently."
348 )));
349 }
350
351 log::info!(
352 "Discovered {} scrobble instances to edit",
353 discovered_edits.len()
354 );
355
356 let mut all_results = Vec::new();
357
358 for (index, discovered_edit) in discovered_edits.iter().enumerate() {
360 log::debug!(
361 "Processing scrobble {}/{}: '{}' from '{}'",
362 index + 1,
363 discovered_edits.len(),
364 discovered_edit.track_name_original,
365 discovered_edit.album_name_original
366 );
367
368 let mut modified_exact_edit = discovered_edit.clone();
370
371 if let Some(new_track_name) = &edit.track_name {
373 modified_exact_edit.track_name = new_track_name.clone();
374 }
375 if let Some(new_album_name) = &edit.album_name {
376 modified_exact_edit.album_name = new_album_name.clone();
377 }
378 modified_exact_edit.artist_name = edit.artist_name.clone();
379 if let Some(new_album_artist_name) = &edit.album_artist_name {
380 modified_exact_edit.album_artist_name = new_album_artist_name.clone();
381 }
382 modified_exact_edit.edit_all = edit.edit_all;
383
384 let album_info = format!(
385 "{} by {}",
386 modified_exact_edit.album_name_original,
387 modified_exact_edit.album_artist_name_original
388 );
389
390 let single_response = self.edit_scrobble_single(&modified_exact_edit, 3).await?;
391 let success = single_response.success();
392 let message = single_response.message();
393
394 all_results.push(SingleEditResponse {
395 success,
396 message,
397 album_info: Some(album_info),
398 exact_scrobble_edit: modified_exact_edit.clone(),
399 });
400
401 if index < discovered_edits.len() - 1 {
403 tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
404 }
405 }
406
407 Ok(EditResponse::from_results(all_results))
408 }
409
410 pub async fn edit_scrobble_single(
421 &self,
422 exact_edit: &ExactScrobbleEdit,
423 max_retries: u32,
424 ) -> Result<EditResponse> {
425 let config = RetryConfig {
426 max_retries,
427 base_delay: 5,
428 max_delay: 300,
429 };
430
431 let edit_clone = exact_edit.clone();
432 let client = self.clone();
433
434 match retry::retry_with_backoff(
435 config,
436 "Edit scrobble",
437 || client.edit_scrobble_impl(&edit_clone),
438 |delay, operation_name| {
439 self.broadcast_event(ClientEvent::RateLimited {
440 delay_seconds: delay,
441 request: None, rate_limit_type: RateLimitType::ResponsePattern,
443 });
444 log::debug!("{operation_name} rate limited, waiting {delay} seconds");
445 },
446 )
447 .await
448 {
449 Ok(retry_result) => Ok(EditResponse::single(
450 retry_result.result,
451 None,
452 None,
453 exact_edit.clone(),
454 )),
455 Err(LastFmError::RateLimit { .. }) => Ok(EditResponse::single(
456 false,
457 Some(format!("Rate limit exceeded after {max_retries} retries")),
458 None,
459 exact_edit.clone(),
460 )),
461 Err(other_error) => Ok(EditResponse::single(
462 false,
463 Some(other_error.to_string()),
464 None,
465 exact_edit.clone(),
466 )),
467 }
468 }
469
470 async fn edit_scrobble_impl(&self, exact_edit: &ExactScrobbleEdit) -> Result<bool> {
471 let start_time = std::time::Instant::now();
472 let result = self.edit_scrobble_impl_internal(exact_edit).await;
473 let duration_ms = start_time.elapsed().as_millis() as u64;
474
475 match &result {
477 Ok(success) => {
478 self.broadcast_event(ClientEvent::EditAttempted {
479 edit: exact_edit.clone(),
480 success: *success,
481 error_message: None,
482 duration_ms,
483 });
484 }
485 Err(error) => {
486 self.broadcast_event(ClientEvent::EditAttempted {
487 edit: exact_edit.clone(),
488 success: false,
489 error_message: Some(error.to_string()),
490 duration_ms,
491 });
492 }
493 }
494
495 result
496 }
497
498 async fn edit_scrobble_impl_internal(&self, exact_edit: &ExactScrobbleEdit) -> Result<bool> {
499 let edit_url = {
500 let session = self.session.lock().unwrap();
501 format!(
502 "{}/user/{}/library/edit?edited-variation=library-track-scrobble",
503 session.base_url, session.username
504 )
505 };
506
507 log::debug!("Getting fresh CSRF token for edit");
508
509 let form_html = self.get_edit_form_html(&edit_url).await?;
511
512 let form_document = Html::parse_document(&form_html);
514 let fresh_csrf_token = self.extract_csrf_token(&form_document)?;
515
516 log::debug!("Submitting edit with fresh token");
517
518 let form_data = exact_edit.build_form_data(&fresh_csrf_token);
519
520 log::debug!(
521 "Editing scrobble: '{}' -> '{}'",
522 exact_edit.track_name_original,
523 exact_edit.track_name
524 );
525 {
526 let session = self.session.lock().unwrap();
527 log::trace!("Session cookies count: {}", session.cookies.len());
528 }
529
530 let mut request = Request::new(Method::Post, edit_url.parse::<Url>().unwrap());
531
532 let referer_url = {
534 let session = self.session.lock().unwrap();
535 headers::add_cookies(&mut request, &session.cookies);
536 format!("{}/user/{}/library", session.base_url, session.username)
537 };
538
539 headers::add_edit_headers(&mut request, &referer_url);
540
541 let form_string: String = form_data
543 .iter()
544 .map(|(k, v)| format!("{}={}", urlencoding::encode(k), urlencoding::encode(v)))
545 .collect::<Vec<_>>()
546 .join("&");
547
548 request.set_body(form_string);
549
550 let request_info = RequestInfo::from_url_and_method(&edit_url, "POST");
552 let request_start = std::time::Instant::now();
553
554 self.broadcast_event(ClientEvent::RequestStarted {
556 request: request_info.clone(),
557 });
558
559 let mut response = self
560 .client
561 .send(request)
562 .await
563 .map_err(|e| LastFmError::Http(e.to_string()))?;
564
565 self.broadcast_event(ClientEvent::RequestCompleted {
567 request: request_info.clone(),
568 status_code: response.status().into(),
569 duration_ms: request_start.elapsed().as_millis() as u64,
570 });
571
572 log::debug!("Edit response status: {}", response.status());
573
574 let response_text = response
575 .body_string()
576 .await
577 .map_err(|e| LastFmError::Http(e.to_string()))?;
578
579 let analysis = edit_analysis::analyze_edit_response(&response_text, response.status());
581
582 Ok(analysis.success)
583 }
584
585 async fn get_edit_form_html(&self, edit_url: &str) -> Result<String> {
588 let mut form_response = self.get(edit_url).await?;
589 let form_html = form_response
590 .body_string()
591 .await
592 .map_err(|e| LastFmError::Http(e.to_string()))?;
593
594 log::debug!("Edit form response status: {}", form_response.status());
595 Ok(form_html)
596 }
597
598 pub async fn load_edit_form_values_internal(
601 &self,
602 track_name: &str,
603 artist_name: &str,
604 ) -> Result<Vec<ExactScrobbleEdit>> {
605 log::debug!("Loading edit form values for '{track_name}' by '{artist_name}'");
606
607 let base_track_url = {
611 let session = self.session.lock().unwrap();
612 format!(
613 "{}/user/{}/library/music/+noredirect/{}/_/{}",
614 session.base_url,
615 session.username,
616 urlencoding::encode(artist_name),
617 urlencoding::encode(track_name)
618 )
619 };
620
621 log::debug!("Fetching track page: {base_track_url}");
622
623 let mut response = self.get(&base_track_url).await?;
624 let html = response
625 .body_string()
626 .await
627 .map_err(|e| crate::LastFmError::Http(e.to_string()))?;
628
629 let document = Html::parse_document(&html);
630
631 let mut all_scrobble_edits = Vec::new();
633 let mut unique_albums = std::collections::HashSet::new();
634 let max_pages = 5;
635
636 let page_edits = self.extract_scrobble_edits_from_page(
638 &document,
639 track_name,
640 artist_name,
641 &mut unique_albums,
642 )?;
643 all_scrobble_edits.extend(page_edits);
644
645 log::debug!(
646 "Page 1: found {} unique album variations",
647 all_scrobble_edits.len()
648 );
649
650 let pagination_selector = Selector::parse(".pagination .pagination-next").unwrap();
652 let mut has_next_page = document.select(&pagination_selector).next().is_some();
653 let mut page = 2;
654
655 while has_next_page && page <= max_pages {
656 let page_url = {
658 let session = self.session.lock().unwrap();
659 format!(
660 "{}/user/{}/library/music/{}/_/{}?page={page}",
661 session.base_url,
662 session.username,
663 urlencoding::encode(artist_name),
664 urlencoding::encode(track_name)
665 )
666 };
667
668 log::debug!("Fetching page {page} for additional album variations");
669
670 let mut response = self.get(&page_url).await?;
671 let html = response
672 .body_string()
673 .await
674 .map_err(|e| crate::LastFmError::Http(e.to_string()))?;
675
676 let document = Html::parse_document(&html);
677
678 let page_edits = self.extract_scrobble_edits_from_page(
679 &document,
680 track_name,
681 artist_name,
682 &mut unique_albums,
683 )?;
684
685 let initial_count = all_scrobble_edits.len();
686 all_scrobble_edits.extend(page_edits);
687 let found_new_unique_albums = all_scrobble_edits.len() > initial_count;
688
689 has_next_page = document.select(&pagination_selector).next().is_some();
691
692 log::debug!(
693 "Page {page}: found {} total unique albums ({})",
694 all_scrobble_edits.len(),
695 if found_new_unique_albums {
696 "new albums found"
697 } else {
698 "no new unique albums"
699 }
700 );
701
702 page += 1;
705 }
706
707 if all_scrobble_edits.is_empty() {
708 return Err(crate::LastFmError::Parse(format!(
709 "No scrobble forms found for track '{track_name}' by '{artist_name}'"
710 )));
711 }
712
713 log::debug!(
714 "Final result: found {} unique album variations for '{track_name}' by '{artist_name}'",
715 all_scrobble_edits.len(),
716 );
717
718 Ok(all_scrobble_edits)
719 }
720
721 fn extract_scrobble_edits_from_page(
724 &self,
725 document: &Html,
726 expected_track: &str,
727 expected_artist: &str,
728 unique_albums: &mut std::collections::HashSet<(String, String)>,
729 ) -> Result<Vec<ExactScrobbleEdit>> {
730 let mut scrobble_edits = Vec::new();
731 let table_selector =
733 Selector::parse("table.chartlist:not(.chartlist__placeholder)").unwrap();
734 let table = document.select(&table_selector).next().ok_or_else(|| {
735 crate::LastFmError::Parse("No chartlist table found on track page".to_string())
736 })?;
737
738 let row_selector = Selector::parse("tr").unwrap();
740 for row in table.select(&row_selector) {
741 let count_bar_link_selector = Selector::parse(".chartlist-count-bar-link").unwrap();
743 if row.select(&count_bar_link_selector).next().is_some() {
744 log::debug!("Found count bar link, skipping aggregated row");
745 continue;
746 }
747
748 let form_selector = Selector::parse("form[data-edit-scrobble]").unwrap();
750 if let Some(form) = row.select(&form_selector).next() {
751 let extract_form_value = |name: &str| -> Option<String> {
753 let selector = Selector::parse(&format!("input[name='{name}']")).unwrap();
754 form.select(&selector)
755 .next()
756 .and_then(|input| input.value().attr("value"))
757 .map(|s| s.to_string())
758 };
759
760 let form_track = extract_form_value("track_name").unwrap_or_default();
762 let form_artist = extract_form_value("artist_name").unwrap_or_default();
763 let form_album = extract_form_value("album_name").unwrap_or_default();
764 let form_album_artist =
765 extract_form_value("album_artist_name").unwrap_or_else(|| form_artist.clone());
766 let form_timestamp = extract_form_value("timestamp").unwrap_or_default();
767
768 log::debug!(
769 "Found scrobble form - Track: '{form_track}', Artist: '{form_artist}', Album: '{form_album}', Timestamp: {form_timestamp}"
770 );
771
772 if form_track == expected_track && form_artist == expected_artist {
774 let album_key = (form_album.clone(), form_album_artist.clone());
776 if unique_albums.insert(album_key) {
777 let timestamp = if form_timestamp.is_empty() {
779 None
780 } else {
781 form_timestamp.parse::<u64>().ok()
782 };
783
784 if let Some(timestamp) = timestamp {
785 log::debug!(
786 "✅ Found unique album variation: '{form_album}' by '{form_album_artist}' for '{expected_track}' by '{expected_artist}'"
787 );
788
789 let scrobble_edit = ExactScrobbleEdit::new(
791 form_track.clone(),
792 form_album.clone(),
793 form_artist.clone(),
794 form_album_artist.clone(),
795 form_track,
796 form_album,
797 form_artist,
798 form_album_artist,
799 timestamp,
800 true,
801 );
802 scrobble_edits.push(scrobble_edit);
803 } else {
804 log::debug!(
805 "⚠️ Skipping album variation without valid timestamp: '{form_album}' by '{form_album_artist}'"
806 );
807 }
808 }
809 }
810 }
811 }
812
813 Ok(scrobble_edits)
814 }
815
816 pub async fn get_artist_tracks_page(&self, artist: &str, page: u32) -> Result<TrackPage> {
817 let url = {
819 let session = self.session.lock().unwrap();
820 format!(
821 "{}/user/{}/library/music/{}/+tracks?page={}&ajax=true",
822 session.base_url,
823 session.username,
824 artist.replace(" ", "+"),
825 page
826 )
827 };
828
829 log::debug!("Fetching tracks page {page} for artist: {artist}");
830 let mut response = self.get(&url).await?;
831 let content = response
832 .body_string()
833 .await
834 .map_err(|e| LastFmError::Http(e.to_string()))?;
835
836 log::debug!(
837 "AJAX response: {} status, {} chars",
838 response.status(),
839 content.len()
840 );
841
842 log::debug!("Parsing HTML response from AJAX endpoint");
843 let document = Html::parse_document(&content);
844 self.parser.parse_tracks_page(&document, page, artist, None)
845 }
846
847 pub fn extract_tracks_from_document(
849 &self,
850 document: &Html,
851 artist: &str,
852 album: Option<&str>,
853 ) -> Result<Vec<Track>> {
854 self.parser
855 .extract_tracks_from_document(document, artist, album)
856 }
857
858 pub fn parse_tracks_page(
860 &self,
861 document: &Html,
862 page_number: u32,
863 artist: &str,
864 album: Option<&str>,
865 ) -> Result<TrackPage> {
866 self.parser
867 .parse_tracks_page(document, page_number, artist, album)
868 }
869
870 pub fn parse_recent_scrobbles(&self, document: &Html) -> Result<Vec<Track>> {
872 self.parser.parse_recent_scrobbles(document)
873 }
874
875 fn extract_csrf_token(&self, document: &Html) -> Result<String> {
876 let csrf_selector = Selector::parse("input[name=\"csrfmiddlewaretoken\"]").unwrap();
877
878 document
879 .select(&csrf_selector)
880 .next()
881 .and_then(|input| input.value().attr("value"))
882 .map(|token| token.to_string())
883 .ok_or(LastFmError::CsrfNotFound)
884 }
885
886 pub async fn get(&self, url: &str) -> Result<Response> {
888 self.get_with_retry(url, 3).await
889 }
890
891 async fn get_with_retry(&self, url: &str, max_retries: u32) -> Result<Response> {
893 let config = RetryConfig {
894 max_retries,
895 base_delay: 30, max_delay: 300,
897 };
898
899 let url_string = url.to_string();
900 let client = self.clone();
901
902 let retry_result = retry::retry_with_backoff(
903 config,
904 &format!("GET {url}"),
905 || async {
906 let mut response = client.get_with_redirects(&url_string, 0).await?;
907
908 let body = client
910 .extract_response_body(&url_string, &mut response)
911 .await?;
912
913 if response.status().is_success() && client.is_rate_limit_response(&body) {
915 log::debug!("Response body contains rate limit patterns");
916 return Err(LastFmError::RateLimit { retry_after: 60 });
917 }
918
919 let mut new_response = http_types::Response::new(response.status());
921 for (name, values) in response.iter() {
922 for value in values {
923 let _ = new_response.insert_header(name.clone(), value.clone());
924 }
925 }
926 new_response.set_body(body);
927
928 Ok(new_response)
929 },
930 |delay, operation_name| {
931 self.broadcast_event(ClientEvent::RateLimited {
932 delay_seconds: delay,
933 request: None, rate_limit_type: RateLimitType::ResponsePattern,
935 });
936 log::debug!("{operation_name} rate limited, waiting {delay} seconds");
937 },
938 )
939 .await?;
940
941 Ok(retry_result.result)
942 }
943
944 async fn get_with_redirects(&self, url: &str, redirect_count: u32) -> Result<Response> {
945 if redirect_count > 5 {
946 return Err(LastFmError::Http("Too many redirects".to_string()));
947 }
948
949 let mut request = Request::new(Method::Get, url.parse::<Url>().unwrap());
950
951 {
953 let session = self.session.lock().unwrap();
954 headers::add_cookies(&mut request, &session.cookies);
955 if session.cookies.is_empty() && url.contains("page=") {
956 log::debug!("No cookies available for paginated request!");
957 }
958 }
959
960 let is_ajax = url.contains("ajax=true");
961 let referer_url = if url.contains("page=") {
962 Some(url.split('?').next().unwrap_or(url))
963 } else {
964 None
965 };
966
967 headers::add_get_headers(&mut request, is_ajax, referer_url);
968
969 let request_info = RequestInfo::from_url_and_method(url, "GET");
970 let request_start = std::time::Instant::now();
971
972 self.broadcast_event(ClientEvent::RequestStarted {
973 request: request_info.clone(),
974 });
975
976 let response = self
977 .client
978 .send(request)
979 .await
980 .map_err(|e| LastFmError::Http(e.to_string()))?;
981
982 self.broadcast_event(ClientEvent::RequestCompleted {
983 request: request_info.clone(),
984 status_code: response.status().into(),
985 duration_ms: request_start.elapsed().as_millis() as u64,
986 });
987
988 self.extract_cookies(&response);
990
991 if response.status() == 302 || response.status() == 301 {
993 if let Some(location) = response.header("location") {
994 if let Some(redirect_url) = location.get(0) {
995 let redirect_url_str = redirect_url.as_str();
996 if url.contains("page=") {
997 log::debug!("Following redirect from {url} to {redirect_url_str}");
998
999 if redirect_url_str.contains("/login") {
1001 log::debug!("Redirect to login page - authentication failed for paginated request");
1002 return Err(LastFmError::Auth(
1003 "Session expired or invalid for paginated request".to_string(),
1004 ));
1005 }
1006 }
1007
1008 let full_redirect_url = if redirect_url_str.starts_with('/') {
1010 let base_url = self.session.lock().unwrap().base_url.clone();
1011 format!("{base_url}{redirect_url_str}")
1012 } else if redirect_url_str.starts_with("http") {
1013 redirect_url_str.to_string()
1014 } else {
1015 let base_url = url
1017 .rsplit('/')
1018 .skip(1)
1019 .collect::<Vec<_>>()
1020 .into_iter()
1021 .rev()
1022 .collect::<Vec<_>>()
1023 .join("/");
1024 format!("{base_url}/{redirect_url_str}")
1025 };
1026
1027 return Box::pin(
1029 self.get_with_redirects(&full_redirect_url, redirect_count + 1),
1030 )
1031 .await;
1032 }
1033 }
1034 }
1035
1036 if response.status() == 429 {
1038 let retry_after = response
1039 .header("retry-after")
1040 .and_then(|h| h.get(0))
1041 .and_then(|v| v.as_str().parse::<u64>().ok())
1042 .unwrap_or(60);
1043 self.broadcast_event(ClientEvent::RateLimited {
1044 delay_seconds: retry_after,
1045 request: Some(request_info.clone()),
1046 rate_limit_type: RateLimitType::Http429,
1047 });
1048 return Err(LastFmError::RateLimit { retry_after });
1049 }
1050
1051 if response.status() == 403 {
1053 log::debug!("Got 403 response, checking if it's a rate limit");
1054 {
1056 let session = self.session.lock().unwrap();
1057 if !session.cookies.is_empty() {
1058 log::debug!("403 on authenticated request - likely rate limit");
1059 self.broadcast_event(ClientEvent::RateLimited {
1060 delay_seconds: 60,
1061 request: Some(request_info.clone()),
1062 rate_limit_type: RateLimitType::Http403,
1063 });
1064 return Err(LastFmError::RateLimit { retry_after: 60 });
1065 }
1066 }
1067 }
1068
1069 Ok(response)
1070 }
1071
1072 fn is_rate_limit_response(&self, response_body: &str) -> bool {
1074 let body_lower = response_body.to_lowercase();
1075
1076 for pattern in &self.rate_limit_patterns {
1078 if body_lower.contains(&pattern.to_lowercase()) {
1079 return true;
1080 }
1081 }
1082
1083 false
1084 }
1085
1086 fn extract_cookies(&self, response: &Response) {
1087 let mut session = self.session.lock().unwrap();
1088 extract_cookies_from_response(response, &mut session.cookies);
1089 }
1090
1091 async fn extract_response_body(&self, _url: &str, response: &mut Response) -> Result<String> {
1093 let body = response
1094 .body_string()
1095 .await
1096 .map_err(|e| LastFmError::Http(e.to_string()))?;
1097
1098 Ok(body)
1099 }
1100
1101 pub async fn get_artist_albums_page(&self, artist: &str, page: u32) -> Result<AlbumPage> {
1102 let url = {
1104 let session = self.session.lock().unwrap();
1105 format!(
1106 "{}/user/{}/library/music/{}/+albums?page={}&ajax=true",
1107 session.base_url,
1108 session.username,
1109 artist.replace(" ", "+"),
1110 page
1111 )
1112 };
1113
1114 log::debug!("Fetching albums page {page} for artist: {artist}");
1115 let mut response = self.get(&url).await?;
1116 let content = response
1117 .body_string()
1118 .await
1119 .map_err(|e| LastFmError::Http(e.to_string()))?;
1120
1121 log::debug!(
1122 "AJAX response: {} status, {} chars",
1123 response.status(),
1124 content.len()
1125 );
1126
1127 log::debug!("Parsing HTML response from AJAX endpoint");
1128 let document = Html::parse_document(&content);
1129 self.parser.parse_albums_page(&document, page, artist)
1130 }
1131}
1132
1133#[async_trait(?Send)]
1134impl LastFmEditClient for LastFmEditClientImpl {
1135 fn username(&self) -> String {
1136 self.username()
1137 }
1138
1139 async fn get_recent_scrobbles(&self, page: u32) -> Result<Vec<Track>> {
1140 self.get_recent_scrobbles(page).await
1141 }
1142
1143 async fn find_recent_scrobble_for_track(
1144 &self,
1145 track_name: &str,
1146 artist_name: &str,
1147 max_pages: u32,
1148 ) -> Result<Option<Track>> {
1149 self.find_recent_scrobble_for_track(track_name, artist_name, max_pages)
1150 .await
1151 }
1152
1153 async fn edit_scrobble(&self, edit: &ScrobbleEdit) -> Result<EditResponse> {
1154 self.edit_scrobble(edit).await
1155 }
1156
1157 async fn edit_scrobble_single(
1158 &self,
1159 exact_edit: &ExactScrobbleEdit,
1160 max_retries: u32,
1161 ) -> Result<EditResponse> {
1162 self.edit_scrobble_single(exact_edit, max_retries).await
1163 }
1164
1165 fn get_session(&self) -> LastFmEditSession {
1166 self.get_session()
1167 }
1168
1169 fn restore_session(&self, session: LastFmEditSession) {
1170 self.restore_session(session)
1171 }
1172
1173 fn subscribe(&self) -> ClientEventReceiver {
1174 self.subscribe()
1175 }
1176
1177 fn latest_event(&self) -> Option<ClientEvent> {
1178 self.latest_event()
1179 }
1180
1181 fn discover_scrobbles(
1182 &self,
1183 edit: ScrobbleEdit,
1184 ) -> Box<dyn crate::AsyncDiscoveryIterator<crate::ExactScrobbleEdit>> {
1185 let track_name = edit.track_name_original.clone();
1186 let album_name = edit.album_name_original.clone();
1187
1188 match (&track_name, &album_name) {
1189 (Some(track_name), Some(album_name)) => Box::new(crate::ExactMatchDiscovery::new(
1191 self.clone(),
1192 edit,
1193 track_name.clone(),
1194 album_name.clone(),
1195 )),
1196
1197 (Some(track_name), None) => Box::new(crate::TrackVariationsDiscovery::new(
1199 self.clone(),
1200 edit,
1201 track_name.clone(),
1202 )),
1203
1204 (None, Some(album_name)) => Box::new(crate::AlbumTracksDiscovery::new(
1206 self.clone(),
1207 edit,
1208 album_name.clone(),
1209 )),
1210
1211 (None, None) => Box::new(crate::ArtistTracksDiscovery::new(self.clone(), edit)),
1213 }
1214 }
1215
1216 async fn get_artist_tracks_page(&self, artist: &str, page: u32) -> Result<TrackPage> {
1217 self.get_artist_tracks_page(artist, page).await
1218 }
1219
1220 async fn get_artist_albums_page(&self, artist: &str, page: u32) -> Result<AlbumPage> {
1221 self.get_artist_albums_page(artist, page).await
1222 }
1223
1224 fn artist_tracks(&self, artist: &str) -> crate::ArtistTracksIterator {
1225 crate::ArtistTracksIterator::new(self.clone(), artist.to_string())
1226 }
1227
1228 fn artist_albums(&self, artist: &str) -> crate::ArtistAlbumsIterator {
1229 crate::ArtistAlbumsIterator::new(self.clone(), artist.to_string())
1230 }
1231
1232 fn album_tracks(&self, album_name: &str, artist_name: &str) -> crate::AlbumTracksIterator {
1233 crate::AlbumTracksIterator::new(
1234 self.clone(),
1235 album_name.to_string(),
1236 artist_name.to_string(),
1237 )
1238 }
1239
1240 fn recent_tracks(&self) -> crate::RecentTracksIterator {
1241 crate::RecentTracksIterator::new(self.clone())
1242 }
1243
1244 fn recent_tracks_from_page(&self, starting_page: u32) -> crate::RecentTracksIterator {
1245 crate::RecentTracksIterator::with_starting_page(self.clone(), starting_page)
1246 }
1247
1248 async fn validate_session(&self) -> bool {
1249 self.validate_session().await
1250 }
1251}