A decentralized music tracking and discovery platform built on AT Protocol 馃幍
at fix/spotify 171 lines 5.8 kB view raw
1use std::env; 2 3use crate::cache::Cache; 4use crate::crypto::decrypt_aes_256_ctr; 5use crate::musicbrainz::client::MusicbrainzClient; 6use crate::spotify::client::SpotifyClient; 7use crate::spotify::refresh_token; 8use crate::types::{ScrobbleRequest, Track}; 9use crate::{repo, rocksky}; 10use anyhow::Error; 11use owo_colors::OwoColorize; 12use rand::Rng; 13use sqlx::{Pool, Postgres}; 14 15pub async fn scrobble( 16 pool: &Pool<Postgres>, 17 cache: &Cache, 18 scrobble: ScrobbleRequest, 19 did: &str, 20) -> Result<(), Error> { 21 let spofity_tokens = repo::spotify_token::get_spotify_tokens(pool, 100).await?; 22 23 if spofity_tokens.is_empty() { 24 return Err(Error::msg("No Spotify tokens found")); 25 } 26 27 let mb_client = MusicbrainzClient::new(); 28 29 let key = format!( 30 "{} - {}", 31 scrobble.data.song.parsed.artist.to_lowercase(), 32 scrobble.data.song.parsed.track.to_lowercase() 33 ); 34 35 let cached = cache.get(&key)?; 36 if cached.is_some() { 37 println!("{}", format!("Cached: {}", key).yellow()); 38 let track = serde_json::from_str::<Track>(&cached.unwrap())?; 39 rocksky::scrobble(cache, &did, track, scrobble.time).await?; 40 tokio::time::sleep(std::time::Duration::from_secs(1)).await; 41 return Ok(()); 42 } 43 44 let result = repo::track::get_track( 45 pool, 46 &scrobble.data.song.parsed.track, 47 &scrobble.data.song.parsed.artist, 48 ) 49 .await?; 50 51 if let Some(track) = result { 52 println!("{}", "Xata (track)".yellow()); 53 let album = repo::album::get_album_by_track_id(pool, &track.xata_id).await?; 54 let artist = repo::artist::get_artist_by_track_id(pool, &track.xata_id).await?; 55 let mut track: Track = track.into(); 56 track.year = match album.year { 57 Some(year) => Some(year as u32), 58 None => match album.release_date.clone() { 59 Some(release_date) => { 60 let year = release_date.split("-").next(); 61 year.and_then(|x| x.parse::<u32>().ok()) 62 } 63 None => None, 64 }, 65 }; 66 track.release_date = album 67 .release_date 68 .map(|x| x.split("T").next().unwrap().to_string()); 69 track.artist_picture = artist.picture.clone(); 70 71 rocksky::scrobble(cache, &did, track, scrobble.time).await?; 72 tokio::time::sleep(std::time::Duration::from_secs(1)).await; 73 return Ok(()); 74 } 75 76 // we need to pick a random token to avoid Spotify rate limiting 77 // and to avoid using the same token for all scrobbles 78 // this is a simple way to do it, but we can improve it later 79 // by using a more sophisticated algorithm 80 // or by using a token pool 81 let mut rng = rand::rng(); 82 let random_index = rng.random_range(0..spofity_tokens.len()); 83 let spotify_token = &spofity_tokens[random_index]; 84 85 let spotify_token = decrypt_aes_256_ctr( 86 &spotify_token.refresh_token, 87 &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)?, 88 )?; 89 90 let spotify_token = refresh_token(&spotify_token).await?; 91 let spotify_client = SpotifyClient::new(&spotify_token.access_token); 92 93 let query = match scrobble.data.song.parsed.artist.contains(" x ") { 94 true => { 95 let artists = scrobble 96 .data 97 .song 98 .parsed 99 .artist 100 .split(" x ") 101 .map(|a| format!(r#"artist:"{}""#, a.trim())) 102 .collect::<Vec<_>>() 103 .join(" "); 104 format!(r#"track:"{}" {}"#, scrobble.data.song.parsed.track, artists) 105 } 106 false => match scrobble.data.song.parsed.artist.contains(", ") { 107 true => { 108 let artists = scrobble 109 .data 110 .song 111 .parsed 112 .artist 113 .split(", ") 114 .map(|a| format!(r#"artist:"{}""#, a.trim())) 115 .collect::<Vec<_>>() 116 .join(" "); 117 format!(r#"track:"{}" {}"#, scrobble.data.song.parsed.track, artists) 118 } 119 false => format!( 120 r#"track:"{}" artist:"{}""#, 121 scrobble.data.song.parsed.track, 122 scrobble.data.song.parsed.artist.trim() 123 ), 124 }, 125 }; 126 127 let result = spotify_client.search(&query).await?; 128 129 if let Some(track) = result.tracks.items.first() { 130 println!("{}", "Spotify (track)".yellow()); 131 let mut track = track.clone(); 132 133 if let Some(album) = spotify_client.get_album(&track.album.id).await? { 134 track.album = album; 135 } 136 137 if let Some(artist) = spotify_client 138 .get_artist(&track.album.artists[0].id) 139 .await? 140 { 141 track.album.artists[0] = artist; 142 } 143 144 rocksky::scrobble(cache, &did, track.into(), scrobble.time).await?; 145 tokio::time::sleep(std::time::Duration::from_secs(1)).await; 146 return Ok(()); 147 } 148 149 let query = format!( 150 r#"recording:"{}" AND artist:"{}""#, 151 scrobble.data.song.parsed.track, scrobble.data.song.parsed.artist 152 ); 153 let result = mb_client.search(&query).await?; 154 155 if let Some(recording) = result.recordings.first() { 156 let result = mb_client.get_recording(&recording.id).await?; 157 println!("{}", "Musicbrainz (recording)".yellow()); 158 rocksky::scrobble(cache, &did, result.into(), scrobble.time).await?; 159 tokio::time::sleep(std::time::Duration::from_secs(1)).await; 160 return Ok(()); 161 } 162 163 println!( 164 "{} {} - {}, skipping", 165 "Track not found: ".yellow(), 166 scrobble.data.song.parsed.artist, 167 scrobble.data.song.parsed.track 168 ); 169 170 Ok(()) 171}