A decentralized music tracking and discovery platform built on AT Protocol 馃幍
at feat/pgpull 176 lines 5.7 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 "genres": match &artist { 82 Some(artist) => artist.genres.clone(), 83 None => None 84 }, 85 })) 86 .send() 87 .await?; 88 89 if !response.status().is_success() { 90 println!("Failed to scrobble: {}", response.text().await?); 91 } 92 93 Ok(()) 94} 95 96pub async fn update_library( 97 cache: Cache, 98 spotify_email: &str, 99 did: &str, 100 refresh_token: &str, 101) -> Result<(), Error> { 102 let cached = cache.get(spotify_email)?; 103 if cached.is_none() { 104 println!( 105 "No currently playing song is cached for {}, refreshing", 106 spotify_email 107 ); 108 get_currently_playing(cache.clone(), &spotify_email, &refresh_token).await?; 109 } 110 111 let cached = cache.get(spotify_email)?; 112 let track = serde_json::from_str::<CurrentlyPlaying>(&cached.unwrap())?; 113 if track.item.is_none() { 114 println!("No currently playing song found, skipping"); 115 return Ok(()); 116 } 117 let track_item = track.item.unwrap(); 118 let cached = cache.get(&format!("{}:tracks", track_item.album.id))?; 119 if cached.is_none() { 120 println!("Album not cached {}, skipping", track_item.album.id); 121 return Ok(()); 122 } 123 124 let tracks = serde_json::from_str::<Vec<Track>>(&cached.unwrap())?; 125 126 let cached = cache.get(&track_item.album.id)?; 127 let album = serde_json::from_str::<Album>(&cached.unwrap())?; 128 129 let token = generate_token(did)?; 130 131 for track in tracks { 132 let client = Client::new(); 133 let response = client 134 .post(&format!("{}/tracks", ROCKSKY_API)) 135 .bearer_auth(&token) 136 .json(&serde_json::json!({ 137 "title": track.name, 138 "album": album.name, 139 "artist": track.artists.iter().map(|artist| artist.name.clone()).collect::<Vec<String>>().join(", "), 140 "albumArtist": album.artists.first().map(|artist| artist.name.clone()), 141 "duration": track.duration_ms, 142 "trackNumber": track.track_number, 143 "releaseDate": match album.release_date_precision.as_str() { 144 "day" => Some(album.release_date.clone()), 145 _ => None 146 }, 147 "year": match album.release_date_precision.as_str() { 148 "day" => Some(album.release_date.split('-').next().unwrap().parse::<u32>().unwrap()), 149 "year" => Some(album.release_date.parse::<u32>().unwrap()), 150 _ => None 151 }, 152 "discNumber": track.disc_number, 153 "albumArt": album.images.first().map(|image| image.url.clone()), 154 "spotifyLink": match track.external_urls { 155 Some(urls) => Some(urls.spotify), 156 None => None, 157 }, 158 "label": album.label, 159 "artistPicture": track.artists.first().map(|artist| match &artist.images { 160 Some(images) => Some(images.first().map(|image| image.url.clone())), 161 None => None 162 }), 163 })) 164 .send() 165 .await?; 166 167 // wait 50 seconds to avoid rate limiting 168 tokio::time::sleep(tokio::time::Duration::from_secs(50)).await; 169 170 if !response.status().is_success() { 171 println!("Failed to save track: {}", response.text().await?); 172 } 173 } 174 175 Ok(()) 176}