Skip to main content

lastfm_edit/
client.rs

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    /// Custom URL encoding for Last.fm paths
31    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", &timestamp.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        // Skip retry if disabled in config or max_retries is 0
569        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, // No specific request context in retry callback
600                    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, // No specific request context in retry callback
1073                    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        // Debug logging for albums that return 0 tracks
1363        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        // For search results, we need to determine pagination differently
1417        // since we don't have the same pagination structure as regular library pages
1418        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        // For search results, we need to determine pagination differently
1457        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        // For search results, we need to determine pagination differently
1496        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    /// Expose the inner HTTP client for advanced use cases like VCR cassette management
1507    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}