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