forked from
rocksky.app/rocksky
A decentralized music tracking and discovery platform built on AT Protocol 馃幍
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}