A decentralized music tracking and discovery platform built on AT Protocol 馃幍
at fix/spotify 875 lines 28 kB view raw
1use serde::{Deserialize, Serialize}; 2 3use crate::{musicbrainz, spotify, xata}; 4 5#[derive(Deserialize, Debug, Clone)] 6pub struct Connector { 7 pub id: String, 8 pub js: String, 9 pub label: String, 10 pub matches: Vec<String>, 11} 12 13#[derive(Deserialize, Debug, Clone)] 14#[serde(rename_all = "camelCase")] 15pub struct IsRegrexEditedByUser { 16 pub album: bool, 17 pub album_artist: bool, 18 pub artist: bool, 19 pub track: bool, 20} 21 22#[derive(Deserialize, Debug, Clone)] 23#[serde(rename_all = "camelCase")] 24pub struct Flags { 25 pub finished_processing: bool, 26 pub has_blocked_tag: bool, 27 pub is_album_fetched: bool, 28 pub is_corrected_by_user: bool, 29 pub is_loved_in_service: Option<bool>, 30 pub is_marked_as_playing: bool, 31 pub is_regex_edited_by_user: IsRegrexEditedByUser, 32 pub is_replaying: bool, 33 pub is_scrobbled: bool, 34 pub is_skipped: bool, 35 pub is_valid: bool, 36} 37 38#[derive(Deserialize, Debug, Clone)] 39#[serde(rename_all = "camelCase")] 40pub struct Metadata { 41 pub album_url: Option<String>, 42 pub artist_url: Option<String>, 43 pub label: String, 44 pub start_timestamp: u64, 45 pub track_url: Option<String>, 46 pub user_play_count: Option<u32>, 47 pub userloved: Option<bool>, 48} 49 50#[derive(Deserialize, Debug, Clone)] 51#[serde(rename_all = "camelCase")] 52pub struct NoRegex { 53 pub album: Option<String>, 54 pub album_artist: Option<String>, 55 pub artist: String, 56 pub duration: Option<u32>, 57 pub track: String, 58} 59 60#[derive(Deserialize, Debug, Clone)] 61#[serde(rename_all = "camelCase")] 62pub struct Parsed { 63 pub album: Option<String>, 64 pub album_artist: Option<String>, 65 pub artist: String, 66 pub current_time: Option<u32>, 67 pub duration: Option<u32>, 68 pub is_playing: bool, 69 pub is_podcast: bool, 70 pub origin_url: Option<String>, 71 pub scrobbling_disallowed_reason: Option<String>, 72 pub track: String, 73 pub track_art: Option<String>, 74 #[serde(rename = "uniqueID")] 75 pub unique_id: Option<String>, 76} 77 78#[derive(Deserialize, Debug, Clone)] 79#[serde(rename_all = "camelCase")] 80pub struct Song { 81 pub connector: Connector, 82 pub controller_tab_id: u64, 83 pub flags: Flags, 84 pub metadata: Metadata, 85 pub no_regex: NoRegex, 86 pub parsed: Parsed, 87} 88 89#[derive(Deserialize, Debug, Clone)] 90#[serde(rename_all = "camelCase")] 91pub struct Processed { 92 pub album: String, 93 pub album_artist: Option<String>, 94 pub artist: String, 95 pub duration: u32, 96 pub track: String, 97} 98 99#[derive(Deserialize, Debug, Clone)] 100#[serde(rename_all = "camelCase")] 101pub struct Scrobble { 102 pub song: Song, 103} 104 105#[derive(Deserialize, Debug, Clone)] 106#[serde(rename_all = "camelCase")] 107pub struct ScrobbleRequest { 108 pub data: Scrobble, 109 pub event_name: String, 110 pub time: u64, 111} 112 113#[derive(Debug, Serialize, Deserialize, Default)] 114#[serde(rename_all = "camelCase")] 115pub struct Track { 116 pub title: String, 117 pub album: String, 118 pub artist: String, 119 pub album_artist: Option<String>, 120 pub duration: u32, 121 pub mbid: Option<String>, 122 pub track_number: u32, 123 pub release_date: Option<String>, 124 pub year: Option<u32>, 125 pub disc_number: u32, 126 pub album_art: Option<String>, 127 pub spotify_link: Option<String>, 128 pub label: Option<String>, 129 pub artist_picture: Option<String>, 130 pub timestamp: Option<u64>, 131} 132 133impl From<xata::track::Track> for Track { 134 fn from(track: xata::track::Track) -> Self { 135 Track { 136 title: track.title, 137 album: track.album, 138 artist: track.artist, 139 album_artist: Some(track.album_artist), 140 album_art: track.album_art, 141 spotify_link: track.spotify_link, 142 label: track.label, 143 artist_picture: None, 144 timestamp: None, 145 duration: track.duration as u32, 146 mbid: track.mb_id, 147 track_number: track.track_number as u32, 148 disc_number: track.disc_number as u32, 149 year: None, 150 release_date: None, 151 } 152 } 153} 154 155impl From<musicbrainz::recording::Recording> for Track { 156 fn from(recording: musicbrainz::recording::Recording) -> Self { 157 let artist_credit = recording 158 .artist_credit 159 .unwrap_or_default() 160 .first() 161 .map(|credit| credit.name.clone()) 162 .unwrap_or_default(); 163 let releases = recording.releases.unwrap_or_default(); 164 let album_artist = releases 165 .first() 166 .and_then(|release| release.artist_credit.first()) 167 .map(|credit| credit.name.clone()); 168 let album = releases 169 .first() 170 .map(|release| release.title.clone()) 171 .unwrap_or_default(); 172 Track { 173 title: recording.title.clone(), 174 album, 175 artist: artist_credit, 176 album_artist, 177 duration: recording.length.unwrap_or_default(), 178 year: recording 179 .first_release_date 180 .as_ref() 181 .and_then(|date| date.split('-').next()) 182 .and_then(|year| year.parse::<u32>().ok()), 183 release_date: recording.first_release_date.clone(), 184 track_number: releases 185 .first() 186 .and_then(|release| { 187 release 188 .media 189 .as_ref() 190 .and_then(|media| media.first()) 191 .and_then(|media| { 192 media 193 .tracks 194 .as_ref() 195 .and_then(|tracks| tracks.first()) 196 .map(|track| track.number.parse::<u32>().unwrap()) 197 }) 198 }) 199 .unwrap_or_default(), 200 disc_number: releases 201 .first() 202 .and_then(|release| { 203 release 204 .media 205 .as_ref() 206 .and_then(|media| media.first()) 207 .map(|media| media.position) 208 }) 209 .unwrap_or_default(), 210 ..Default::default() 211 } 212 } 213} 214 215impl From<&spotify::types::Track> for Track { 216 fn from(track: &spotify::types::Track) -> Self { 217 Track { 218 title: track.name.clone(), 219 album: track.album.name.clone(), 220 artist: track 221 .artists 222 .iter() 223 .map(|artist| artist.name.clone()) 224 .collect::<Vec<_>>() 225 .join(", "), 226 album_artist: track 227 .album 228 .artists 229 .first() 230 .map(|artist| artist.name.clone()), 231 duration: track.duration_ms as u32, 232 album_art: track.album.images.first().map(|image| image.url.clone()), 233 spotify_link: Some(track.external_urls.spotify.clone()), 234 artist_picture: track.album.artists.first().and_then(|artist| { 235 artist 236 .images 237 .as_ref() 238 .and_then(|images| images.first().map(|image| image.url.clone())) 239 }), 240 track_number: track.track_number, 241 disc_number: track.disc_number, 242 release_date: match track.album.release_date_precision.as_str() { 243 "day" => Some(track.album.release_date.clone()), 244 _ => None, 245 }, 246 year: match track.album.release_date_precision.as_str() { 247 "day" => Some( 248 track 249 .album 250 .release_date 251 .split('-') 252 .next() 253 .unwrap() 254 .parse::<u32>() 255 .unwrap(), 256 ), 257 "year" => Some(track.album.release_date.parse::<u32>().unwrap()), 258 _ => None, 259 }, 260 label: track.album.label.clone(), 261 ..Default::default() 262 } 263 } 264} 265 266impl From<spotify::types::Track> for Track { 267 fn from(track: spotify::types::Track) -> Self { 268 Track::from(&track) 269 } 270} 271 272#[cfg(test)] 273mod tests { 274 use super::*; 275 276 #[test] 277 fn test_tidal_scrobble_request() { 278 let json = r#" 279 { 280 "data": { 281 "song": { 282 "connector": { 283 "id": "tidal", 284 "js": "tidal.js", 285 "label": "Tidal", 286 "matches": [ 287 "*://listen.tidalhifi.com/*", 288 "*://listen.tidal.com/*" 289 ] 290 }, 291 "controllerTabId": 2105806618, 292 "flags": { 293 "finishedProcessing": true, 294 "hasBlockedTag": false, 295 "isAlbumFetched": false, 296 "isCorrectedByUser": false, 297 "isLovedInService": null, 298 "isMarkedAsPlaying": true, 299 "isRegexEditedByUser": { 300 "album": false, 301 "albumArtist": false, 302 "artist": false, 303 "track": false 304 }, 305 "isReplaying": false, 306 "isScrobbled": false, 307 "isSkipped": false, 308 "isValid": true 309 }, 310 "metadata": { 311 "albumUrl": "https://www.last.fm/music/Tee+Grizzley/Forever+My+Moment+%5BClean%5D+%5BClean%5D", 312 "artistUrl": "https://www.last.fm/music/Tee+Grizzley", 313 "label": "Tidal", 314 "startTimestamp": 1747766980, 315 "trackUrl": "https://www.last.fm/music/Tee+Grizzley/_/Forever+My+Moment", 316 "userPlayCount": 0, 317 "userloved": false 318 }, 319 "noRegex": { 320 "album": "FOREVER MY MOMENT", 321 "albumArtist": null, 322 "artist": "Tee Grizzley", 323 "duration": null, 324 "track": "Forever My Moment" 325 }, 326 "parsed": { 327 "album": "FOREVER MY MOMENT", 328 "albumArtist": null, 329 "artist": "Tee Grizzley", 330 "currentTime": 17, 331 "duration": 182, 332 "isPlaying": false, 333 "isPodcast": false, 334 "originUrl": "https://listen.tidal.com/", 335 "scrobblingDisallowedReason": null, 336 "track": "Forever My Moment", 337 "trackArt": "https://resources.tidal.com/images/275251bf/9f03/46bf/9e46/3a3b0a67abe6/80x80.jpg", 338 "uniqueID": "434750253" 339 }, 340 "processed": { 341 "album": "FOREVER MY MOMENT", 342 "albumArtist": null, 343 "artist": "Tee Grizzley", 344 "duration": 182, 345 "track": "Forever My Moment" 346 } 347 } 348 }, 349 "eventName": "paused", 350 "time": 1747766997907 351 } 352 "#; 353 354 let result = serde_json::from_str::<ScrobbleRequest>(json); 355 assert!(result.is_ok(), "Failed to parse JSON: {:?}", result.err()); 356 } 357 358 #[test] 359 fn test_spotify_nowplaying_request() { 360 let json = r#" 361 { 362 "data": { 363 "song": { 364 "connector": { 365 "hasNativeScrobbler": true, 366 "id": "spotify", 367 "js": "spotify.js", 368 "label": "Spotify", 369 "matches": [ 370 "*://open.spotify.com/*" 371 ] 372 }, 373 "controllerTabId": 2105804433, 374 "flags": { 375 "finishedProcessing": true, 376 "hasBlockedTag": false, 377 "isAlbumFetched": false, 378 "isCorrectedByUser": false, 379 "isLovedInService": null, 380 "isMarkedAsPlaying": true, 381 "isRegexEditedByUser": { 382 "album": false, 383 "albumArtist": false, 384 "artist": false, 385 "track": false 386 }, 387 "isReplaying": false, 388 "isScrobbled": false, 389 "isSkipped": false, 390 "isValid": true 391 }, 392 "metadata": { 393 "albumUrl": "https://www.last.fm/music/The+Weeknd/Hurry+Up+Tomorrow+(First+Press)", 394 "artistUrl": "https://www.last.fm/music/The+Weeknd", 395 "label": "Spotify", 396 "startTimestamp": 1747753805, 397 "trackArtUrl": "https://lastfm.freetls.fastly.net/i/u/300x300/eadb0529b2c5066ebe7f53c52e329def.png", 398 "trackUrl": "https://www.last.fm/music/The+Weeknd/_/Given+Up+on+Me", 399 "userPlayCount": 0, 400 "userloved": false 401 }, 402 "noRegex": { 403 "album": "Hurry Up Tomorrow", 404 "albumArtist": null, 405 "artist": "The Weeknd", 406 "duration": null, 407 "track": "Given Up On Me" 408 }, 409 "parsed": { 410 "album": "Hurry Up Tomorrow", 411 "albumArtist": null, 412 "artist": "The Weeknd", 413 "currentTime": null, 414 "duration": 354, 415 "isPlaying": true, 416 "isPodcast": false, 417 "originUrl": null, 418 "scrobblingDisallowedReason": null, 419 "track": "Given Up On Me", 420 "trackArt": "https://i.scdn.co/image/ab67616d00001e02982320da137d0de34410df61", 421 "uniqueID": null 422 }, 423 "processed": { 424 "album": "Hurry Up Tomorrow", 425 "albumArtist": null, 426 "artist": "The Weeknd", 427 "duration": 354, 428 "track": "Given Up on Me" 429 } 430 } 431 }, 432 "eventName": "nowplaying", 433 "time": 1747753806195 434 } 435 "#; 436 437 let result = serde_json::from_str::<ScrobbleRequest>(json); 438 assert!(result.is_ok(), "Failed to parse JSON: {:?}", result.err()); 439 } 440 441 #[test] 442 fn test_spotify_scrobble_request() { 443 let json = r#" 444 { 445 "data": { 446 "currentlyPlaying": true, 447 "song": { 448 "connector": { 449 "hasNativeScrobbler": true, 450 "id": "spotify", 451 "js": "spotify.js", 452 "label": "Spotify", 453 "matches": [ 454 "*://open.spotify.com/*" 455 ] 456 }, 457 "controllerTabId": 2105804433, 458 "flags": { 459 "finishedProcessing": true, 460 "hasBlockedTag": false, 461 "isAlbumFetched": false, 462 "isCorrectedByUser": false, 463 "isLovedInService": null, 464 "isMarkedAsPlaying": true, 465 "isRegexEditedByUser": { 466 "album": false, 467 "albumArtist": false, 468 "artist": false, 469 "track": false 470 }, 471 "isReplaying": false, 472 "isScrobbled": false, 473 "isSkipped": false, 474 "isValid": true 475 }, 476 "metadata": { 477 "artistUrl": "https://www.last.fm/music/VIZE,+Tom+Gregory", 478 "label": "Spotify", 479 "startTimestamp": 1747753624, 480 "trackUrl": "https://www.last.fm/music/VIZE,+Tom+Gregory/_/Never+Let+Me+Down", 481 "userPlayCount": 0, 482 "userloved": false 483 }, 484 "noRegex": { 485 "album": "Never Let Me Down", 486 "albumArtist": null, 487 "artist": "VIZE, Tom Gregory", 488 "duration": null, 489 "track": "Never Let Me Down" 490 }, 491 "parsed": { 492 "album": "Never Let Me Down", 493 "albumArtist": null, 494 "artist": "VIZE, Tom Gregory", 495 "currentTime": 76, 496 "duration": 153, 497 "isPlaying": true, 498 "isPodcast": false, 499 "originUrl": null, 500 "scrobblingDisallowedReason": null, 501 "track": "Never Let Me Down", 502 "trackArt": "https://i.scdn.co/image/ab67616d00001e02e33c4ba1bf5eecbbc7dddc85", 503 "uniqueID": null 504 }, 505 "processed": { 506 "album": "Never Let Me Down", 507 "albumArtist": null, 508 "artist": "VIZE, Tom Gregory", 509 "duration": 153, 510 "track": "Never Let Me Down" 511 } 512 }, 513 "songs": [ 514 { 515 "connector": { 516 "hasNativeScrobbler": true, 517 "id": "spotify", 518 "js": "spotify.js", 519 "label": "Spotify", 520 "matches": [ 521 "*://open.spotify.com/*" 522 ] 523 }, 524 "controllerTabId": 2105804433, 525 "flags": { 526 "finishedProcessing": true, 527 "hasBlockedTag": false, 528 "isAlbumFetched": false, 529 "isCorrectedByUser": false, 530 "isLovedInService": null, 531 "isMarkedAsPlaying": true, 532 "isRegexEditedByUser": { 533 "album": false, 534 "albumArtist": false, 535 "artist": false, 536 "track": false 537 }, 538 "isReplaying": false, 539 "isScrobbled": false, 540 "isSkipped": false, 541 "isValid": true 542 }, 543 "metadata": { 544 "artistUrl": "https://www.last.fm/music/VIZE,+Tom+Gregory", 545 "label": "Spotify", 546 "startTimestamp": 1747753624, 547 "trackUrl": "https://www.last.fm/music/VIZE,+Tom+Gregory/_/Never+Let+Me+Down", 548 "userPlayCount": 0, 549 "userloved": false 550 }, 551 "noRegex": { 552 "album": "Never Let Me Down", 553 "albumArtist": null, 554 "artist": "VIZE, Tom Gregory", 555 "duration": null, 556 "track": "Never Let Me Down" 557 }, 558 "parsed": { 559 "album": "Never Let Me Down", 560 "albumArtist": null, 561 "artist": "VIZE, Tom Gregory", 562 "currentTime": 76, 563 "duration": 153, 564 "isPlaying": true, 565 "isPodcast": false, 566 "originUrl": null, 567 "scrobblingDisallowedReason": null, 568 "track": "Never Let Me Down", 569 "trackArt": "https://i.scdn.co/image/ab67616d00001e02e33c4ba1bf5eecbbc7dddc85", 570 "uniqueID": null 571 }, 572 "processed": { 573 "album": "Never Let Me Down", 574 "albumArtist": null, 575 "artist": "VIZE, Tom Gregory", 576 "duration": 153, 577 "track": "Never Let Me Down" 578 } 579 } 580 ] 581 }, 582 "eventName": "scrobble", 583 "time": 1747753702338 584 } 585 "#; 586 587 let result = serde_json::from_str::<ScrobbleRequest>(json); 588 assert!(result.is_ok(), "Failed to parse JSON: {:?}", result.err()); 589 } 590 591 #[test] 592 fn test_youtube_scrobble_request() { 593 let json = r#" 594 { 595 "eventName": "nowplaying", 596 "time": 1747899797294, 597 "data": { 598 "song": { 599 "parsed": { 600 "track": "Let It Talk To Me", 601 "artist": "Sean Paul x INNA", 602 "albumArtist": null, 603 "album": null, 604 "duration": 155, 605 "uniqueID": "nkRyAVQdqAA", 606 "currentTime": 11, 607 "isPlaying": true, 608 "isPodcast": false, 609 "originUrl": "https://youtu.be/nkRyAVQdqAA", 610 "scrobblingDisallowedReason": null, 611 "trackArt": null 612 }, 613 "processed": { 614 "track": "Let It Talk To Me", 615 "artist": "Sean Paul x INNA", 616 "albumArtist": null, 617 "duration": 154.661 618 }, 619 "noRegex": { 620 "track": "Let It Talk To Me", 621 "artist": "Sean Paul x INNA", 622 "albumArtist": null, 623 "duration": null 624 }, 625 "flags": { 626 "isScrobbled": false, 627 "isCorrectedByUser": false, 628 "isRegexEditedByUser": { 629 "track": false, 630 "artist": false, 631 "album": false, 632 "albumArtist": false 633 }, 634 "isAlbumFetched": true, 635 "isValid": true, 636 "isMarkedAsPlaying": true, 637 "isSkipped": false, 638 "isReplaying": false, 639 "hasBlockedTag": false, 640 "isLovedInService": null, 641 "finishedProcessing": true 642 }, 643 "metadata": { 644 "userloved": false, 645 "startTimestamp": 1747899788, 646 "label": "YouTube", 647 "trackArtUrl": "https://coverartarchive.org/release/b74fe4b2-d633-4607-af93-b277b8b6a6b6/front-500", 648 "artistUrl": "https://www.last.fm/music/Sean+Paul+x+INNA", 649 "trackUrl": "https://www.last.fm/music/Sean+Paul+x+INNA/_/Let+It+Talk+To+Me", 650 "userPlayCount": 0 651 }, 652 "connector": { 653 "label": "YouTube", 654 "matches": [ 655 "*://www.youtube.com/*", 656 "*://m.youtube.com/*" 657 ], 658 "js": "youtube.js", 659 "id": "youtube", 660 "usesBlocklist": true 661 }, 662 "controllerTabId": 2105807456 663 } 664 } 665} 666 "#; 667 668 let result = serde_json::from_str::<ScrobbleRequest>(json); 669 assert!(result.is_ok(), "Failed to parse JSON: {:?}", result.err()); 670 } 671 672 #[test] 673 fn test_artists_parsing() { 674 let track = "Let It Talk To Me"; 675 let artist = " Sean Paul x INNA"; 676 let query = match artist.contains(" x ") { 677 true => { 678 let artists = artist 679 .split(" x ") 680 .map(|a| format!(r#"artist:"{}""#, a.trim())) 681 .collect::<Vec<_>>() 682 .join(" "); 683 format!(r#"track:"{}" {}"#, track, artists) 684 } 685 false => format!(r#"track:"{}" artist:"{}""#, track, artist.trim()), 686 }; 687 688 assert_eq!( 689 query, 690 r#"track:"Let It Talk To Me" artist:"Sean Paul" artist:"INNA""# 691 ); 692 693 let artist = "Sean Paul, INNA"; 694 let track = "Let It Talk To Me"; 695 match artist.contains(", ") { 696 true => { 697 let artists = artist 698 .split(", ") 699 .map(|a| format!(r#"artist:"{}""#, a.trim())) 700 .collect::<Vec<_>>() 701 .join(" "); 702 format!(r#"track:"{}" {}"#, track, artists) 703 } 704 false => format!(r#"track:"{}" artist:"{}""#, track, artist.trim()), 705 }; 706 707 assert_eq!( 708 query, 709 r#"track:"Let It Talk To Me" artist:"Sean Paul" artist:"INNA""# 710 ); 711 } 712 713 #[test] 714 fn test_kexp_scrobble_request() { 715 let json = r#" 716 { 717 "eventName": "nowplaying", 718 "time": 1749848844651, 719 "data": { 720 "song": { 721 "parsed": { 722 "track": "ENERGY", 723 "artist": "Disclosure", 724 "albumArtist": null, 725 "album": "ENERGY", 726 "duration": null, 727 "uniqueID": null, 728 "currentTime": null, 729 "isPlaying": true, 730 "trackArt": "https://ia803209.us.archive.org/7/items/mbid-6f1db1e3-71b3-4524-b0fe-c1e29f361dfe/mbid-6f1db1e3-71b3-4524-b0fe-c1e29f361dfe-26365923265_thumb250.jpg", 731 "isPodcast": false, 732 "originUrl": "https://www.kexp.org/", 733 "scrobblingDisallowedReason": null 734 }, 735 "processed": { 736 "track": "ENERGY", 737 "artist": "Disclosure", 738 "albumArtist": null, 739 "album": "ENERGY", 740 "duration": null 741 }, 742 "noRegex": { 743 "track": "ENERGY", 744 "artist": "Disclosure", 745 "albumArtist": null, 746 "album": "ENERGY", 747 "duration": null 748 }, 749 "flags": { 750 "isScrobbled": false, 751 "isCorrectedByUser": false, 752 "isRegexEditedByUser": { 753 "track": false, 754 "artist": false, 755 "album": false, 756 "albumArtist": false 757 }, 758 "isAlbumFetched": false, 759 "isValid": true, 760 "isMarkedAsPlaying": true, 761 "isSkipped": false, 762 "isReplaying": true, 763 "hasBlockedTag": false, 764 "isLovedInService": null, 765 "finishedProcessing": true 766 }, 767 "metadata": { 768 "userloved": false, 769 "startTimestamp": 1749848842, 770 "label": "KEXP", 771 "trackArtUrl": "https://lastfm.freetls.fastly.net/i/u/300x300/a18f5dadcc38ad3d264e74ce51d9cf08.png", 772 "artistUrl": "https://www.last.fm/music/Disclosure", 773 "trackUrl": "https://www.last.fm/music/Disclosure/_/ENERGY", 774 "albumUrl": "https://www.last.fm/music/Various+Artists/Festival+Dance", 775 "userPlayCount": 0 776 }, 777 "connector": { 778 "label": "KEXP", 779 "matches": [ 780 "*://*.kexp.org/*" 781 ], 782 "js": "kexp.js", 783 "id": "kexp" 784 }, 785 "controllerTabId": 943739308 786 } 787 } 788} 789 "#; 790 791 let result = serde_json::from_str::<ScrobbleRequest>(json); 792 assert!(result.is_ok(), "Failed to parse JSON: {:?}", result.err()); 793 } 794 795 #[test] 796 fn test_deezer_scrobble_request() { 797 let json = r#" 798 { 799 "eventName": "nowplaying", 800 "time": 1747899797294, 801 "data": { 802 "song": { 803 "parsed": { 804 "track": "Exile Is A Habit", 805 "artist": "Fox Stevenson", 806 "albumArtist": null, 807 "album": "Sunk Cost Fallacy", 808 "duration": 261, 809 "uniqueID": "3382194151", 810 "currentTime": null, 811 "isPlaying": true, 812 "trackArt": "https://cdn-images.dzcdn.net/images/cover/f8f25df28395fcc1b036982ad1475737/500x500.jpg", 813 "isPodcast": false, 814 "originUrl": "https://www.deezer.com/en/album/761728451", 815 "scrobblingDisallowedReason": null 816 }, 817 "processed": { 818 "track": "Exile Is A Habit", 819 "artist": "Fox Stevenson", 820 "albumArtist": null, 821 "album": "Sunk Cost Fallacy", 822 "duration": 258 823 }, 824 "noRegex": { 825 "track": "Exile Is A Habit", 826 "artist": "Fox Stevenson", 827 "albumArtist": null, 828 "album": "Sunk Cost Fallacy", 829 "duration": null 830 }, 831 "flags": { 832 "isScrobbled": false, 833 "isCorrectedByUser": false, 834 "isRegexEditedByUser": { 835 "track": false, 836 "artist": false, 837 "album": false, 838 "albumArtist": false 839 }, 840 "isAlbumFetched": false, 841 "isValid": true, 842 "isMarkedAsPlaying": false, 843 "isSkipped": false, 844 "isReplaying": false, 845 "hasBlockedTag": false, 846 "isLovedInService": null, 847 "finishedProcessing": true 848 }, 849 "metadata": { 850 "startTimestamp": 1754673593, 851 "label": "Deezer", 852 "trackArtUrl": "https://lastfm.freetls.fastly.net/i/u/300x300/b3eee782a80469085b2105fc63f0e534.png", 853 "artistUrl": "https://www.last.fm/music/Fox+Stevenson", 854 "trackUrl": "https://www.last.fm/music/Fox+Stevenson/_/Exile+Is+A+Habit", 855 "albumUrl": "https://www.last.fm/music/Fox+Stevenson/Exile+Is+A+Habit", 856 "userPlayCount": 0 857 }, 858 "connector": { 859 "label": "Deezer", 860 "matches": [ 861 "*://www.deezer.com/*" 862 ], 863 "js": "deezer.js", 864 "id": "deezer" 865 }, 866 "controllerTabId": 2105807456 867 } 868 } 869} 870 "#; 871 872 let result = serde_json::from_str::<ScrobbleRequest>(json); 873 assert!(result.is_ok(), "Failed to parse JSON: {:?}", result.err()); 874 } 875}