A decentralized music tracking and discovery platform built on AT Protocol 馃幍
at feat/pgpull 722 lines 26 kB view raw
1use std::{collections::BTreeMap, env}; 2 3use anyhow::Error; 4use rand::Rng; 5use sqlx::{Pool, Postgres}; 6 7use crate::{ 8 auth::{decode_token, extract_did}, 9 cache::Cache, 10 crypto::decrypt_aes_256_ctr, 11 listenbrainz::types::SubmitListensRequest, 12 musicbrainz::{ 13 client::MusicbrainzClient, get_best_release_from_recordings, recording::Recording, 14 }, 15 repo::{self}, 16 rocksky, 17 spotify::{client::SpotifyClient, refresh_token}, 18 types::{Scrobble, Track}, 19 xata::user::User, 20}; 21 22fn parse_batch(form: &BTreeMap<String, String>) -> Result<Vec<Scrobble>, Error> { 23 let mut result = vec![]; 24 let mut index = 0; 25 26 loop { 27 let artist = form.get(&format!("artist[{}]", index)); 28 let track = form.get(&format!("track[{}]", index)); 29 let timestamp = form.get(&format!("timestamp[{}]", index)); 30 31 if artist.is_none() || track.is_none() || timestamp.is_none() { 32 break; 33 } 34 35 let album = form 36 .get(&format!("album[{}]", index)) 37 .cloned() 38 .map(|x| x.trim().to_string()); 39 let context = form 40 .get(&format!("context[{}]", index)) 41 .cloned() 42 .map(|x| x.trim().to_string()); 43 let stream_id = form 44 .get(&format!("streamId[{}]", index)) 45 .and_then(|s| s.trim().parse().ok()); 46 let chosen_by_user = form 47 .get(&format!("chosenByUser[{}]", index)) 48 .and_then(|s| s.trim().parse().ok()); 49 let track_number = form 50 .get(&format!("trackNumber[{}]", index)) 51 .and_then(|s| s.trim().parse().ok()); 52 let mbid = form.get(&format!("mbid[{}]", index)).cloned(); 53 let album_artist = form 54 .get(&format!("albumArtist[{}]", index)) 55 .map(|x| x.trim().to_string()); 56 let duration = form 57 .get(&format!("duration[{}]", index)) 58 .and_then(|s| s.trim().parse().ok()); 59 60 let timestamp = timestamp 61 .unwrap() 62 .trim() 63 .parse() 64 .unwrap_or(chrono::Utc::now().timestamp() as u64); 65 66 // validate timestamp, must be in the past (between 14 days before to present) 67 let now = chrono::Utc::now().timestamp() as u64; 68 if timestamp > now { 69 return Err(Error::msg("Timestamp is in the future")); 70 } 71 72 if timestamp < now - 14 * 24 * 60 * 60 { 73 return Err(Error::msg("Timestamp is too old")); 74 } 75 76 result.push(Scrobble { 77 artist: artist.unwrap().trim().to_string(), 78 track: track.unwrap().trim().to_string(), 79 timestamp, 80 album, 81 context, 82 stream_id, 83 chosen_by_user, 84 track_number, 85 mbid, 86 album_artist, 87 duration, 88 ignored: None, 89 }); 90 91 index += 1; 92 } 93 94 Ok(result) 95} 96 97pub async fn scrobble( 98 pool: &Pool<Postgres>, 99 cache: &Cache, 100 mb_client: &MusicbrainzClient, 101 form: &BTreeMap<String, String>, 102) -> Result<Vec<Scrobble>, Error> { 103 let mut scrobbles = parse_batch(form)?; 104 105 if scrobbles.is_empty() { 106 return Err(Error::msg("No scrobbles found")); 107 } 108 109 let did = extract_did(pool, form).await?; 110 111 let spofity_tokens = repo::spotify_token::get_spotify_tokens(pool, 100).await?; 112 113 if spofity_tokens.is_empty() { 114 return Err(Error::msg("No Spotify tokens found")); 115 } 116 117 for scrobble in &mut scrobbles { 118 /* 119 0. check if scrobble is cached 120 1. if mbid is present, check if it exists in the database 121 2. if it exists, scrobble 122 3. if it doesn't exist, check if it exists in Musicbrainz (using mbid) 123 4. if it exists, get album art from spotify and scrobble 124 5. if it doesn't exist, check if it exists in Spotify 125 6. if it exists, scrobble 126 7. if it doesn't exist, check if it exists in Musicbrainz (using track and artist) 127 8. if it exists, scrobble 128 9. if it doesn't exist, skip unknown track 129 */ 130 let key = format!( 131 "{} - {}", 132 scrobble.artist.to_lowercase(), 133 scrobble.track.to_lowercase() 134 ); 135 let cached = cache.get(&key)?; 136 if cached.is_some() { 137 tracing::info!(key = %key, "Cached:"); 138 let track = serde_json::from_str::<Track>(&cached.unwrap())?; 139 scrobble.album = Some(track.album.clone()); 140 rocksky::scrobble(cache, &did, track, scrobble.timestamp).await?; 141 tokio::time::sleep(std::time::Duration::from_secs(1)).await; 142 continue; 143 } 144 145 if let Some(mbid) = &scrobble.mbid { 146 // let result = repo::track::get_track_by_mbid(pool, mbid).await?; 147 let result = mb_client.get_recording(mbid).await?; 148 tracing::info!(%scrobble.artist, %scrobble.track, "Musicbrainz (mbid)"); 149 scrobble.album = Some(Track::from(result.clone()).album); 150 rocksky::scrobble(cache, &did, result.into(), scrobble.timestamp).await?; 151 tokio::time::sleep(std::time::Duration::from_secs(1)).await; 152 continue; 153 } 154 155 let result = repo::track::get_track(pool, &scrobble.track, &scrobble.artist).await?; 156 157 if let Some(track) = result { 158 tracing::info!(artist = %scrobble.artist, track = %scrobble.track, "Xata (track)"); 159 scrobble.album = Some(track.album.clone()); 160 let album = repo::album::get_album_by_track_id(pool, &track.xata_id).await?; 161 let artist = repo::artist::get_artist_by_track_id(pool, &track.xata_id).await?; 162 let mut track: Track = track.into(); 163 track.year = match album.year { 164 Some(year) => Some(year as u32), 165 None => match album.release_date.clone() { 166 Some(release_date) => { 167 let year = release_date.split("-").next(); 168 year.and_then(|x| x.parse::<u32>().ok()) 169 } 170 None => None, 171 }, 172 }; 173 track.release_date = album 174 .release_date 175 .map(|x| x.split("T").next().unwrap().to_string()); 176 track.artist_picture = artist.picture.clone(); 177 178 rocksky::scrobble(cache, &did, track, scrobble.timestamp).await?; 179 tokio::time::sleep(std::time::Duration::from_secs(1)).await; 180 continue; 181 } 182 183 // we need to pick a random token to avoid Spotify rate limiting 184 // and to avoid using the same token for all scrobbles 185 // this is a simple way to do it, but we can improve it later 186 // by using a more sophisticated algorithm 187 // or by using a token pool 188 let mut rng = rand::rng(); 189 let random_index = rng.random_range(0..spofity_tokens.len()); 190 let spotify_token = &spofity_tokens[random_index]; 191 192 let spotify_token = decrypt_aes_256_ctr( 193 &spotify_token.refresh_token, 194 &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)?, 195 )?; 196 197 let spotify_token = refresh_token(&spotify_token).await?; 198 let spotify_client = SpotifyClient::new(&spotify_token.access_token); 199 200 let result = spotify_client 201 .search(&format!( 202 r#"track:"{}" artist:"{}""#, 203 scrobble.track, scrobble.artist 204 )) 205 .await?; 206 207 if let Some(track) = result.tracks.items.first() { 208 tracing::info!(artist = %scrobble.artist, track = %scrobble.track, "Spotify (track)"); 209 scrobble.album = Some(track.album.name.clone()); 210 let mut track = track.clone(); 211 212 if let Some(album) = spotify_client.get_album(&track.album.id).await? { 213 track.album = album; 214 } 215 216 if let Some(artist) = spotify_client 217 .get_artist(&track.album.artists[0].id) 218 .await? 219 { 220 track.album.artists[0] = artist; 221 } 222 223 rocksky::scrobble(cache, &did, track.into(), scrobble.timestamp).await?; 224 tokio::time::sleep(std::time::Duration::from_secs(1)).await; 225 continue; 226 } 227 228 let query = format!( 229 r#"recording:"{}" AND artist:"{}" AND status:Official"#, 230 scrobble.track, scrobble.artist 231 ); 232 let result = mb_client.search(&query).await?; 233 234 if let Some(recording) = result.recordings.first() { 235 let result = mb_client.get_recording(&recording.id).await?; 236 tracing::info!(%scrobble.artist, %scrobble.track, "Musicbrainz (recording)"); 237 scrobble.album = Some(Track::from(result.clone()).album); 238 rocksky::scrobble(cache, &did, result.into(), scrobble.timestamp).await?; 239 tokio::time::sleep(std::time::Duration::from_secs(1)).await; 240 continue; 241 } 242 243 tracing::info!(artist = %scrobble.artist, track = %scrobble.track, "Track not found, skipping"); 244 scrobble.ignored = Some(true); 245 } 246 247 Ok(scrobbles.clone()) 248} 249 250pub async fn scrobble_v1( 251 pool: &Pool<Postgres>, 252 cache: &Cache, 253 mb_client: &MusicbrainzClient, 254 form: &BTreeMap<String, String>, 255) -> Result<(), Error> { 256 let session_id = form.get("s").unwrap().to_string(); 257 let artist = form.get("a[0]").unwrap().to_string(); 258 let track = form.get("t[0]").unwrap().to_string(); 259 let timestamp = form.get("i[0]").unwrap().to_string(); 260 261 let user = cache.get(&format!("lastfm:{}", session_id))?; 262 if user.is_none() { 263 return Err(Error::msg("Session ID not found")); 264 } 265 266 let user = user.unwrap(); 267 let user = serde_json::from_str::<User>(&user)?; 268 269 let spofity_tokens = repo::spotify_token::get_spotify_tokens(pool, 100).await?; 270 271 if spofity_tokens.is_empty() { 272 return Err(Error::msg("No Spotify tokens found")); 273 } 274 275 let mut scrobble = Scrobble { 276 artist: artist.trim().to_string(), 277 track: track.trim().to_string(), 278 timestamp: timestamp.parse::<u64>()?, 279 album: None, 280 context: None, 281 stream_id: None, 282 chosen_by_user: None, 283 track_number: None, 284 mbid: None, 285 album_artist: None, 286 duration: None, 287 ignored: None, 288 }; 289 290 let did = user.did.clone(); 291 292 /* 293 0. check if scrobble is cached 294 1. if mbid is present, check if it exists in the database 295 2. if it exists, scrobble 296 3. if it doesn't exist, check if it exists in Musicbrainz (using mbid) 297 4. if it exists, get album art from spotify and scrobble 298 5. if it doesn't exist, check if it exists in Spotify 299 6. if it exists, scrobble 300 7. if it doesn't exist, check if it exists in Musicbrainz (using track and artist) 301 8. if it exists, scrobble 302 9. if it doesn't exist, skip unknown track 303 */ 304 let key = format!( 305 "{} - {}", 306 scrobble.artist.to_lowercase(), 307 scrobble.track.to_lowercase() 308 ); 309 let cached = cache.get(&key)?; 310 if cached.is_some() { 311 tracing::info!(key = %key, "Cached:"); 312 let track = serde_json::from_str::<Track>(&cached.unwrap())?; 313 scrobble.album = Some(track.album.clone()); 314 rocksky::scrobble(cache, &did, track, scrobble.timestamp).await?; 315 tokio::time::sleep(std::time::Duration::from_secs(1)).await; 316 return Ok(()); 317 } 318 319 if let Some(mbid) = &scrobble.mbid { 320 // let result = repo::track::get_track_by_mbid(pool, mbid).await?; 321 let result = mb_client.get_recording(mbid).await?; 322 tracing::info!(%scrobble.artist, %scrobble.track, "Musicbrainz (mbid)"); 323 scrobble.album = Some(Track::from(result.clone()).album); 324 rocksky::scrobble(cache, &did, result.into(), scrobble.timestamp).await?; 325 tokio::time::sleep(std::time::Duration::from_secs(1)).await; 326 return Ok(()); 327 } 328 329 let result = repo::track::get_track(pool, &scrobble.track, &scrobble.artist).await?; 330 331 if let Some(track) = result { 332 tracing::info!(artist = %scrobble.artist, track = %scrobble.track, "Xata (track)"); 333 scrobble.album = Some(track.album.clone()); 334 let album = repo::album::get_album_by_track_id(pool, &track.xata_id).await?; 335 let artist = repo::artist::get_artist_by_track_id(pool, &track.xata_id).await?; 336 let mut track: Track = track.into(); 337 track.year = match album.year { 338 Some(year) => Some(year as u32), 339 None => match album.release_date.clone() { 340 Some(release_date) => { 341 let year = release_date.split("-").next(); 342 year.and_then(|x| x.parse::<u32>().ok()) 343 } 344 None => None, 345 }, 346 }; 347 track.release_date = album 348 .release_date 349 .map(|x| x.split("T").next().unwrap().to_string()); 350 track.artist_picture = artist.picture.clone(); 351 352 rocksky::scrobble(cache, &did, track, scrobble.timestamp).await?; 353 tokio::time::sleep(std::time::Duration::from_secs(1)).await; 354 return Ok(()); 355 } 356 357 // we need to pick a random token to avoid Spotify rate limiting 358 // and to avoid using the same token for all scrobbles 359 // this is a simple way to do it, but we can improve it later 360 // by using a more sophisticated algorithm 361 // or by using a token pool 362 let mut rng = rand::rng(); 363 let random_index = rng.random_range(0..spofity_tokens.len()); 364 let spotify_token = &spofity_tokens[random_index]; 365 366 let spotify_token = decrypt_aes_256_ctr( 367 &spotify_token.refresh_token, 368 &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)?, 369 )?; 370 371 let spotify_token = refresh_token(&spotify_token).await?; 372 let spotify_client = SpotifyClient::new(&spotify_token.access_token); 373 374 let result = spotify_client 375 .search(&format!( 376 r#"track:"{}" artist:"{}""#, 377 scrobble.track, scrobble.artist 378 )) 379 .await?; 380 381 if let Some(track) = result.tracks.items.first() { 382 let artists = track 383 .artists 384 .iter() 385 .map(|a| a.name.to_lowercase().clone()) 386 .collect::<Vec<_>>() 387 .join(", ") 388 .to_lowercase(); 389 390 // check if artists don't contain the scrobble artist (to avoid wrong matches) 391 if !artists.contains(&scrobble.artist.to_lowercase()) { 392 tracing::warn!(artist = %artist, track = ?track, "Artist mismatch, skipping"); 393 } else { 394 tracing::info!(artist = %scrobble.artist, track = %scrobble.track, "Spotify (track)"); 395 scrobble.album = Some(track.album.name.clone()); 396 let mut track = track.clone(); 397 398 if let Some(album) = spotify_client.get_album(&track.album.id).await? { 399 track.album = album; 400 } 401 402 if let Some(artist) = spotify_client 403 .get_artist(&track.album.artists[0].id) 404 .await? 405 { 406 track.album.artists[0] = artist; 407 } 408 409 rocksky::scrobble(cache, &did, track.into(), scrobble.timestamp).await?; 410 tokio::time::sleep(std::time::Duration::from_secs(1)).await; 411 return Ok(()); 412 } 413 } 414 415 let query = format!( 416 r#"recording:"{}" AND artist:"{}" AND status:Official"#, 417 scrobble.track, scrobble.artist 418 ); 419 let result = search_musicbrainz_recording(&query, mb_client, &scrobble).await; 420 if let Err(e) = result { 421 tracing::warn!(artist = %scrobble.artist, track = %scrobble.track, "Musicbrainz search error: {}", e); 422 return Ok(()); 423 } 424 let result = result.unwrap(); 425 if let Some(recording) = result { 426 let result = mb_client.get_recording(&recording.id).await?; 427 tracing::info!(%scrobble.artist, %scrobble.track, "Musicbrainz (recording)"); 428 scrobble.album = Some(Track::from(result.clone()).album); 429 rocksky::scrobble(cache, &did, result.into(), scrobble.timestamp).await?; 430 tokio::time::sleep(std::time::Duration::from_secs(1)).await; 431 return Ok(()); 432 } 433 434 tracing::info!(artist = %artist, track = %track, "Track not found, skipping"); 435 436 Ok(()) 437} 438 439pub async fn scrobble_listenbrainz( 440 pool: &Pool<Postgres>, 441 cache: &Cache, 442 mb_client: &MusicbrainzClient, 443 req: &SubmitListensRequest, 444 token: &str, 445) -> Result<(), Error> { 446 tracing::info!(req = ?req, "Listenbrainz submission"); 447 448 if req.payload.is_empty() { 449 return Err(Error::msg("No payload found")); 450 } 451 452 let artist = req.payload[0].track_metadata.artist_name.clone(); 453 let track = req.payload[0].track_metadata.track_name.clone(); 454 let timestamp = match req.payload[0].listened_at { 455 Some(timestamp) => timestamp.to_string(), 456 None => chrono::Utc::now().timestamp().to_string(), 457 }; 458 459 let did = match decode_token(token) { 460 Ok(claims) => claims.did, 461 Err(e) => { 462 let user = repo::user::get_user_by_apikey(pool, token) 463 .await? 464 .map(|user| user.did); 465 if let Some(did) = user { 466 did 467 } else { 468 return Err(Error::msg(format!( 469 "Failed to decode token: {} {}", 470 e, token 471 ))); 472 } 473 } 474 }; 475 476 let user = repo::user::get_user_by_did(pool, &did).await?; 477 478 if user.is_none() { 479 return Err(Error::msg("User not found")); 480 } 481 482 cache.setex( 483 &format!("listenbrainz:emby:{}:{}:{}", artist, track, did), 484 "1", 485 60 * 5, // 5 minutes 486 )?; 487 488 if cache 489 .get(&format!("listenbrainz:cache:{}:{}:{}", artist, track, did))? 490 .is_some() 491 { 492 tracing::info!(artist= %artist, track = %track, "Recently scrobbled, skipping"); 493 return Ok(()); 494 } 495 496 let spotify_user = repo::spotify_account::get_spotify_account(pool, &did).await?; 497 if let Some(spotify_user) = spotify_user { 498 if cache 499 .get(&format!("{}:current", spotify_user.email))? 500 .is_some() 501 { 502 tracing::info!(artist= %artist, track = %track, "Currently scrobbling, skipping"); 503 return Ok(()); 504 } 505 } 506 507 if cache.get(&format!("nowplaying:{}", did))?.is_some() { 508 tracing::info!(artist= %artist, track = %track, "Currently scrobbling, skipping"); 509 return Ok(()); 510 } 511 512 // set cache for 5 seconds to avoid duplicate scrobbles 513 cache.setex( 514 &format!("listenbrainz:cache:{}:{}:{}", artist, track, did), 515 "1", 516 5, 517 )?; 518 519 let spofity_tokens = repo::spotify_token::get_spotify_tokens(pool, 100).await?; 520 521 if spofity_tokens.is_empty() { 522 return Err(Error::msg("No Spotify tokens found")); 523 } 524 525 let mut scrobble = Scrobble { 526 artist: artist.trim().to_string(), 527 track: track.trim().to_string(), 528 timestamp: timestamp.parse::<u64>()?, 529 album: None, 530 context: None, 531 stream_id: None, 532 chosen_by_user: None, 533 track_number: None, 534 mbid: None, 535 album_artist: None, 536 duration: None, 537 ignored: None, 538 }; 539 540 /* 541 0. check if scrobble is cached 542 1. if mbid is present, check if it exists in the database 543 2. if it exists, scrobble 544 3. if it doesn't exist, check if it exists in Musicbrainz (using mbid) 545 4. if it exists, get album art from spotify and scrobble 546 5. if it doesn't exist, check if it exists in Spotify 547 6. if it exists, scrobble 548 7. if it doesn't exist, check if it exists in Musicbrainz (using track and artist) 549 8. if it exists, scrobble 550 9. if it doesn't exist, skip unknown track 551 */ 552 let key = format!( 553 "{} - {}", 554 scrobble.artist.to_lowercase(), 555 scrobble.track.to_lowercase() 556 ); 557 let cached = cache.get(&key)?; 558 if cached.is_some() { 559 tracing::info!(key = %key, "Cached"); 560 let track = serde_json::from_str::<Track>(&cached.unwrap())?; 561 scrobble.album = Some(track.album.clone()); 562 rocksky::scrobble(cache, &did, track, scrobble.timestamp).await?; 563 tokio::time::sleep(std::time::Duration::from_secs(1)).await; 564 return Ok(()); 565 } 566 567 if let Some(mbid) = &scrobble.mbid { 568 // let result = repo::track::get_track_by_mbid(pool, mbid).await?; 569 let result = mb_client.get_recording(mbid).await?; 570 tracing::info!("Musicbrainz (mbid)"); 571 scrobble.album = Some(Track::from(result.clone()).album); 572 rocksky::scrobble(cache, &did, result.into(), scrobble.timestamp).await?; 573 tokio::time::sleep(std::time::Duration::from_secs(1)).await; 574 return Ok(()); 575 } 576 577 let result = repo::track::get_track(pool, &scrobble.track, &scrobble.artist).await?; 578 579 if let Some(track) = result { 580 tracing::info!(id = %track.xata_id, artist = %track.artist, album = %track.album, album_atist = %track.album_artist, album_uri = ?track.album_uri, artist_uri = ?track.artist_uri, "Xata (track)"); 581 scrobble.album = Some(track.album.clone()); 582 let album = 583 repo::album::get_album_by_uri(pool, &track.album_uri.clone().unwrap_or_default()) 584 .await?; 585 let artist = 586 repo::artist::get_artist_by_uri(pool, &track.artist_uri.clone().unwrap_or_default()) 587 .await?; 588 let mut track: Track = track.into(); 589 track.year = match album.year { 590 Some(year) => Some(year as u32), 591 None => match album.release_date.clone() { 592 Some(release_date) => { 593 let year = release_date.split("-").next(); 594 year.and_then(|x| x.parse::<u32>().ok()) 595 } 596 None => None, 597 }, 598 }; 599 track.release_date = album 600 .release_date 601 .map(|x| x.split("T").next().unwrap().to_string()); 602 track.artist_picture = artist.picture.clone(); 603 604 rocksky::scrobble(cache, &did, track, scrobble.timestamp).await?; 605 tokio::time::sleep(std::time::Duration::from_secs(1)).await; 606 return Ok(()); 607 } 608 609 // we need to pick a random token to avoid Spotify rate limiting 610 // and to avoid using the same token for all scrobbles 611 // this is a simple way to do it, but we can improve it later 612 // by using a more sophisticated algorithm 613 // or by using a token pool 614 let mut rng = rand::rng(); 615 let random_index = rng.random_range(0..spofity_tokens.len()); 616 let spotify_token = &spofity_tokens[random_index]; 617 618 let spotify_token = decrypt_aes_256_ctr( 619 &spotify_token.refresh_token, 620 &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)?, 621 )?; 622 623 let spotify_token = refresh_token(&spotify_token).await?; 624 let spotify_client = SpotifyClient::new(&spotify_token.access_token); 625 626 let result = spotify_client 627 .search(&format!( 628 r#"track:"{}" artist:"{}""#, 629 scrobble.track, scrobble.artist 630 )) 631 .await?; 632 633 if let Some(track) = result.tracks.items.first() { 634 let artists = track 635 .artists 636 .iter() 637 .map(|a| a.name.to_lowercase().clone()) 638 .collect::<Vec<_>>() 639 .join(", ") 640 .to_lowercase(); 641 642 // check if artists don't contain the scrobble artist (to avoid wrong matches) 643 if !artists.contains(&scrobble.artist.to_lowercase()) { 644 tracing::warn!(artist = %artist, track = ?track, "Artist mismatch, skipping"); 645 } else { 646 tracing::info!("Spotify (track)"); 647 scrobble.album = Some(track.album.name.clone()); 648 let mut track = track.clone(); 649 650 if let Some(album) = spotify_client.get_album(&track.album.id).await? { 651 track.album = album; 652 } 653 654 if let Some(artist) = spotify_client 655 .get_artist(&track.album.artists[0].id) 656 .await? 657 { 658 track.album.artists[0] = artist; 659 } 660 661 rocksky::scrobble(cache, &did, track.into(), scrobble.timestamp).await?; 662 tokio::time::sleep(std::time::Duration::from_secs(1)).await; 663 return Ok(()); 664 } 665 } 666 667 let query = format!( 668 r#"recording:"{}" AND artist:"{}" AND status:Official"#, 669 scrobble.track, scrobble.artist 670 ); 671 let result = search_musicbrainz_recording(&query, mb_client, &scrobble).await; 672 if let Err(e) = result { 673 tracing::warn!(artist = %artist, track = %track, "Musicbrainz search error: {}", e); 674 return Ok(()); 675 } 676 let result = result.unwrap(); 677 if let Some(result) = result { 678 tracing::info!("Musicbrainz (recording)"); 679 rocksky::scrobble(cache, &did, result.into(), scrobble.timestamp).await?; 680 tokio::time::sleep(std::time::Duration::from_secs(1)).await; 681 return Ok(()); 682 } 683 684 tracing::warn!(artist = %artist, track = %track, "Track not found, skipping"); 685 686 Ok(()) 687} 688 689async fn search_musicbrainz_recording( 690 query: &str, 691 mb_client: &MusicbrainzClient, 692 scrobble: &Scrobble, 693) -> Result<Option<Recording>, Error> { 694 let result = mb_client.search(&query).await; 695 if let Err(e) = result { 696 tracing::warn!(artist = %scrobble.artist, track = %scrobble.track, "Musicbrainz search error: {}", e); 697 return Ok(None); 698 } 699 let result = result.unwrap(); 700 701 let release = get_best_release_from_recordings(&result, &scrobble.artist); 702 703 if let Some(release) = release { 704 let recording = result.recordings.into_iter().find(|r| { 705 r.releases 706 .as_ref() 707 .map(|releases| releases.iter().any(|rel| rel.id == release.id)) 708 .unwrap_or(false) 709 }); 710 if recording.is_none() { 711 tracing::warn!(artist = %scrobble.artist, track = %scrobble.track, "Recording not found in MusicBrainz result, skipping"); 712 return Ok(None); 713 } 714 let recording = recording.unwrap(); 715 let mut result = mb_client.get_recording(&recording.id).await?; 716 tracing::info!("Musicbrainz (recording)"); 717 result.releases = Some(vec![release]); 718 return Ok(Some(result)); 719 } 720 721 Ok(None) 722}