A decentralized music tracking and discovery platform built on AT Protocol 馃幍
at main 189 lines 6.0 kB view raw
1use anyhow::Error; 2use reqwest::Client; 3 4use crate::{ 5 cache::Cache, 6 get_artist, get_currently_playing, 7 token::generate_token, 8 types::{ 9 album_tracks::Track, 10 currently_playing::{Album, CurrentlyPlaying}, 11 }, 12}; 13 14const ROCKSKY_API: &str = "https://api.rocksky.app"; 15 16pub async fn scrobble( 17 cache: Cache, 18 spotify_email: &str, 19 did: &str, 20 refresh_token: &str, 21 client_id: &str, 22 client_secret: &str, 23) -> Result<(), Error> { 24 let cached = cache.get(spotify_email)?; 25 if cached.is_none() { 26 println!( 27 "No currently playing song is cached for {}, skipping", 28 spotify_email 29 ); 30 return Ok(()); 31 } 32 33 let track = serde_json::from_str::<CurrentlyPlaying>(&cached.unwrap())?; 34 if track.item.is_none() { 35 println!("No currently playing song found, skipping"); 36 return Ok(()); 37 } 38 39 let track_item = track.item.unwrap(); 40 41 let artist = get_artist( 42 cache.clone(), 43 &track_item.artists.first().unwrap().id, 44 &refresh_token, 45 &client_id, 46 &client_secret, 47 ) 48 .await?; 49 50 let token = generate_token(did)?; 51 let client = Client::new(); 52 let response = client 53 .post(&format!("{}/now-playing", ROCKSKY_API)) 54 .bearer_auth(token) 55 .json(&serde_json::json!({ 56 "title": track_item.name, 57 "album": track_item.album.name, 58 "artist": track_item.artists.iter().map(|artist| artist.name.clone()).collect::<Vec<String>>().join(", "), 59 "albumArtist": track_item.album.artists.first().map(|artist| artist.name.clone()), 60 "duration": track_item.duration_ms, 61 "trackNumber": track_item.track_number, 62 "releaseDate": match track_item.album.release_date_precision.as_str() { 63 "day" => Some(track_item.album.release_date.clone()), 64 _ => None 65 }, 66 "year": match track_item.album.release_date_precision.as_str() { 67 "day" => Some(track_item.album.release_date.split('-').next().unwrap().parse::<u32>().unwrap()), 68 "year" => Some(track_item.album.release_date.parse::<u32>().unwrap()), 69 _ => None 70 }, 71 "discNumber": track_item.disc_number, 72 "albumArt": track_item.album.images.first().map(|image| image.url.clone()), 73 "spotifyLink": match track_item.external_urls { 74 Some(urls) => Some(urls.spotify), 75 None => None, 76 }, 77 "label": track_item.album.label, 78 "artistPicture": match &artist { 79 Some(artist) => match &artist.images { 80 Some(images) => Some(images.first().map(|image| image.url.clone())), 81 None => None 82 }, 83 None => None 84 }, 85 "genres": match &artist { 86 Some(artist) => artist.genres.clone(), 87 None => None 88 }, 89 })) 90 .send() 91 .await?; 92 93 if !response.status().is_success() { 94 println!("Failed to scrobble: {}", response.text().await?); 95 } 96 97 Ok(()) 98} 99 100pub async fn update_library( 101 cache: Cache, 102 spotify_email: &str, 103 did: &str, 104 refresh_token: &str, 105 client_id: &str, 106 client_secret: &str, 107) -> Result<(), Error> { 108 let cached = cache.get(spotify_email)?; 109 if cached.is_none() { 110 println!( 111 "No currently playing song is cached for {}, refreshing", 112 spotify_email 113 ); 114 get_currently_playing( 115 cache.clone(), 116 &spotify_email, 117 &refresh_token, 118 client_id, 119 client_secret, 120 ) 121 .await?; 122 } 123 124 let cached = cache.get(spotify_email)?; 125 let track = serde_json::from_str::<CurrentlyPlaying>(&cached.unwrap())?; 126 if track.item.is_none() { 127 println!("No currently playing song found, skipping"); 128 return Ok(()); 129 } 130 let track_item = track.item.unwrap(); 131 let cached = cache.get(&format!("{}:tracks", track_item.album.id))?; 132 if cached.is_none() { 133 println!("Album not cached {}, skipping", track_item.album.id); 134 return Ok(()); 135 } 136 137 let tracks = serde_json::from_str::<Vec<Track>>(&cached.unwrap())?; 138 139 let cached = cache.get(&track_item.album.id)?; 140 let album = serde_json::from_str::<Album>(&cached.unwrap())?; 141 142 let token = generate_token(did)?; 143 144 for track in tracks { 145 let client = Client::new(); 146 let response = client 147 .post(&format!("{}/tracks", ROCKSKY_API)) 148 .bearer_auth(&token) 149 .json(&serde_json::json!({ 150 "title": track.name, 151 "album": album.name, 152 "artist": track.artists.iter().map(|artist| artist.name.clone()).collect::<Vec<String>>().join(", "), 153 "albumArtist": album.artists.first().map(|artist| artist.name.clone()), 154 "duration": track.duration_ms, 155 "trackNumber": track.track_number, 156 "releaseDate": match album.release_date_precision.as_str() { 157 "day" => Some(album.release_date.clone()), 158 _ => None 159 }, 160 "year": match album.release_date_precision.as_str() { 161 "day" => Some(album.release_date.split('-').next().unwrap().parse::<u32>().unwrap()), 162 "year" => Some(album.release_date.parse::<u32>().unwrap()), 163 _ => None 164 }, 165 "discNumber": track.disc_number, 166 "albumArt": album.images.first().map(|image| image.url.clone()), 167 "spotifyLink": match track.external_urls { 168 Some(urls) => Some(urls.spotify), 169 None => None, 170 }, 171 "label": album.label, 172 "artistPicture": track.artists.first().map(|artist| match &artist.images { 173 Some(images) => Some(images.first().map(|image| image.url.clone())), 174 None => None 175 }), 176 })) 177 .send() 178 .await?; 179 180 // wait 50 seconds to avoid rate limiting 181 tokio::time::sleep(tokio::time::Duration::from_secs(50)).await; 182 183 if !response.status().is_success() { 184 println!("Failed to save track: {}", response.text().await?); 185 } 186 } 187 188 Ok(()) 189}