forked from
rocksky.app/rocksky
A decentralized music tracking and discovery platform built on AT Protocol 馃幍
1use std::env;
2
3use crate::cache::Cache;
4use crate::crypto::decrypt_aes_256_ctr;
5use crate::musicbrainz::client::MusicbrainzClient;
6use crate::spotify::client::SpotifyClient;
7use crate::spotify::refresh_token;
8use crate::types::{ScrobbleRequest, Track};
9use crate::{repo, rocksky};
10use anyhow::Error;
11use owo_colors::OwoColorize;
12use rand::Rng;
13use sqlx::{Pool, Postgres};
14
15pub async fn scrobble(
16 pool: &Pool<Postgres>,
17 cache: &Cache,
18 scrobble: ScrobbleRequest,
19 did: &str,
20) -> Result<(), Error> {
21 let spofity_tokens = repo::spotify_token::get_spotify_tokens(pool, 100).await?;
22
23 if spofity_tokens.is_empty() {
24 return Err(Error::msg("No Spotify tokens found"));
25 }
26
27 let mb_client = MusicbrainzClient::new();
28
29 let key = format!(
30 "{} - {}",
31 scrobble.data.song.parsed.artist.to_lowercase(),
32 scrobble.data.song.parsed.track.to_lowercase()
33 );
34
35 let cached = cache.get(&key)?;
36 if cached.is_some() {
37 println!("{}", format!("Cached: {}", key).yellow());
38 let track = serde_json::from_str::<Track>(&cached.unwrap())?;
39 rocksky::scrobble(cache, &did, track, scrobble.time).await?;
40 tokio::time::sleep(std::time::Duration::from_secs(1)).await;
41 return Ok(());
42 }
43
44 let result = repo::track::get_track(
45 pool,
46 &scrobble.data.song.parsed.track,
47 &scrobble.data.song.parsed.artist,
48 )
49 .await?;
50
51 if let Some(track) = result {
52 println!("{}", "Xata (track)".yellow());
53 let album = repo::album::get_album_by_track_id(pool, &track.xata_id).await?;
54 let artist = repo::artist::get_artist_by_track_id(pool, &track.xata_id).await?;
55 let mut track: Track = track.into();
56 track.year = match album.year {
57 Some(year) => Some(year as u32),
58 None => match album.release_date.clone() {
59 Some(release_date) => {
60 let year = release_date.split("-").next();
61 year.and_then(|x| x.parse::<u32>().ok())
62 }
63 None => None,
64 },
65 };
66 track.release_date = album
67 .release_date
68 .map(|x| x.split("T").next().unwrap().to_string());
69 track.artist_picture = artist.picture.clone();
70
71 rocksky::scrobble(cache, &did, track, scrobble.time).await?;
72 tokio::time::sleep(std::time::Duration::from_secs(1)).await;
73 return Ok(());
74 }
75
76 // we need to pick a random token to avoid Spotify rate limiting
77 // and to avoid using the same token for all scrobbles
78 // this is a simple way to do it, but we can improve it later
79 // by using a more sophisticated algorithm
80 // or by using a token pool
81 let mut rng = rand::rng();
82 let random_index = rng.random_range(0..spofity_tokens.len());
83 let spotify_token = &spofity_tokens[random_index];
84
85 let spotify_token = decrypt_aes_256_ctr(
86 &spotify_token.refresh_token,
87 &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)?,
88 )?;
89
90 let spotify_token = refresh_token(&spotify_token).await?;
91 let spotify_client = SpotifyClient::new(&spotify_token.access_token);
92
93 let query = match scrobble.data.song.parsed.artist.contains(" x ") {
94 true => {
95 let artists = scrobble
96 .data
97 .song
98 .parsed
99 .artist
100 .split(" x ")
101 .map(|a| format!(r#"artist:"{}""#, a.trim()))
102 .collect::<Vec<_>>()
103 .join(" ");
104 format!(r#"track:"{}" {}"#, scrobble.data.song.parsed.track, artists)
105 }
106 false => match scrobble.data.song.parsed.artist.contains(", ") {
107 true => {
108 let artists = scrobble
109 .data
110 .song
111 .parsed
112 .artist
113 .split(", ")
114 .map(|a| format!(r#"artist:"{}""#, a.trim()))
115 .collect::<Vec<_>>()
116 .join(" ");
117 format!(r#"track:"{}" {}"#, scrobble.data.song.parsed.track, artists)
118 }
119 false => format!(
120 r#"track:"{}" artist:"{}""#,
121 scrobble.data.song.parsed.track,
122 scrobble.data.song.parsed.artist.trim()
123 ),
124 },
125 };
126
127 let result = spotify_client.search(&query).await?;
128
129 if let Some(track) = result.tracks.items.first() {
130 println!("{}", "Spotify (track)".yellow());
131 let mut track = track.clone();
132
133 if let Some(album) = spotify_client.get_album(&track.album.id).await? {
134 track.album = album;
135 }
136
137 if let Some(artist) = spotify_client
138 .get_artist(&track.album.artists[0].id)
139 .await?
140 {
141 track.album.artists[0] = artist;
142 }
143
144 rocksky::scrobble(cache, &did, track.into(), scrobble.time).await?;
145 tokio::time::sleep(std::time::Duration::from_secs(1)).await;
146 return Ok(());
147 }
148
149 let query = format!(
150 r#"recording:"{}" AND artist:"{}""#,
151 scrobble.data.song.parsed.track, scrobble.data.song.parsed.artist
152 );
153 let result = mb_client.search(&query).await?;
154
155 if let Some(recording) = result.recordings.first() {
156 let result = mb_client.get_recording(&recording.id).await?;
157 println!("{}", "Musicbrainz (recording)".yellow());
158 rocksky::scrobble(cache, &did, result.into(), scrobble.time).await?;
159 tokio::time::sleep(std::time::Duration::from_secs(1)).await;
160 return Ok(());
161 }
162
163 println!(
164 "{} {} - {}, skipping",
165 "Track not found: ".yellow(),
166 scrobble.data.song.parsed.artist,
167 scrobble.data.song.parsed.track
168 );
169
170 Ok(())
171}