1use crate::{Album, AlbumPage, LastFmError, Result, Track, TrackPage};
8use scraper::{Html, Selector};
9
10#[derive(Debug, Clone)]
15pub struct LastFmParser;
16
17impl LastFmParser {
18 pub fn new() -> Self {
20 Self
21 }
22
23 pub fn parse_recent_scrobbles(&self, document: &Html) -> Result<Vec<Track>> {
26 let mut tracks = Vec::new();
27
28 let table_selector = Selector::parse("table.chartlist").unwrap();
30 let row_selector = Selector::parse("tbody tr").unwrap();
31
32 let tables: Vec<_> = document.select(&table_selector).collect();
33 log::debug!("Found {} chartlist tables", tables.len());
34
35 for table in tables {
36 for row in table.select(&row_selector) {
37 if let Ok(track) = self.parse_recent_scrobble_row(&row) {
38 tracks.push(track);
39 }
40 }
41 }
42
43 if tracks.is_empty() {
44 log::debug!("No tracks found in recent scrobbles");
45 }
46
47 log::debug!("Parsed {} recent scrobbles", tracks.len());
48 Ok(tracks)
49 }
50
51 fn parse_recent_scrobble_row(&self, row: &scraper::ElementRef) -> Result<Track> {
53 let name_selector = Selector::parse(".chartlist-name a").unwrap();
55 let name = row
56 .select(&name_selector)
57 .next()
58 .ok_or(LastFmError::Parse("Missing track name".to_string()))?
59 .text()
60 .collect::<String>()
61 .trim()
62 .to_string();
63
64 let artist_selector = Selector::parse(".chartlist-artist a").unwrap();
66 let artist = row
67 .select(&artist_selector)
68 .next()
69 .ok_or(LastFmError::Parse("Missing artist name".to_string()))?
70 .text()
71 .collect::<String>()
72 .trim()
73 .to_string();
74
75 let timestamp = self.extract_scrobble_timestamp(row);
77
78 let album = self.extract_scrobble_album(row);
80
81 let album_artist = self.extract_scrobble_album_artist(row);
83
84 let playcount = 1;
86
87 Ok(Track {
88 name,
89 artist,
90 playcount,
91 timestamp,
92 album,
93 album_artist,
94 })
95 }
96
97 fn extract_scrobble_timestamp(&self, row: &scraper::ElementRef) -> Option<u64> {
99 if let Some(timestamp_str) = row.value().attr("data-timestamp") {
103 if let Ok(timestamp) = timestamp_str.parse::<u64>() {
104 return Some(timestamp);
105 }
106 }
107
108 let timestamp_input_selector = Selector::parse("input[name='timestamp']").unwrap();
110 if let Some(input) = row.select(×tamp_input_selector).next() {
111 if let Some(value) = input.value().attr("value") {
112 if let Ok(timestamp) = value.parse::<u64>() {
113 return Some(timestamp);
114 }
115 }
116 }
117
118 let edit_form_selector =
120 Selector::parse("form[data-edit-scrobble] input[name='timestamp']").unwrap();
121 if let Some(timestamp_input) = row.select(&edit_form_selector).next() {
122 if let Some(value) = timestamp_input.value().attr("value") {
123 if let Ok(timestamp) = value.parse::<u64>() {
124 return Some(timestamp);
125 }
126 }
127 }
128
129 let time_selector = Selector::parse("time").unwrap();
131 if let Some(time_elem) = row.select(&time_selector).next() {
132 if let Some(datetime) = time_elem.value().attr("datetime") {
133 if let Ok(parsed_time) = chrono::DateTime::parse_from_rfc3339(datetime) {
135 return Some(parsed_time.timestamp() as u64);
136 }
137 }
138 }
139
140 None
141 }
142
143 fn extract_scrobble_album(&self, row: &scraper::ElementRef) -> Option<String> {
145 let album_input_selector =
147 Selector::parse("form[data-edit-scrobble] input[name='album_name']").unwrap();
148
149 if let Some(album_input) = row.select(&album_input_selector).next() {
150 if let Some(album_name) = album_input.value().attr("value") {
151 if !album_name.is_empty() {
152 return Some(album_name.to_string());
153 }
154 }
155 }
156
157 None
158 }
159
160 fn extract_scrobble_album_artist(&self, row: &scraper::ElementRef) -> Option<String> {
162 let album_artist_input_selector =
164 Selector::parse("form[data-edit-scrobble] input[name='album_artist_name']").unwrap();
165
166 if let Some(album_artist_input) = row.select(&album_artist_input_selector).next() {
167 if let Some(album_artist_name) = album_artist_input.value().attr("value") {
168 if !album_artist_name.is_empty() {
169 return Some(album_artist_name.to_string());
170 }
171 }
172 }
173
174 None
175 }
176
177 pub fn parse_tracks_page(
179 &self,
180 document: &Html,
181 page_number: u32,
182 artist: &str,
183 album: Option<&str>,
184 ) -> Result<TrackPage> {
185 let tracks = self.extract_tracks_from_document(document, artist, album)?;
186
187 let (has_next_page, total_pages) = self.parse_pagination(document, page_number)?;
189
190 Ok(TrackPage {
191 tracks,
192 page_number,
193 has_next_page,
194 total_pages,
195 })
196 }
197
198 pub fn extract_tracks_from_document(
200 &self,
201 document: &Html,
202 artist: &str,
203 album: Option<&str>,
204 ) -> Result<Vec<Track>> {
205 let mut tracks = Vec::new();
206 let mut seen_tracks = std::collections::HashSet::new();
207
208 if let Ok(json_tracks) = self.parse_json_tracks_page(document, 1, artist, album) {
210 return Ok(json_tracks.tracks);
211 }
212
213 let track_selector = Selector::parse("[data-track-name]").unwrap();
215 let track_elements: Vec<_> = document.select(&track_selector).collect();
216
217 if !track_elements.is_empty() {
218 for element in track_elements {
219 let track_name = element.value().attr("data-track-name").unwrap_or("");
220 if !track_name.is_empty() && !seen_tracks.contains(track_name) {
221 seen_tracks.insert(track_name.to_string());
222
223 if let Ok(playcount) = self.find_playcount_for_track(document, track_name) {
224 let timestamp = self.find_timestamp_for_track(document, track_name);
225 let track = Track {
226 name: track_name.to_string(),
227 artist: artist.to_string(),
228 playcount,
229 timestamp,
230 album: album.map(|a| a.to_string()),
231 album_artist: None, };
233 tracks.push(track);
234 }
235 if tracks.len() >= 50 {
236 break;
237 }
238 }
239 }
240 }
241
242 if tracks.len() < 50 {
244 let form_input_selector = Selector::parse("input[name='track']").unwrap();
245 for input in document.select(&form_input_selector) {
246 if let Some(track_name) = input.value().attr("value") {
247 if !track_name.is_empty() && !seen_tracks.contains(track_name) {
248 seen_tracks.insert(track_name.to_string());
249
250 let playcount = self
251 .find_playcount_for_track(document, track_name)
252 .unwrap_or(0);
253 let timestamp = self.find_timestamp_for_track(document, track_name);
254 let track = Track {
255 name: track_name.to_string(),
256 artist: artist.to_string(),
257 playcount,
258 timestamp,
259 album: album.map(|a| a.to_string()),
260 album_artist: None, };
262 tracks.push(track);
263 if tracks.len() >= 50 {
264 break;
265 }
266 }
267 }
268 }
269 }
270
271 if tracks.len() < 10 {
273 let table_tracks = self.parse_tracks_from_rows(document, artist, album)?;
274 for track in table_tracks {
275 if !seen_tracks.contains(&track.name) && tracks.len() < 50 {
276 seen_tracks.insert(track.name.clone());
277 tracks.push(track);
278 }
279 }
280 }
281
282 log::debug!("Successfully extracted {} unique tracks", tracks.len());
283 Ok(tracks)
284 }
285
286 fn parse_tracks_from_rows(
288 &self,
289 document: &Html,
290 artist: &str,
291 album: Option<&str>,
292 ) -> Result<Vec<Track>> {
293 let mut tracks = Vec::new();
294 let table_selector = Selector::parse("table.chartlist").unwrap();
295 let row_selector = Selector::parse("tbody tr").unwrap();
296
297 for table in document.select(&table_selector) {
298 for row in table.select(&row_selector) {
299 if let Ok(mut track) = self.parse_track_row(&row) {
300 track.artist = artist.to_string(); track.album = album.map(|a| a.to_string()); tracks.push(track);
303 }
304 }
305 }
306 Ok(tracks)
307 }
308
309 pub fn parse_track_row(&self, row: &scraper::ElementRef) -> Result<Track> {
311 let name = self.extract_name_from_row(row, "track")?;
313
314 let playcount = self.extract_playcount_from_row(row);
316
317 let artist = "".to_string(); Ok(Track {
320 name,
321 artist,
322 playcount,
323 timestamp: None, album: None, album_artist: None, })
327 }
328
329 pub fn parse_albums_page(
331 &self,
332 document: &Html,
333 page_number: u32,
334 artist: &str,
335 ) -> Result<AlbumPage> {
336 let mut albums = Vec::new();
337
338 let album_selector = Selector::parse("[data-album-name]").unwrap();
340 let album_elements: Vec<_> = document.select(&album_selector).collect();
341
342 if !album_elements.is_empty() {
343 log::debug!(
344 "Found {} album elements with data-album-name",
345 album_elements.len()
346 );
347
348 let mut seen_albums = std::collections::HashSet::new();
350
351 for element in album_elements {
352 let album_name = element.value().attr("data-album-name").unwrap_or("");
353 if !album_name.is_empty() && !seen_albums.contains(album_name) {
354 seen_albums.insert(album_name.to_string());
355
356 if let Ok(playcount) = self.find_playcount_for_album(document, album_name) {
357 let timestamp = self.find_timestamp_for_album(document, album_name);
358 let album = Album {
359 name: album_name.to_string(),
360 artist: artist.to_string(),
361 playcount,
362 timestamp,
363 };
364 albums.push(album);
365 }
366
367 if albums.len() >= 50 {
368 break;
369 }
370 }
371 }
372 } else {
373 albums = self.parse_albums_from_rows(document, artist)?;
375 }
376
377 let (has_next_page, total_pages) = self.parse_pagination(document, page_number)?;
378
379 Ok(AlbumPage {
380 albums,
381 page_number,
382 has_next_page,
383 total_pages,
384 })
385 }
386
387 fn parse_albums_from_rows(&self, document: &Html, artist: &str) -> Result<Vec<Album>> {
389 let mut albums = Vec::new();
390 let table_selector = Selector::parse("table.chartlist").unwrap();
391 let row_selector = Selector::parse("tbody tr").unwrap();
392
393 for table in document.select(&table_selector) {
394 for row in table.select(&row_selector) {
395 if let Ok(mut album) = self.parse_album_row(&row) {
396 album.artist = artist.to_string();
397 albums.push(album);
398 }
399 }
400 }
401 Ok(albums)
402 }
403
404 pub fn parse_album_row(&self, row: &scraper::ElementRef) -> Result<Album> {
406 let name = self.extract_name_from_row(row, "album")?;
408
409 let playcount = self.extract_playcount_from_row(row);
411
412 let artist = "".to_string(); Ok(Album {
415 name,
416 artist,
417 playcount,
418 timestamp: None, })
420 }
421
422 pub fn parse_track_search_results(&self, document: &Html) -> Result<Vec<Track>> {
429 let mut tracks = Vec::new();
430
431 let table_selector = Selector::parse("table.chartlist").unwrap();
433 let row_selector = Selector::parse("tbody tr").unwrap();
434
435 let tables: Vec<_> = document.select(&table_selector).collect();
436 log::debug!("Found {} chartlist tables in search results", tables.len());
437
438 for table in tables {
439 for row in table.select(&row_selector) {
440 if let Ok(track) = self.parse_search_track_row(&row) {
441 tracks.push(track);
442 }
443 }
444 }
445
446 log::debug!("Parsed {} tracks from search results", tracks.len());
447 Ok(tracks)
448 }
449
450 pub fn parse_album_search_results(&self, document: &Html) -> Result<Vec<Album>> {
455 let mut albums = Vec::new();
456
457 let table_selector = Selector::parse("table.chartlist").unwrap();
459 let row_selector = Selector::parse("tbody tr").unwrap();
460
461 let tables: Vec<_> = document.select(&table_selector).collect();
462 log::debug!(
463 "Found {} chartlist tables in album search results",
464 tables.len()
465 );
466
467 for table in tables {
468 for row in table.select(&row_selector) {
469 if let Ok(album) = self.parse_search_album_row(&row) {
470 albums.push(album);
471 }
472 }
473 }
474
475 log::debug!("Parsed {} albums from search results", albums.len());
476 Ok(albums)
477 }
478
479 fn parse_search_track_row(&self, row: &scraper::ElementRef) -> Result<Track> {
481 let name = self.extract_name_from_row(row, "track")?;
483
484 let artist_selector = Selector::parse(".chartlist-artist a").unwrap();
486 let artist = row
487 .select(&artist_selector)
488 .next()
489 .map(|el| el.text().collect::<String>().trim().to_string())
490 .ok_or_else(|| {
491 LastFmError::Parse("Missing artist name in search results".to_string())
492 })?;
493
494 let playcount = self.extract_playcount_from_row(row);
496
497 let timestamp = None;
499
500 let album = self.extract_album_from_search_row(row);
502 let album_artist = self.extract_album_artist_from_search_row(row);
503
504 Ok(Track {
505 name,
506 artist,
507 playcount,
508 timestamp,
509 album,
510 album_artist,
511 })
512 }
513
514 fn parse_search_album_row(&self, row: &scraper::ElementRef) -> Result<Album> {
516 let name = self.extract_name_from_row(row, "album")?;
518
519 let artist_selector = Selector::parse(".chartlist-artist a").unwrap();
521 let artist = row
522 .select(&artist_selector)
523 .next()
524 .map(|el| el.text().collect::<String>().trim().to_string())
525 .ok_or_else(|| {
526 LastFmError::Parse("Missing artist name in album search results".to_string())
527 })?;
528
529 let playcount = self.extract_playcount_from_row(row);
531
532 Ok(Album {
533 name,
534 artist,
535 playcount,
536 timestamp: None, })
538 }
539
540 fn extract_album_from_search_row(&self, row: &scraper::ElementRef) -> Option<String> {
542 let album_input_selector = Selector::parse("input[name='album']").unwrap();
544 if let Some(input) = row.select(&album_input_selector).next() {
545 if let Some(value) = input.value().attr("value") {
546 let album = value.trim().to_string();
547 if !album.is_empty() {
548 return Some(album);
549 }
550 }
551 }
552 None
553 }
554
555 fn extract_album_artist_from_search_row(&self, row: &scraper::ElementRef) -> Option<String> {
557 let album_artist_input_selector = Selector::parse("input[name='album_artist']").unwrap();
559 if let Some(input) = row.select(&album_artist_input_selector).next() {
560 if let Some(value) = input.value().attr("value") {
561 let album_artist = value.trim().to_string();
562 if !album_artist.is_empty() {
563 return Some(album_artist);
564 }
565 }
566 }
567 None
568 }
569
570 fn extract_name_from_row(&self, row: &scraper::ElementRef, item_type: &str) -> Result<String> {
574 let name_selector = Selector::parse(".chartlist-name a").unwrap();
575 let name = row
576 .select(&name_selector)
577 .next()
578 .map(|el| el.text().collect::<String>().trim().to_string())
579 .ok_or_else(|| LastFmError::Parse(format!("Missing {item_type} name")))?;
580 Ok(name)
581 }
582
583 fn extract_playcount_from_row(&self, row: &scraper::ElementRef) -> u32 {
585 let playcount_selector = Selector::parse(".chartlist-count-bar-value").unwrap();
586 let mut playcount = 1; if let Some(element) = row.select(&playcount_selector).next() {
589 let text = element.text().collect::<String>().trim().to_string();
590 if let Some(number_part) = text.split_whitespace().next() {
592 if let Ok(count) = number_part.parse::<u32>() {
593 playcount = count;
594 }
595 }
596 }
597 playcount
598 }
599
600 pub fn parse_pagination(
602 &self,
603 document: &Html,
604 _current_page: u32,
605 ) -> Result<(bool, Option<u32>)> {
606 let pagination_selector = Selector::parse(".pagination-list").unwrap();
607
608 if let Some(pagination) = document.select(&pagination_selector).next() {
609 let next_selectors = [
611 "a[aria-label=\"Next\"]",
612 ".pagination-next a",
613 "a:contains(\"Next\")",
614 ".next a",
615 ];
616
617 let mut has_next = false;
618 for selector_str in &next_selectors {
619 if let Ok(selector) = Selector::parse(selector_str) {
620 if pagination.select(&selector).next().is_some() {
621 has_next = true;
622 break;
623 }
624 }
625 }
626
627 let total_pages = self.extract_total_pages_from_pagination(&pagination);
629
630 Ok((has_next, total_pages))
631 } else {
632 Ok((false, Some(1)))
634 }
635 }
636
637 fn extract_total_pages_from_pagination(&self, pagination: &scraper::ElementRef) -> Option<u32> {
639 let text = pagination.text().collect::<String>();
641 if let Some(of_pos) = text.find(" of ") {
642 let after_of = &text[of_pos + 4..];
643 if let Some(number_end) = after_of.find(|c: char| !c.is_ascii_digit()) {
644 if let Ok(total) = after_of[..number_end].parse::<u32>() {
645 return Some(total);
646 }
647 } else if let Ok(total) = after_of.trim().parse::<u32>() {
648 return Some(total);
649 }
650 }
651 None
652 }
653
654 fn parse_json_tracks_page(
657 &self,
658 _document: &Html,
659 _page: u32,
660 _artist: &str,
661 _album: Option<&str>,
662 ) -> Result<TrackPage> {
663 Err(crate::LastFmError::Parse(
665 "JSON parsing not implemented".to_string(),
666 ))
667 }
668
669 pub fn find_timestamp_for_track(&self, _document: &Html, _track_name: &str) -> Option<u64> {
672 None
674 }
675
676 pub fn find_playcount_for_track(&self, document: &Html, track_name: &str) -> Result<u32> {
677 let count_selector = Selector::parse(".chartlist-count-bar-value").unwrap();
679 let link_selector = Selector::parse("a[href*=\"/music/\"]").unwrap();
680
681 for link in document.select(&link_selector) {
683 let link_text = link.text().collect::<String>().trim().to_string();
684 if link_text == track_name {
685 if let Some(row) = self.find_ancestor_row(link) {
686 if let Some(count_element) = row.select(&count_selector).next() {
687 let text = count_element.text().collect::<String>().trim().to_string();
688 if let Some(number_part) = text.split_whitespace().next() {
689 if let Ok(count) = number_part.parse::<u32>() {
690 return Ok(count);
691 }
692 }
693 }
694 }
695 }
696 }
697 Err(LastFmError::Parse(format!(
698 "Could not find playcount for track: {track_name}"
699 )))
700 }
701
702 pub fn find_timestamp_for_album(&self, _document: &Html, _album_name: &str) -> Option<u64> {
703 None
705 }
706
707 pub fn find_playcount_for_album(&self, document: &Html, album_name: &str) -> Result<u32> {
708 let count_selector = Selector::parse(".chartlist-count-bar-value").unwrap();
710 let link_selector = Selector::parse("a[href*=\"/music/\"]").unwrap();
711
712 for link in document.select(&link_selector) {
714 let link_text = link.text().collect::<String>().trim().to_string();
715 if link_text == album_name {
716 if let Some(row) = self.find_ancestor_row(link) {
717 if let Some(count_element) = row.select(&count_selector).next() {
718 let text = count_element.text().collect::<String>().trim().to_string();
719 if let Some(number_part) = text.split_whitespace().next() {
720 if let Ok(count) = number_part.parse::<u32>() {
721 return Ok(count);
722 }
723 }
724 }
725 }
726 }
727 }
728 Err(LastFmError::Parse(format!(
729 "Could not find playcount for album: {album_name}"
730 )))
731 }
732
733 pub fn find_ancestor_row<'a>(
734 &self,
735 element: scraper::ElementRef<'a>,
736 ) -> Option<scraper::ElementRef<'a>> {
737 let mut current = element;
738 while let Some(parent) = current.parent() {
739 if let Some(parent_elem) = scraper::ElementRef::wrap(parent) {
740 if parent_elem.value().name() == "tr" {
741 return Some(parent_elem);
742 }
743 current = parent_elem;
744 } else {
745 break;
746 }
747 }
748 None
749 }
750}
751
752impl Default for LastFmParser {
753 fn default() -> Self {
754 Self::new()
755 }
756}