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