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