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