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) -> 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}