1use crate::edit::{ExactScrobbleEdit, SingleEditResponse};
2use crate::edit_analysis;
3use crate::headers;
4use crate::login::extract_cookies_from_response;
5use crate::parsing::LastFmParser;
6use crate::r#trait::LastFmEditClient;
7use crate::retry::{self, RetryConfig};
8use crate::session::LastFmEditSession;
9use crate::{AlbumPage, EditResponse, LastFmError, Result, ScrobbleEdit, Track, TrackPage};
10use async_trait::async_trait;
11use http_client::{HttpClient, Request, Response};
12use http_types::{Method, Url};
13use scraper::{Html, Selector};
14use std::fs;
15use std::path::Path;
16use std::sync::{Arc, Mutex};
17use tokio::sync::{broadcast, watch};
18
19#[derive(Clone, Debug)]
21pub enum ClientEvent {
22 RateLimited(u64),
24}
25
26pub type ClientEventReceiver = broadcast::Receiver<ClientEvent>;
28
29pub type ClientEventWatcher = watch::Receiver<Option<ClientEvent>>;
31
32#[derive(Clone)]
34struct SharedEventBroadcaster {
35 event_tx: broadcast::Sender<ClientEvent>,
36 last_event_tx: watch::Sender<Option<ClientEvent>>,
37}
38
39impl SharedEventBroadcaster {
40 fn new() -> Self {
41 let (event_tx, _) = broadcast::channel(100);
42 let (last_event_tx, _) = watch::channel(None);
43 Self {
44 event_tx,
45 last_event_tx,
46 }
47 }
48
49 fn broadcast_event(&self, event: ClientEvent) {
50 let _ = self.event_tx.send(event.clone());
52 let _ = self.last_event_tx.send(Some(event));
54 }
55
56 fn subscribe(&self) -> ClientEventReceiver {
57 self.event_tx.subscribe()
58 }
59
60 fn latest_event(&self) -> Option<ClientEvent> {
61 self.last_event_tx.borrow().clone()
62 }
63}
64
65#[derive(Clone)]
71pub struct LastFmEditClientImpl {
72 client: Arc<dyn HttpClient + Send + Sync>,
73 session: Arc<Mutex<LastFmEditSession>>,
74 rate_limit_patterns: Vec<String>,
75 debug_save_responses: bool,
76 parser: LastFmParser,
77 broadcaster: Arc<SharedEventBroadcaster>,
78}
79
80impl LastFmEditClientImpl {
81 pub fn from_session(
92 client: Box<dyn HttpClient + Send + Sync>,
93 session: LastFmEditSession,
94 ) -> Self {
95 Self::from_session_with_arc(Arc::from(client), session)
96 }
97
98 fn from_session_with_arc(
102 client: Arc<dyn HttpClient + Send + Sync>,
103 session: LastFmEditSession,
104 ) -> Self {
105 Self::from_session_with_broadcaster_arc(
106 client,
107 session,
108 Arc::new(SharedEventBroadcaster::new()),
109 )
110 }
111
112 pub fn from_session_with_rate_limit_patterns(
122 client: Box<dyn HttpClient + Send + Sync>,
123 session: LastFmEditSession,
124 rate_limit_patterns: Vec<String>,
125 ) -> Self {
126 Self {
127 client: Arc::from(client),
128 session: Arc::new(Mutex::new(session)),
129 rate_limit_patterns,
130 debug_save_responses: std::env::var("LASTFM_DEBUG_SAVE_RESPONSES").is_ok(),
131 parser: LastFmParser::new(),
132 broadcaster: Arc::new(SharedEventBroadcaster::new()),
133 }
134 }
135
136 pub async fn login_with_credentials(
151 client: Box<dyn HttpClient + Send + Sync>,
152 username: &str,
153 password: &str,
154 ) -> Result<Self> {
155 let client_arc: Arc<dyn HttpClient + Send + Sync> = Arc::from(client);
156 let login_manager =
157 crate::login::LoginManager::new(client_arc.clone(), "https://www.last.fm".to_string());
158 let session = login_manager.login(username, password).await?;
159 Ok(Self::from_session_with_arc(client_arc, session))
160 }
161
162 fn from_session_with_broadcaster(
177 client: Box<dyn HttpClient + Send + Sync>,
178 session: LastFmEditSession,
179 broadcaster: Arc<SharedEventBroadcaster>,
180 ) -> Self {
181 Self::from_session_with_broadcaster_arc(Arc::from(client), session, broadcaster)
182 }
183
184 fn from_session_with_broadcaster_arc(
186 client: Arc<dyn HttpClient + Send + Sync>,
187 session: LastFmEditSession,
188 broadcaster: Arc<SharedEventBroadcaster>,
189 ) -> Self {
190 Self {
191 client,
192 session: Arc::new(Mutex::new(session)),
193 rate_limit_patterns: vec![
194 "you've tried to log in too many times".to_string(),
195 "you're requesting too many pages".to_string(),
196 "slow down".to_string(),
197 "too fast".to_string(),
198 "rate limit".to_string(),
199 "throttled".to_string(),
200 "temporarily blocked".to_string(),
201 "temporarily restricted".to_string(),
202 "captcha".to_string(),
203 "verify you're human".to_string(),
204 "prove you're not a robot".to_string(),
205 "security check".to_string(),
206 "service temporarily unavailable".to_string(),
207 "quota exceeded".to_string(),
208 "limit exceeded".to_string(),
209 "daily limit".to_string(),
210 ],
211 debug_save_responses: std::env::var("LASTFM_DEBUG_SAVE_RESPONSES").is_ok(),
212 parser: LastFmParser::new(),
213 broadcaster,
214 }
215 }
216
217 pub fn get_session(&self) -> LastFmEditSession {
219 self.session.lock().unwrap().clone()
220 }
221
222 pub fn restore_session(&self, session: LastFmEditSession) {
224 *self.session.lock().unwrap() = session;
225 }
226
227 pub fn with_shared_broadcaster(&self, client: Box<dyn HttpClient + Send + Sync>) -> Self {
241 let session = self.get_session();
242 Self::from_session_with_broadcaster(client, session, self.broadcaster.clone())
243 }
244
245 pub fn username(&self) -> String {
249 self.session.lock().unwrap().username.clone()
250 }
251
252 pub fn subscribe(&self) -> ClientEventReceiver {
254 self.broadcaster.subscribe()
255 }
256
257 pub fn latest_event(&self) -> Option<ClientEvent> {
259 self.broadcaster.latest_event()
260 }
261
262 fn broadcast_event(&self, event: ClientEvent) {
266 self.broadcaster.broadcast_event(event);
267 }
268
269 pub async fn get_recent_scrobbles(&self, page: u32) -> Result<Vec<Track>> {
272 let url = {
273 let session = self.session.lock().unwrap();
274 format!(
275 "{}/user/{}/library?page={}",
276 session.base_url, session.username, page
277 )
278 };
279
280 log::debug!("Fetching recent scrobbles page {page}");
281 let mut response = self.get(&url).await?;
282 let content = response
283 .body_string()
284 .await
285 .map_err(|e| LastFmError::Http(e.to_string()))?;
286
287 log::debug!(
288 "Recent scrobbles response: {} status, {} chars",
289 response.status(),
290 content.len()
291 );
292
293 let document = Html::parse_document(&content);
294 self.parser.parse_recent_scrobbles(&document)
295 }
296
297 pub async fn get_recent_tracks_page(&self, page: u32) -> Result<TrackPage> {
299 let tracks = self.get_recent_scrobbles(page).await?;
300
301 let has_next_page = !tracks.is_empty(); Ok(TrackPage {
306 tracks,
307 page_number: page,
308 has_next_page,
309 total_pages: None, })
311 }
312
313 pub async fn find_recent_scrobble_for_track(
316 &self,
317 track_name: &str,
318 artist_name: &str,
319 max_pages: u32,
320 ) -> Result<Option<Track>> {
321 log::debug!("Searching for recent scrobble: '{track_name}' by '{artist_name}'");
322
323 for page in 1..=max_pages {
324 let scrobbles = self.get_recent_scrobbles(page).await?;
325
326 for scrobble in scrobbles {
327 if scrobble.name == track_name && scrobble.artist == artist_name {
328 log::debug!(
329 "Found recent scrobble: '{}' with timestamp {:?}",
330 scrobble.name,
331 scrobble.timestamp
332 );
333 return Ok(Some(scrobble));
334 }
335 }
336 }
337
338 log::debug!(
339 "No recent scrobble found for '{track_name}' by '{artist_name}' in {max_pages} pages"
340 );
341 Ok(None)
342 }
343
344 pub async fn edit_scrobble(&self, edit: &ScrobbleEdit) -> Result<EditResponse> {
345 let discovered_edits = self.discover_scrobble_edit_variations(edit).await?;
347
348 if discovered_edits.is_empty() {
349 let context = match (&edit.track_name_original, &edit.album_name_original) {
350 (Some(track_name), _) => {
351 format!("track '{}' by '{}'", track_name, edit.artist_name_original)
352 }
353 (None, Some(album_name)) => {
354 format!("album '{}' by '{}'", album_name, edit.artist_name_original)
355 }
356 (None, None) => format!("artist '{}'", edit.artist_name_original),
357 };
358 return Err(LastFmError::Parse(format!(
359 "No scrobbles found for {context}. Make sure the names are correct and that you have scrobbled recently."
360 )));
361 }
362
363 log::info!(
364 "Discovered {} scrobble instances to edit",
365 discovered_edits.len()
366 );
367
368 let mut all_results = Vec::new();
369
370 for (index, discovered_edit) in discovered_edits.iter().enumerate() {
372 log::debug!(
373 "Processing scrobble {}/{}: '{}' from '{}'",
374 index + 1,
375 discovered_edits.len(),
376 discovered_edit.track_name_original,
377 discovered_edit.album_name_original
378 );
379
380 let mut modified_exact_edit = discovered_edit.clone();
382
383 if let Some(new_track_name) = &edit.track_name {
385 modified_exact_edit.track_name = new_track_name.clone();
386 }
387 if let Some(new_album_name) = &edit.album_name {
388 modified_exact_edit.album_name = new_album_name.clone();
389 }
390 modified_exact_edit.artist_name = edit.artist_name.clone();
391 if let Some(new_album_artist_name) = &edit.album_artist_name {
392 modified_exact_edit.album_artist_name = new_album_artist_name.clone();
393 }
394 modified_exact_edit.edit_all = edit.edit_all;
395
396 let album_info = format!(
397 "{} by {}",
398 modified_exact_edit.album_name_original,
399 modified_exact_edit.album_artist_name_original
400 );
401
402 let single_response = self.edit_scrobble_single(&modified_exact_edit, 3).await?;
403 let success = single_response.success();
404 let message = single_response.message();
405
406 all_results.push(SingleEditResponse {
407 success,
408 message,
409 album_info: Some(album_info),
410 exact_scrobble_edit: modified_exact_edit.clone(),
411 });
412
413 if index < discovered_edits.len() - 1 {
415 tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
416 }
417 }
418
419 Ok(EditResponse::from_results(all_results))
420 }
421
422 pub async fn edit_scrobble_single(
433 &self,
434 exact_edit: &ExactScrobbleEdit,
435 max_retries: u32,
436 ) -> Result<EditResponse> {
437 let config = RetryConfig {
438 max_retries,
439 base_delay: 5,
440 max_delay: 300,
441 };
442
443 let edit_clone = exact_edit.clone();
444 let client = self.clone();
445
446 match retry::retry_with_backoff(
447 config,
448 "Edit scrobble",
449 || client.edit_scrobble_impl(&edit_clone),
450 |delay, operation_name| {
451 self.broadcast_event(ClientEvent::RateLimited(delay));
452 log::debug!("{operation_name} rate limited, waiting {delay} seconds");
453 },
454 )
455 .await
456 {
457 Ok(retry_result) => Ok(EditResponse::single(
458 retry_result.result,
459 None,
460 None,
461 exact_edit.clone(),
462 )),
463 Err(LastFmError::RateLimit { .. }) => Ok(EditResponse::single(
464 false,
465 Some(format!("Rate limit exceeded after {max_retries} retries")),
466 None,
467 exact_edit.clone(),
468 )),
469 Err(other_error) => Ok(EditResponse::single(
470 false,
471 Some(other_error.to_string()),
472 None,
473 exact_edit.clone(),
474 )),
475 }
476 }
477
478 async fn edit_scrobble_impl(&self, exact_edit: &ExactScrobbleEdit) -> Result<bool> {
479 let edit_url = {
480 let session = self.session.lock().unwrap();
481 format!(
482 "{}/user/{}/library/edit?edited-variation=library-track-scrobble",
483 session.base_url, session.username
484 )
485 };
486
487 log::debug!("Getting fresh CSRF token for edit");
488
489 let form_html = self.get_edit_form_html(&edit_url).await?;
491
492 let form_document = Html::parse_document(&form_html);
494 let fresh_csrf_token = self.extract_csrf_token(&form_document)?;
495
496 log::debug!("Submitting edit with fresh token");
497
498 let form_data = exact_edit.build_form_data(&fresh_csrf_token);
499
500 log::debug!(
501 "Editing scrobble: '{}' -> '{}'",
502 exact_edit.track_name_original,
503 exact_edit.track_name
504 );
505 {
506 let session = self.session.lock().unwrap();
507 log::trace!("Session cookies count: {}", session.cookies.len());
508 }
509
510 let mut request = Request::new(Method::Post, edit_url.parse::<Url>().unwrap());
511
512 let referer_url = {
514 let session = self.session.lock().unwrap();
515 headers::add_cookies(&mut request, &session.cookies);
516 format!("{}/user/{}/library", session.base_url, session.username)
517 };
518
519 headers::add_edit_headers(&mut request, &referer_url);
520
521 let form_string: String = form_data
523 .iter()
524 .map(|(k, v)| format!("{}={}", urlencoding::encode(k), urlencoding::encode(v)))
525 .collect::<Vec<_>>()
526 .join("&");
527
528 request.set_body(form_string);
529
530 let mut response = self
531 .client
532 .send(request)
533 .await
534 .map_err(|e| LastFmError::Http(e.to_string()))?;
535
536 log::debug!("Edit response status: {}", response.status());
537
538 let response_text = response
539 .body_string()
540 .await
541 .map_err(|e| LastFmError::Http(e.to_string()))?;
542
543 let analysis = edit_analysis::analyze_edit_response(&response_text, response.status());
545
546 Ok(analysis.success)
547 }
548
549 async fn get_edit_form_html(&self, edit_url: &str) -> Result<String> {
552 let mut form_response = self.get(edit_url).await?;
553 let form_html = form_response
554 .body_string()
555 .await
556 .map_err(|e| LastFmError::Http(e.to_string()))?;
557
558 log::debug!("Edit form response status: {}", form_response.status());
559 Ok(form_html)
560 }
561
562 pub async fn load_edit_form_values_internal(
565 &self,
566 track_name: &str,
567 artist_name: &str,
568 ) -> Result<Vec<ExactScrobbleEdit>> {
569 log::debug!("Loading edit form values for '{track_name}' by '{artist_name}'");
570
571 let base_track_url = {
575 let session = self.session.lock().unwrap();
576 format!(
577 "{}/user/{}/library/music/+noredirect/{}/_/{}",
578 session.base_url,
579 session.username,
580 urlencoding::encode(artist_name),
581 urlencoding::encode(track_name)
582 )
583 };
584
585 log::debug!("Fetching track page: {base_track_url}");
586
587 let mut response = self.get(&base_track_url).await?;
588 let html = response
589 .body_string()
590 .await
591 .map_err(|e| crate::LastFmError::Http(e.to_string()))?;
592
593 let document = Html::parse_document(&html);
594
595 let mut all_scrobble_edits = Vec::new();
597 let mut unique_albums = std::collections::HashSet::new();
598 let max_pages = 5;
599
600 let page_edits = self.extract_scrobble_edits_from_page(
602 &document,
603 track_name,
604 artist_name,
605 &mut unique_albums,
606 )?;
607 all_scrobble_edits.extend(page_edits);
608
609 log::debug!(
610 "Page 1: found {} unique album variations",
611 all_scrobble_edits.len()
612 );
613
614 let pagination_selector = Selector::parse(".pagination .pagination-next").unwrap();
616 let mut has_next_page = document.select(&pagination_selector).next().is_some();
617 let mut page = 2;
618
619 while has_next_page && page <= max_pages {
620 let page_url = {
622 let session = self.session.lock().unwrap();
623 format!(
624 "{}/user/{}/library/music/{}/_/{}?page={page}",
625 session.base_url,
626 session.username,
627 urlencoding::encode(artist_name),
628 urlencoding::encode(track_name)
629 )
630 };
631
632 log::debug!("Fetching page {page} for additional album variations");
633
634 let mut response = self.get(&page_url).await?;
635 let html = response
636 .body_string()
637 .await
638 .map_err(|e| crate::LastFmError::Http(e.to_string()))?;
639
640 let document = Html::parse_document(&html);
641
642 let page_edits = self.extract_scrobble_edits_from_page(
643 &document,
644 track_name,
645 artist_name,
646 &mut unique_albums,
647 )?;
648
649 let initial_count = all_scrobble_edits.len();
650 all_scrobble_edits.extend(page_edits);
651 let found_new_unique_albums = all_scrobble_edits.len() > initial_count;
652
653 has_next_page = document.select(&pagination_selector).next().is_some();
655
656 log::debug!(
657 "Page {page}: found {} total unique albums ({})",
658 all_scrobble_edits.len(),
659 if found_new_unique_albums {
660 "new albums found"
661 } else {
662 "no new unique albums"
663 }
664 );
665
666 page += 1;
669 }
670
671 if all_scrobble_edits.is_empty() {
672 return Err(crate::LastFmError::Parse(format!(
673 "No scrobble forms found for track '{track_name}' by '{artist_name}'"
674 )));
675 }
676
677 log::debug!(
678 "Final result: found {} unique album variations for '{track_name}' by '{artist_name}'",
679 all_scrobble_edits.len(),
680 );
681
682 Ok(all_scrobble_edits)
683 }
684
685 fn extract_scrobble_edits_from_page(
688 &self,
689 document: &Html,
690 expected_track: &str,
691 expected_artist: &str,
692 unique_albums: &mut std::collections::HashSet<(String, String)>,
693 ) -> Result<Vec<ExactScrobbleEdit>> {
694 let mut scrobble_edits = Vec::new();
695 let table_selector =
697 Selector::parse("table.chartlist:not(.chartlist__placeholder)").unwrap();
698 let table = document.select(&table_selector).next().ok_or_else(|| {
699 crate::LastFmError::Parse("No chartlist table found on track page".to_string())
700 })?;
701
702 let row_selector = Selector::parse("tr").unwrap();
704 for row in table.select(&row_selector) {
705 let count_bar_link_selector = Selector::parse(".chartlist-count-bar-link").unwrap();
707 if row.select(&count_bar_link_selector).next().is_some() {
708 log::debug!("Found count bar link, skipping aggregated row");
709 continue;
710 }
711
712 let form_selector = Selector::parse("form[data-edit-scrobble]").unwrap();
714 if let Some(form) = row.select(&form_selector).next() {
715 let extract_form_value = |name: &str| -> Option<String> {
717 let selector = Selector::parse(&format!("input[name='{name}']")).unwrap();
718 form.select(&selector)
719 .next()
720 .and_then(|input| input.value().attr("value"))
721 .map(|s| s.to_string())
722 };
723
724 let form_track = extract_form_value("track_name").unwrap_or_default();
726 let form_artist = extract_form_value("artist_name").unwrap_or_default();
727 let form_album = extract_form_value("album_name").unwrap_or_default();
728 let form_album_artist =
729 extract_form_value("album_artist_name").unwrap_or_else(|| form_artist.clone());
730 let form_timestamp = extract_form_value("timestamp").unwrap_or_default();
731
732 log::debug!(
733 "Found scrobble form - Track: '{form_track}', Artist: '{form_artist}', Album: '{form_album}', Timestamp: {form_timestamp}"
734 );
735
736 if form_track == expected_track && form_artist == expected_artist {
738 let album_key = (form_album.clone(), form_album_artist.clone());
740 if unique_albums.insert(album_key) {
741 let timestamp = if form_timestamp.is_empty() {
743 None
744 } else {
745 form_timestamp.parse::<u64>().ok()
746 };
747
748 if let Some(timestamp) = timestamp {
749 log::debug!(
750 "✅ Found unique album variation: '{form_album}' by '{form_album_artist}' for '{expected_track}' by '{expected_artist}'"
751 );
752
753 let scrobble_edit = ExactScrobbleEdit::new(
755 form_track.clone(),
756 form_album.clone(),
757 form_artist.clone(),
758 form_album_artist.clone(),
759 form_track,
760 form_album,
761 form_artist,
762 form_album_artist,
763 timestamp,
764 true,
765 );
766 scrobble_edits.push(scrobble_edit);
767 } else {
768 log::debug!(
769 "⚠️ Skipping album variation without valid timestamp: '{form_album}' by '{form_album_artist}'"
770 );
771 }
772 }
773 }
774 }
775 }
776
777 Ok(scrobble_edits)
778 }
779
780 pub async fn get_artist_tracks_page(&self, artist: &str, page: u32) -> Result<TrackPage> {
781 let url = {
783 let session = self.session.lock().unwrap();
784 format!(
785 "{}/user/{}/library/music/{}/+tracks?page={}&ajax=true",
786 session.base_url,
787 session.username,
788 artist.replace(" ", "+"),
789 page
790 )
791 };
792
793 log::debug!("Fetching tracks page {page} for artist: {artist}");
794 let mut response = self.get(&url).await?;
795 let content = response
796 .body_string()
797 .await
798 .map_err(|e| LastFmError::Http(e.to_string()))?;
799
800 log::debug!(
801 "AJAX response: {} status, {} chars",
802 response.status(),
803 content.len()
804 );
805
806 log::debug!("Parsing HTML response from AJAX endpoint");
807 let document = Html::parse_document(&content);
808 self.parser.parse_tracks_page(&document, page, artist, None)
809 }
810
811 pub fn extract_tracks_from_document(
813 &self,
814 document: &Html,
815 artist: &str,
816 album: Option<&str>,
817 ) -> Result<Vec<Track>> {
818 self.parser
819 .extract_tracks_from_document(document, artist, album)
820 }
821
822 pub fn parse_tracks_page(
824 &self,
825 document: &Html,
826 page_number: u32,
827 artist: &str,
828 album: Option<&str>,
829 ) -> Result<TrackPage> {
830 self.parser
831 .parse_tracks_page(document, page_number, artist, album)
832 }
833
834 pub fn parse_recent_scrobbles(&self, document: &Html) -> Result<Vec<Track>> {
836 self.parser.parse_recent_scrobbles(document)
837 }
838
839 fn extract_csrf_token(&self, document: &Html) -> Result<String> {
840 let csrf_selector = Selector::parse("input[name=\"csrfmiddlewaretoken\"]").unwrap();
841
842 document
843 .select(&csrf_selector)
844 .next()
845 .and_then(|input| input.value().attr("value"))
846 .map(|token| token.to_string())
847 .ok_or(LastFmError::CsrfNotFound)
848 }
849
850 pub async fn get(&self, url: &str) -> Result<Response> {
852 self.get_with_retry(url, 3).await
853 }
854
855 async fn get_with_retry(&self, url: &str, max_retries: u32) -> Result<Response> {
857 let config = RetryConfig {
858 max_retries,
859 base_delay: 30, max_delay: 300,
861 };
862
863 let url_string = url.to_string();
864 let client = self.clone();
865
866 let retry_result = retry::retry_with_backoff(
867 config,
868 &format!("GET {url}"),
869 || async {
870 let mut response = client.get_with_redirects(&url_string, 0).await?;
871
872 let body = client
874 .extract_response_body(&url_string, &mut response)
875 .await?;
876
877 if response.status().is_success() && client.is_rate_limit_response(&body) {
879 log::debug!("Response body contains rate limit patterns");
880 return Err(LastFmError::RateLimit { retry_after: 60 });
881 }
882
883 let mut new_response = http_types::Response::new(response.status());
885 for (name, values) in response.iter() {
886 for value in values {
887 let _ = new_response.insert_header(name.clone(), value.clone());
888 }
889 }
890 new_response.set_body(body);
891
892 Ok(new_response)
893 },
894 |delay, operation_name| {
895 self.broadcast_event(ClientEvent::RateLimited(delay));
896 log::debug!("{operation_name} rate limited, waiting {delay} seconds");
897 },
898 )
899 .await?;
900
901 Ok(retry_result.result)
902 }
903
904 async fn get_with_redirects(&self, url: &str, redirect_count: u32) -> Result<Response> {
905 if redirect_count > 5 {
906 return Err(LastFmError::Http("Too many redirects".to_string()));
907 }
908
909 let mut request = Request::new(Method::Get, url.parse::<Url>().unwrap());
910
911 {
913 let session = self.session.lock().unwrap();
914 headers::add_cookies(&mut request, &session.cookies);
915 if session.cookies.is_empty() && url.contains("page=") {
916 log::debug!("No cookies available for paginated request!");
917 }
918 }
919
920 let is_ajax = url.contains("ajax=true");
922 let referer_url = if url.contains("page=") {
923 Some(url.split('?').next().unwrap_or(url))
924 } else {
925 None
926 };
927
928 headers::add_get_headers(&mut request, is_ajax, referer_url);
929
930 let response = self
931 .client
932 .send(request)
933 .await
934 .map_err(|e| LastFmError::Http(e.to_string()))?;
935
936 self.extract_cookies(&response);
938
939 if response.status() == 302 || response.status() == 301 {
941 if let Some(location) = response.header("location") {
942 if let Some(redirect_url) = location.get(0) {
943 let redirect_url_str = redirect_url.as_str();
944 if url.contains("page=") {
945 log::debug!("Following redirect from {url} to {redirect_url_str}");
946
947 if redirect_url_str.contains("/login") {
949 log::debug!("Redirect to login page - authentication failed for paginated request");
950 return Err(LastFmError::Auth(
951 "Session expired or invalid for paginated request".to_string(),
952 ));
953 }
954 }
955
956 let full_redirect_url = if redirect_url_str.starts_with('/') {
958 let base_url = self.session.lock().unwrap().base_url.clone();
959 format!("{base_url}{redirect_url_str}")
960 } else if redirect_url_str.starts_with("http") {
961 redirect_url_str.to_string()
962 } else {
963 let base_url = url
965 .rsplit('/')
966 .skip(1)
967 .collect::<Vec<_>>()
968 .into_iter()
969 .rev()
970 .collect::<Vec<_>>()
971 .join("/");
972 format!("{base_url}/{redirect_url_str}")
973 };
974
975 return Box::pin(
977 self.get_with_redirects(&full_redirect_url, redirect_count + 1),
978 )
979 .await;
980 }
981 }
982 }
983
984 if response.status() == 429 {
986 let retry_after = response
987 .header("retry-after")
988 .and_then(|h| h.get(0))
989 .and_then(|v| v.as_str().parse::<u64>().ok())
990 .unwrap_or(60);
991 self.broadcast_event(ClientEvent::RateLimited(retry_after));
992 return Err(LastFmError::RateLimit { retry_after });
993 }
994
995 if response.status() == 403 {
997 log::debug!("Got 403 response, checking if it's a rate limit");
998 {
1000 let session = self.session.lock().unwrap();
1001 if !session.cookies.is_empty() {
1002 log::debug!("403 on authenticated request - likely rate limit");
1003 self.broadcast_event(ClientEvent::RateLimited(60));
1004 return Err(LastFmError::RateLimit { retry_after: 60 });
1005 }
1006 }
1007 }
1008
1009 Ok(response)
1010 }
1011
1012 fn is_rate_limit_response(&self, response_body: &str) -> bool {
1014 let body_lower = response_body.to_lowercase();
1015
1016 for pattern in &self.rate_limit_patterns {
1018 if body_lower.contains(&pattern.to_lowercase()) {
1019 return true;
1020 }
1021 }
1022
1023 false
1024 }
1025
1026 fn extract_cookies(&self, response: &Response) {
1027 let mut session = self.session.lock().unwrap();
1028 extract_cookies_from_response(response, &mut session.cookies);
1029 }
1030
1031 async fn extract_response_body(&self, url: &str, response: &mut Response) -> Result<String> {
1033 let body = response
1034 .body_string()
1035 .await
1036 .map_err(|e| LastFmError::Http(e.to_string()))?;
1037
1038 if self.debug_save_responses {
1039 self.save_debug_response(url, response.status().into(), &body);
1040 }
1041
1042 Ok(body)
1043 }
1044
1045 fn save_debug_response(&self, url: &str, status_code: u16, body: &str) {
1047 if let Err(e) = self.try_save_debug_response(url, status_code, body) {
1048 log::warn!("Failed to save debug response: {e}");
1049 }
1050 }
1051
1052 fn try_save_debug_response(&self, url: &str, status_code: u16, body: &str) -> Result<()> {
1054 let debug_dir = Path::new("debug_responses");
1056 if !debug_dir.exists() {
1057 fs::create_dir_all(debug_dir)
1058 .map_err(|e| LastFmError::Http(format!("Failed to create debug directory: {e}")))?;
1059 }
1060
1061 let url_path = {
1063 let session = self.session.lock().unwrap();
1064 if url.starts_with(&session.base_url) {
1065 &url[session.base_url.len()..]
1066 } else {
1067 url
1068 }
1069 };
1070
1071 let now = chrono::Utc::now();
1073 let timestamp = now.format("%Y%m%d_%H%M%S_%3f");
1074 let safe_path = url_path.replace(['/', '?', '&', '=', '%', '+'], "_");
1075
1076 let filename = format!("{timestamp}_{safe_path}_status{status_code}.html");
1077 let file_path = debug_dir.join(filename);
1078
1079 fs::write(&file_path, body)
1081 .map_err(|e| LastFmError::Http(format!("Failed to write debug file: {e}")))?;
1082
1083 log::debug!(
1084 "Saved HTTP response to {file_path:?} (status: {status_code}, url: {url_path})"
1085 );
1086
1087 Ok(())
1088 }
1089
1090 pub async fn get_artist_albums_page(&self, artist: &str, page: u32) -> Result<AlbumPage> {
1091 let url = {
1093 let session = self.session.lock().unwrap();
1094 format!(
1095 "{}/user/{}/library/music/{}/+albums?page={}&ajax=true",
1096 session.base_url,
1097 session.username,
1098 artist.replace(" ", "+"),
1099 page
1100 )
1101 };
1102
1103 log::debug!("Fetching albums page {page} for artist: {artist}");
1104 let mut response = self.get(&url).await?;
1105 let content = response
1106 .body_string()
1107 .await
1108 .map_err(|e| LastFmError::Http(e.to_string()))?;
1109
1110 log::debug!(
1111 "AJAX response: {} status, {} chars",
1112 response.status(),
1113 content.len()
1114 );
1115
1116 log::debug!("Parsing HTML response from AJAX endpoint");
1117 let document = Html::parse_document(&content);
1118 self.parser.parse_albums_page(&document, page, artist)
1119 }
1120}
1121
1122#[async_trait(?Send)]
1123impl LastFmEditClient for LastFmEditClientImpl {
1124 fn username(&self) -> String {
1125 self.username()
1126 }
1127
1128 async fn get_recent_scrobbles(&self, page: u32) -> Result<Vec<Track>> {
1129 self.get_recent_scrobbles(page).await
1130 }
1131
1132 async fn find_recent_scrobble_for_track(
1133 &self,
1134 track_name: &str,
1135 artist_name: &str,
1136 max_pages: u32,
1137 ) -> Result<Option<Track>> {
1138 self.find_recent_scrobble_for_track(track_name, artist_name, max_pages)
1139 .await
1140 }
1141
1142 async fn edit_scrobble(&self, edit: &ScrobbleEdit) -> Result<EditResponse> {
1143 self.edit_scrobble(edit).await
1144 }
1145
1146 async fn edit_scrobble_single(
1147 &self,
1148 exact_edit: &ExactScrobbleEdit,
1149 max_retries: u32,
1150 ) -> Result<EditResponse> {
1151 self.edit_scrobble_single(exact_edit, max_retries).await
1152 }
1153
1154 fn get_session(&self) -> LastFmEditSession {
1155 self.get_session()
1156 }
1157
1158 fn restore_session(&self, session: LastFmEditSession) {
1159 self.restore_session(session)
1160 }
1161
1162 fn subscribe(&self) -> ClientEventReceiver {
1163 self.subscribe()
1164 }
1165
1166 fn latest_event(&self) -> Option<ClientEvent> {
1167 self.latest_event()
1168 }
1169
1170 fn discover_scrobbles(
1171 &self,
1172 edit: ScrobbleEdit,
1173 ) -> Box<dyn crate::AsyncDiscoveryIterator<crate::ExactScrobbleEdit>> {
1174 let track_name = edit.track_name_original.clone();
1175 let album_name = edit.album_name_original.clone();
1176
1177 match (&track_name, &album_name) {
1178 (Some(track_name), Some(album_name)) => Box::new(crate::ExactMatchDiscovery::new(
1180 self.clone(),
1181 edit,
1182 track_name.clone(),
1183 album_name.clone(),
1184 )),
1185
1186 (Some(track_name), None) => Box::new(crate::TrackVariationsDiscovery::new(
1188 self.clone(),
1189 edit,
1190 track_name.clone(),
1191 )),
1192
1193 (None, Some(album_name)) => Box::new(crate::AlbumTracksDiscovery::new(
1195 self.clone(),
1196 edit,
1197 album_name.clone(),
1198 )),
1199
1200 (None, None) => Box::new(crate::ArtistTracksDiscovery::new(self.clone(), edit)),
1202 }
1203 }
1204
1205 async fn get_artist_tracks_page(&self, artist: &str, page: u32) -> Result<TrackPage> {
1206 self.get_artist_tracks_page(artist, page).await
1207 }
1208
1209 async fn get_artist_albums_page(&self, artist: &str, page: u32) -> Result<AlbumPage> {
1210 self.get_artist_albums_page(artist, page).await
1211 }
1212
1213 fn artist_tracks(&self, artist: &str) -> crate::ArtistTracksIterator {
1214 crate::ArtistTracksIterator::new(self.clone(), artist.to_string())
1215 }
1216
1217 fn artist_albums(&self, artist: &str) -> crate::ArtistAlbumsIterator {
1218 crate::ArtistAlbumsIterator::new(self.clone(), artist.to_string())
1219 }
1220
1221 fn album_tracks(&self, album_name: &str, artist_name: &str) -> crate::AlbumTracksIterator {
1222 crate::AlbumTracksIterator::new(
1223 self.clone(),
1224 album_name.to_string(),
1225 artist_name.to_string(),
1226 )
1227 }
1228
1229 fn recent_tracks(&self) -> crate::RecentTracksIterator {
1230 crate::RecentTracksIterator::new(self.clone())
1231 }
1232
1233 fn recent_tracks_from_page(&self, starting_page: u32) -> crate::RecentTracksIterator {
1234 crate::RecentTracksIterator::with_starting_page(self.clone(), starting_page)
1235 }
1236}