···1212pub mod rocksky;
1313pub mod repo;
1414pub mod types;
1515+pub mod listenbrainz;
15161617use actix_session::SessionExt as _;
1718use std::{env, sync::Arc, time::Duration};
···7576 .service(handlers::handle_methods)
7677 .service(handlers::handle_nowplaying)
7778 .service(handlers::handle_submission)
7979+ .service(handlers::handle_submit_listens)
7880 .service(handlers::index)
7981 .service(handlers::handle_get)
8082 })
+17-1
crates/scrobbler/src/repo/user.rs
···1414 .fetch_all(pool)
1515 .await?;
16161717- if results.len() == 0 {
1717+ if results.is_empty(){
1818 return Ok(None);
1919 }
20202121 Ok(Some(results[0].clone()))
2222}
23232424+2525+pub async fn get_user_by_did(pool: &Pool<Postgres>, did: &str) -> Result<Option<User>, Error> {
2626+ let results: Vec<User> = sqlx::query_as(r#"
2727+ SELECT * FROM users
2828+ WHERE did = $1
2929+ "#)
3030+ .bind(did)
3131+ .fetch_all(pool)
3232+ .await?;
3333+3434+ if results.is_empty() {
3535+ return Ok(None);
3636+ }
3737+3838+ Ok(Some(results[0].clone()))
3939+}
+162-1
crates/scrobbler/src/scrobbler.rs
···66use sqlx::{Pool, Postgres};
7788use crate::{
99- auth::extract_did, cache::Cache, crypto::decrypt_aes_256_ctr, musicbrainz::client::MusicbrainzClient, repo, rocksky, spotify::{
99+ auth::{decode_token, extract_did}, cache::Cache, crypto::decrypt_aes_256_ctr, listenbrainz::types::SubmitListensRequest, musicbrainz::client::MusicbrainzClient, repo, rocksky, spotify::{
1010 client::SpotifyClient,
1111 refresh_token
1212 }, types::{Scrobble, Track}, xata::user::User
···231231232232 let user = user.unwrap();
233233 let user = serde_json::from_str::<User>(&user)?;
234234+235235+ let spofity_tokens = repo::spotify_token::get_spotify_tokens(pool, 100).await?;
236236+237237+ if spofity_tokens.is_empty() {
238238+ return Err(Error::msg("No Spotify tokens found"));
239239+ }
240240+241241+ let mb_client = MusicbrainzClient::new();
242242+243243+ let mut scrobble = Scrobble {
244244+ artist: artist.trim().to_string(),
245245+ track: track.trim().to_string(),
246246+ timestamp: timestamp.parse::<u64>()?,
247247+ album: None,
248248+ context: None,
249249+ stream_id: None,
250250+ chosen_by_user: None,
251251+ track_number: None,
252252+ mbid: None,
253253+ album_artist: None,
254254+ duration: None,
255255+ ignored: None,
256256+ };
257257+258258+ let did = user.did.clone();
259259+260260+ /*
261261+ 0. check if scrobble is cached
262262+ 1. if mbid is present, check if it exists in the database
263263+ 2. if it exists, scrobble
264264+ 3. if it doesn't exist, check if it exists in Musicbrainz (using mbid)
265265+ 4. if it exists, get album art from spotify and scrobble
266266+ 5. if it doesn't exist, check if it exists in Spotify
267267+ 6. if it exists, scrobble
268268+ 7. if it doesn't exist, check if it exists in Musicbrainz (using track and artist)
269269+ 8. if it exists, scrobble
270270+ 9. if it doesn't exist, skip unknown track
271271+ */
272272+ let key = format!("{} - {}", scrobble.artist.to_lowercase(), scrobble.track.to_lowercase());
273273+ let cached = cache.get(&key)?;
274274+ if cached.is_some() {
275275+ println!("{}", format!("Cached: {}", key).yellow());
276276+ let track = serde_json::from_str::<Track>(&cached.unwrap())?;
277277+ scrobble.album = Some(track.album.clone());
278278+ rocksky::scrobble(cache, &did, track, scrobble.timestamp).await?;
279279+ tokio::time::sleep(std::time::Duration::from_secs(1)).await;
280280+ return Ok(());
281281+ }
282282+283283+ if let Some(mbid) = &scrobble.mbid {
284284+ // let result = repo::track::get_track_by_mbid(pool, mbid).await?;
285285+ let result = mb_client.get_recording(mbid).await?;
286286+ println!("{}", "Musicbrainz (mbid)".yellow());
287287+ scrobble.album = Some(Track::from(result.clone()).album);
288288+ rocksky::scrobble(cache, &did, result.into(), scrobble.timestamp).await?;
289289+ tokio::time::sleep(std::time::Duration::from_secs(1)).await;
290290+ return Ok(());
291291+ }
292292+293293+ let result = repo::track::get_track(pool, &scrobble.track, &scrobble.artist).await?;
294294+295295+ if let Some(track) = result {
296296+ println!("{}", "Xata (track)".yellow());
297297+ scrobble.album = Some(track.album.clone());
298298+ let album = repo::album::get_album_by_track_id(pool, &track.xata_id).await?;
299299+ let artist = repo::artist::get_artist_by_track_id(pool, &track.xata_id).await?;
300300+ let mut track: Track = track.into();
301301+ track.year = match album.year {
302302+ Some(year) => Some(year as u32),
303303+ None => match album.release_date.clone() {
304304+ Some(release_date) => {
305305+ let year = release_date.split("-").next();
306306+ year.and_then(|x| x.parse::<u32>().ok())
307307+ }
308308+ None => None,
309309+ },
310310+ };
311311+ track.release_date = album.release_date.map(|x| x.split("T").next().unwrap().to_string());
312312+ track.artist_picture = artist.picture.clone();
313313+314314+ rocksky::scrobble(cache, &did, track, scrobble.timestamp).await?;
315315+ tokio::time::sleep(std::time::Duration::from_secs(1)).await;
316316+ return Ok(());
317317+ }
318318+319319+ // we need to pick a random token to avoid Spotify rate limiting
320320+ // and to avoid using the same token for all scrobbles
321321+ // this is a simple way to do it, but we can improve it later
322322+ // by using a more sophisticated algorithm
323323+ // or by using a token pool
324324+ let mut rng = rand::rng();
325325+ let random_index = rng.random_range(0..spofity_tokens.len());
326326+ let spotify_token = &spofity_tokens[random_index];
327327+328328+ let spotify_token = decrypt_aes_256_ctr(
329329+ &spotify_token.refresh_token,
330330+ &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)?
331331+ )?;
332332+333333+ let spotify_token = refresh_token(&spotify_token).await?;
334334+ let spotify_client = SpotifyClient::new(&spotify_token.access_token);
335335+336336+ let result = spotify_client.search(&format!(r#"track:"{}" artist:"{}""#, scrobble.track, scrobble.artist)).await?;
337337+338338+ if let Some(track) = result.tracks.items.first() {
339339+ println!("{}", "Spotify (track)".yellow());
340340+ scrobble.album = Some(track.album.name.clone());
341341+ let mut track = track.clone();
342342+343343+ if let Some(album) = spotify_client.get_album(&track.album.id).await? {
344344+ track.album = album;
345345+ }
346346+347347+ if let Some(artist) = spotify_client.get_artist(&track.album.artists[0].id).await? {
348348+ track.album.artists[0] = artist;
349349+ }
350350+351351+ rocksky::scrobble(cache, &did, track.into(), scrobble.timestamp).await?;
352352+ tokio::time::sleep(std::time::Duration::from_secs(1)).await;
353353+ return Ok(());
354354+ }
355355+356356+ let query = format!(
357357+ r#"recording:"{}" AND artist:"{}""#,
358358+ scrobble.track, scrobble.artist
359359+ );
360360+ let result = mb_client.search(&query).await?;
361361+362362+ if let Some(recording) = result.recordings.first() {
363363+ let result = mb_client.get_recording(&recording.id).await?;
364364+ println!("{}", "Musicbrainz (recording)".yellow());
365365+ scrobble.album = Some(Track::from(result.clone()).album);
366366+ rocksky::scrobble(cache, &did, result.into(), scrobble.timestamp).await?;
367367+ tokio::time::sleep(std::time::Duration::from_secs(1)).await;
368368+ return Ok(());
369369+ }
370370+371371+ println!("{} {} - {}, skipping", "Track not found: ".yellow(), artist, track);
372372+373373+ Ok(())
374374+}
375375+376376+pub async fn scrobble_listenbrainz(pool: &Pool<Postgres>, cache: &Cache, req: SubmitListensRequest, token: &str) -> Result<(), Error> {
377377+ if req.payload.is_empty() {
378378+ return Err(Error::msg("No payload found"));
379379+ }
380380+381381+ let artist = req.payload[0].track_metadata.artist_name.clone();
382382+ let track = req.payload[0].track_metadata.track_name.clone();
383383+ let timestamp = req.payload[0].listened_at.to_string();
384384+385385+ let claims = decode_token(token)?;
386386+ let did = claims.did.clone();
387387+ let user = repo::user::get_user_by_did(pool, &did)
388388+ .await?;
389389+390390+ if user.is_none() {
391391+ return Err(Error::msg("User not found"));
392392+ }
393393+394394+ let user = user.unwrap();
234395235396 let spofity_tokens = repo::spotify_token::get_spotify_tokens(pool, 100).await?;
236397