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