From c7f34f416afcabd0eb2ab78f34fbb33e294faf41 Mon Sep 17 00:00:00 2001 From: Emilia Date: Wed, 12 Jul 2023 22:26:25 -0400 Subject: [PATCH] Add new status endpoint Returns JSON and attributes songs in the playlist to users. Note change to database schema!!! --- Cargo.lock | 30 ++++----- server/Cargo.toml | 3 - server/src/main.rs | 20 +++--- server/src/refresh.rs | 53 +++++++++------- server/src/spotify_auth.rs | 89 ++++++++++++++++++-------- server/src/spotify_get_top.rs | 7 +-- server/src/spotify_playlist.rs | 33 +++++----- server/src/spotify_tracks.rs | 51 +++++++++++++++ server/src/spotify_types.rs | 38 ++++++++++-- server/src/spotify_users.rs | 45 ++++++++++++++ server/src/sql_tracks.rs | 59 ++++++++++++------ server/src/sql_users.rs | 110 +++++++++++++++++++-------------- server/src/status.rs | 62 +++++++++++++++++++ 13 files changed, 438 insertions(+), 162 deletions(-) create mode 100644 server/src/spotify_tracks.rs create mode 100644 server/src/spotify_users.rs create mode 100644 server/src/status.rs diff --git a/Cargo.lock b/Cargo.lock index e44abf5..d97ac9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -370,6 +370,21 @@ dependencies = [ "version_check", ] +[[package]] +name = "cool_spotify_server" +version = "0.1.0" +dependencies = [ + "actix-web", + "dotenvy", + "env_logger", + "log", + "openssl", + "reqwest", + "serde", + "serde_json", + "sqlx", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -1555,21 +1570,6 @@ dependencies = [ "serde", ] -[[package]] -name = "server" -version = "0.1.0" -dependencies = [ - "actix-web", - "dotenvy", - "env_logger", - "log", - "openssl", - "reqwest", - "serde", - "serde_json", - "sqlx", -] - [[package]] name = "sha1" version = "0.10.5" diff --git a/server/Cargo.toml b/server/Cargo.toml index f253342..8f2fe84 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -4,9 +4,6 @@ version = "0.1.0" edition = "2021" authors = ["Emilia Allison"] -[[bin]] -name = "server" - [dependencies] actix-web = "4" dotenvy = "0.15" diff --git a/server/src/main.rs b/server/src/main.rs index 344ab4a..a60e17f 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,29 +1,33 @@ mod spotify_auth; mod spotify_get_top; mod spotify_playlist; +mod spotify_tracks; mod spotify_types; +mod spotify_users; -mod sql_users; mod sql_tracks; +mod sql_users; -mod state; mod joint_err; mod refresh; +mod state; +mod status; +use refresh::{refresh_all_manager, refresh_manager}; use spotify_auth::callback_manager; -use refresh::{refresh_manager, refresh_all_manager}; -use actix_web::{App, HttpServer, HttpResponse, get, Responder, web}; +use actix_web::{get, web, App, HttpResponse, HttpServer, Responder}; use dotenvy::dotenv; +use status::status_manager; use std::env; -const BASE_URL: &'static str = "https://api.spotify.com/v1"; +const BASE_URL: &str = "https://api.spotify.com/v1"; #[get("/")] async fn root(data: web::Data) -> impl Responder { log::warn!("Access to root"); - HttpResponse::Ok().body(format!("hi, pls go to: {}\n k thx", data.auth_url)) + HttpResponse::Ok().body(format!("hi, pls go to:\n {}\n k thx", data.auth_url)) } #[get("/failed")] @@ -45,7 +49,8 @@ async fn main() -> std::io::Result<()> { dotenv().ok(); let port: u16 = std::env::var("PORT") .expect("Cannot proceed without port") - .parse().expect("Port must be a number"); + .parse() + .expect("Port must be a number"); let address = std::env::var("ADDRESS").expect("Cannot proceed without a port to listen to"); let db_url = env::var("DATABASE_URL").expect("Cannot proceed without database url"); @@ -67,6 +72,7 @@ async fn main() -> std::io::Result<()> { .service(failed) .service(refresh_manager) .service(refresh_all_manager) + .service(status_manager) }) .bind((address, port))? .run() diff --git a/server/src/refresh.rs b/server/src/refresh.rs index 6cd0211..eeb9066 100644 --- a/server/src/refresh.rs +++ b/server/src/refresh.rs @@ -1,11 +1,10 @@ +use actix_web::{get, web, HttpResponse, Responder}; use serde::Deserialize; -use actix_web::web::Redirect; -use actix_web::{HttpResponse, get, Responder, web}; use sqlx::MySqlPool; -use crate::spotify_get_top::get_top_tracks; -use crate::{sql_users, spotify_auth, sql_tracks, spotify_playlist}; use crate::joint_err::JointErr; +use crate::spotify_get_top::get_top_tracks; +use crate::{spotify_auth, spotify_playlist, sql_tracks, sql_users}; #[derive(Deserialize)] pub struct RefreshData { @@ -16,9 +15,8 @@ pub struct RefreshData { #[get("/refresh")] pub async fn refresh_manager( data: web::Query, - state: web::Data - ) -> impl Responder -{ + state: web::Data, +) -> impl Responder { let pool = &state.pool; if let Some(spotify_id) = &data.spotify_id { @@ -41,44 +39,55 @@ pub async fn refresh_manager( #[allow(non_snake_case)] #[get("/refresh_all")] -pub async fn refresh_all_manager(state: web::Data) - -> impl Responder { +pub async fn refresh_all_manager(state: web::Data) -> impl Responder { let pool = &state.pool; - let users = sqlx::query!(" + let users = sqlx::query!( + " SELECT SpotifyId FROM Users WHERE LastRefreshed < CURDATE() - INTERVAL 7 DAY - ") - .fetch_all(pool) - .await; + " + ) + .fetch_all(pool) + .await; if let Ok(users) = users { for user in users { - if let Err(_) = refresh(&user.SpotifyId, pool).await { - log::error!("Tried to refresh for {}, failed", &user.SpotifyId); + if let Err(e) = refresh(&user.SpotifyId, pool).await { + log::error!( + "Tried to refresh for {}, failed with\n{:?}", + &user.SpotifyId, + e + ); } } - return HttpResponse::Ok().body("yeah ok"); + HttpResponse::Ok().body("yeah ok") } else { - return HttpResponse::InternalServerError().body("where the users at") + HttpResponse::InternalServerError().body("where the users at") } } async fn refresh(spotify_id: &str, pool: &MySqlPool) -> Result<(), JointErr> { let token = spotify_auth::refresh_token_for_id(spotify_id, pool).await?; let internal_id = sql_users::get_internal_id_by_id(spotify_id, pool).await?; - let top: Vec<_> = get_top_tracks(&token).await?.items + let top: Vec<_> = get_top_tracks(&token) + .await? + .items .iter() .map(|track| track.uri.clone()) .collect(); for track in top { log::info!("Trying to insert {}", track.0); - sqlx::query!(" + sqlx::query!( + " INSERT INTO Tracks (UserId, Uri, DateAdded) VALUES (?, ?, CURDATE()) ON DUPLICATE KEY UPDATE DateAdded = CURDATE() - ", internal_id, track.0) - .execute(pool) - .await?; + ", + internal_id, + track.0 + ) + .execute(pool) + .await?; } // Reset playlist diff --git a/server/src/spotify_auth.rs b/server/src/spotify_auth.rs index 38ef680..a9523e5 100644 --- a/server/src/spotify_auth.rs +++ b/server/src/spotify_auth.rs @@ -1,25 +1,27 @@ use actix_web::web::Redirect; -use actix_web::{HttpResponse, get, Responder, web}; +use actix_web::{get, web, Responder}; use serde::Deserialize; -use serde_json::json; -use sqlx::MySqlPool; -use sqlx::mysql::MySqlPoolOptions; -use std::{env, collections::HashMap}; -use crate::spotify_get_top::get_top_tracks; -use crate::spotify_playlist::add_to_playlist; -use crate::spotify_types::*; + +use sqlx::MySqlPool; +use std::{collections::HashMap, env}; + use crate::joint_err::JointErr; -use crate::sql_users::{add_user_dont_care, get_refresh_token_by_id, GetRefreshTokenByIdErr, AddUserSuccess}; -const URL: &'static str = "https://accounts.spotify.com/api/token"; + + +use crate::sql_users::{ + add_user_dont_care, get_refresh_token_by_id, AddUserSuccess, +}; + +const URL: &str = "https://accounts.spotify.com/api/token"; fn read_from_env() -> (String, String, String) { let id = env::var("CLIENT_ID").expect("Cannot proceed without client id"); let secret = env::var("CLIENT_SECRET").expect("Cannot proceed without client secret"); let url = env::var("REDIRECT_URL").expect("Cannot proceed without redirect url"); - return (id, secret, url) + (id, secret, url) } #[derive(Deserialize)] @@ -28,7 +30,10 @@ pub struct CallbackData { } #[get("/callback")] -pub async fn callback_manager(data: web::Query, state: web::Data) -> impl Responder { +pub async fn callback_manager( + data: web::Query, + state: web::Data, +) -> impl Responder { log::warn!("Access to /callback"); let pool = &state.pool; @@ -37,29 +42,34 @@ pub async fn callback_manager(data: web::Query, state: web::Data { log::warn!("Added a new user: {}", up.id); Redirect::to(format!("/refresh?spotify_id={}&force=true", up.id)) - }, + } Ok(AddUserSuccess::Duplicate) => { Redirect::to(format!("/refresh?spotify_id={}", up.id)) - }, + } _ => { log::error!("SQL Query Failed: couldn't add new user\n{:?}", q); Redirect::to("/failed") } } } else { - log::error!("We failed to get a user profile, the error was:\n{:?}", user_profile.unwrap_err()); + log::error!( + "We failed to get a user profile, the error was:\n{:?}", + user_profile.unwrap_err() + ); Redirect::to("/failed") } - } else { - log::error!("We failed to get tokens, the error was:\n{:?}", tokens.unwrap_err()); + } else { + log::error!( + "We failed to get tokens, the error was:\n{:?}", + tokens.unwrap_err() + ); Redirect::to("/failed") } - } #[derive(Deserialize, Debug)] @@ -75,7 +85,7 @@ pub struct Token(pub String); async fn get_auth_token(code: &str) -> Result { // Consider using env for REDIRECT_URI //const REDIRECT_URI: &'static str = "https://ilia.moe/cool-stuff/cool-spotify-blend/callback"; - const GRANT_TYPE: &'static str = "authorization_code"; + const GRANT_TYPE: &str = "authorization_code"; let (id, secret, redirect_uri) = read_from_env(); @@ -87,7 +97,8 @@ async fn get_auth_token(code: &str) -> Result Result Result { @@ -117,7 +127,8 @@ pub async fn refresh_token_for_id(spotify_id: &str, pool: &MySqlPool) -> Result< params.insert("client_secret", &secret); let client = reqwest::Client::new(); - let res: RefreshTokenForIdRes = client.post(URL) + let res: RefreshTokenForIdRes = client + .post(URL) .header("Content-Type", "application/x-www-form-urlencoded") .header("Content-Length", 0) .query(¶ms) @@ -137,7 +148,8 @@ struct UserProfile { async fn get_user_email(token: &Token) -> Result { let client = reqwest::Client::new(); - let res = client.get(crate::BASE_URL.to_owned() + "/me") + let res = client + .get(crate::BASE_URL.to_owned() + "/me") .header("Authorization", "Bearer ".to_owned() + &token.0) .send() .await? @@ -146,3 +158,30 @@ async fn get_user_email(token: &Token) -> Result { Ok(res) } + +#[derive(Debug, Deserialize)] +#[non_exhaustive] +struct ClientCredentialsResponse { + pub access_token: Token, +} + +pub async fn get_client_credentials() -> Result { + let client = reqwest::Client::new(); + let (id, secret, _) = read_from_env(); + + let mut params = HashMap::new(); + params.insert("grant_type", "client_credentials".to_owned()); + + let res: ClientCredentialsResponse = client + .post("https://accounts.spotify.com/api/token") + .query(¶ms) + .header("Content-Type", "application/x-www-form-urlencoded") + .header("Content-Length", 0) + .basic_auth(&id, Some(&secret)) + .send() + .await? + .json() + .await?; + + Ok(res.access_token) +} diff --git a/server/src/spotify_get_top.rs b/server/src/spotify_get_top.rs index a55441e..3c82299 100644 --- a/server/src/spotify_get_top.rs +++ b/server/src/spotify_get_top.rs @@ -1,12 +1,10 @@ use std::collections::HashMap; - use serde::Deserialize; use crate::spotify_auth::Token; use crate::spotify_types::*; - #[derive(Deserialize, Debug)] pub struct TopSongsResponse { pub href: String, @@ -22,9 +20,9 @@ pub async fn get_top_tracks(token: &Token) -> Result Result, token: &Token) -> Result<(), reqwest::Error> { +pub async fn add_to_playlist( + id: &str, + uris: Vec, + token: &Token, +) -> Result<(), reqwest::Error> { let mut params = HashMap::new(); params.insert("position", "0"); // Insert at top, we could remove to append let mut body: HashMap> = HashMap::new(); - let uris: Vec = uris.iter() - .map(|u| &u.0) - .cloned() - .collect(); + let uris: Vec = uris.iter().map(|u| &u.0).cloned().collect(); body.insert("uris".to_owned(), uris); log::info!("Uri body: {:?}", body); let client = reqwest::Client::new(); - let res = client.post(crate::BASE_URL.to_owned() + "/playlists/" + id + "/tracks") + let _res = client + .post(crate::BASE_URL.to_owned() + "/playlists/" + id + "/tracks") .query(¶ms) .json(&body) .header("Authorization", "Bearer ".to_owned() + &token.0) @@ -29,18 +31,19 @@ pub async fn add_to_playlist(id: &str, uris: Vec, token: &Token) -> Result< } // WE CANNOT SET MORE THAN 100 AT ONCE!!! -pub async fn set_playlist(id: &str, uris: Vec, token: &Token, pool: &MySqlPool) - -> Result<(), reqwest::Error> { - +pub async fn set_playlist( + id: &str, + uris: Vec, + token: &Token, + _pool: &MySqlPool, +) -> Result<(), reqwest::Error> { let mut body: HashMap> = HashMap::new(); - let uris: Vec = uris.iter() - .map(|u| &u.0) - .cloned() - .collect(); + let uris: Vec = uris.iter().map(|u| &u.0).cloned().collect(); body.insert("uris".to_owned(), uris); let client = reqwest::Client::new(); - let res = client.put(crate::BASE_URL.to_owned() + "/playlists/" + id + "/tracks") + let _res = client + .put(crate::BASE_URL.to_owned() + "/playlists/" + id + "/tracks") .json(&body) .header("Authorization", "Bearer ".to_owned() + &token.0) .send() diff --git a/server/src/spotify_tracks.rs b/server/src/spotify_tracks.rs new file mode 100644 index 0000000..832b110 --- /dev/null +++ b/server/src/spotify_tracks.rs @@ -0,0 +1,51 @@ +use serde::{Deserialize}; +use std::collections::HashMap; + +use crate::spotify_auth::Token; +use crate::{spotify_types::*, BASE_URL}; + +pub async fn get_tracks( + token: &Token, + tracks: &[&Uri], +) -> Result, reqwest::Error> { + let pieces = tracks.chunks(50); + + let mut out = Vec::::new(); + for piece in pieces { + out.append(&mut get_tracks_helper(token, piece).await?) + } + + Ok(out) +} + +#[derive(Debug, Deserialize)] +pub struct TracksResult { + pub tracks: Vec, +} + +// We can only get 50 at a time! +async fn get_tracks_helper( + token: &Token, + tracks: &[&Uri], +) -> Result, reqwest::Error> { + let mut params: HashMap<&str, String> = HashMap::new(); + let tracks_str: String = tracks + .iter() + .map(|uri| uri.get_suffix()) + .collect::>() + .join(","); + params.insert("ids", tracks_str); + + let client = reqwest::Client::new(); + let res = client + .get(BASE_URL.to_owned() + "/tracks") + .query(¶ms) + .bearer_auth(token.0.clone()) + .send() + .await? + .json::() + .await? + .tracks; + + Ok(res) +} diff --git a/server/src/spotify_types.rs b/server/src/spotify_types.rs index beac454..aa67dce 100644 --- a/server/src/spotify_types.rs +++ b/server/src/spotify_types.rs @@ -1,6 +1,6 @@ -use serde::Deserialize; +use serde::{Deserialize, Serialize}; -#[derive(Deserialize, Debug, Clone)] +#[derive(Deserialize, Serialize, Debug, Clone)] #[non_exhaustive] pub struct TrackObject { pub album: Option, @@ -11,7 +11,7 @@ pub struct TrackObject { pub uri: Uri, } -#[derive(Deserialize, Debug, Clone)] +#[derive(Deserialize, Serialize, Debug, Clone)] #[non_exhaustive] pub struct AlbumObject { pub album_type: String, @@ -22,10 +22,9 @@ pub struct AlbumObject { pub release_date: String, pub uri: Uri, pub genres: Option, - } -#[derive(Deserialize, Debug, Clone)] +#[derive(Deserialize, Serialize, Debug, Clone)] #[non_exhaustive] pub struct ArtistObject { pub href: String, @@ -33,5 +32,32 @@ pub struct ArtistObject { pub name: String, } -#[derive(Deserialize, Debug, Clone)] +#[derive(Deserialize, Serialize, Debug, Clone)] pub struct Uri(pub String); + +impl Uri { + pub fn get_suffix(&self) -> String { + self.0 + .split(':') + .nth(2) + .expect("URI should have the correct format") + .to_string() + } +} + +#[derive(Deserialize, Debug, Clone, PartialEq, Eq, Hash)] +pub struct SpotifyId(pub String); + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[non_exhaustive] +pub struct UserProfile { + id: String, + display_name: Option, + images: Option>, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[non_exhaustive] +pub struct ImageObject { + pub url: String, +} diff --git a/server/src/spotify_users.rs b/server/src/spotify_users.rs new file mode 100644 index 0000000..51f7e75 --- /dev/null +++ b/server/src/spotify_users.rs @@ -0,0 +1,45 @@ +use std::collections::HashMap; + +use crate::spotify_auth::Token; +use crate::{spotify_types::*, BASE_URL}; + +pub async fn get_users( + token: &Token, + users: &[&SpotifyId], +) -> Result, reqwest::Error> { + let unique_users = { + let mut unique_users: Vec<_> = users.into(); + unique_users.dedup(); + unique_users + }; + let user_map = get_users_helper(token, &unique_users).await?; + let out: Vec = users + .iter() + .filter_map(|spotify_id| user_map.get(spotify_id)) + .cloned() + .collect(); + + Ok(out) +} + +async fn get_users_helper( + token: &Token, + unique_users: &[&SpotifyId], +) -> Result, reqwest::Error> { + let client = reqwest::Client::new(); + + let mut map: HashMap = HashMap::new(); + for user in unique_users { + let res = client + .get(BASE_URL.to_string() + "/users/" + &user.0) + .bearer_auth(token.0.clone()) + .query(&[("user_id", user.0.clone())]) + .send() + .await? + .json::() + .await?; + map.insert(SpotifyId(user.0.clone()), res); + } + + Ok(map) +} diff --git a/server/src/sql_tracks.rs b/server/src/sql_tracks.rs index 932c510..63846ea 100644 --- a/server/src/sql_tracks.rs +++ b/server/src/sql_tracks.rs @@ -1,33 +1,56 @@ -use sqlx::{MySqlPool, mysql::MySqlQueryResult}; use crate::spotify_types::*; +use sqlx::{ + MySqlPool, +}; #[allow(non_snake_case)] // Fixes warning for macro pub async fn even_allocation(pool: &MySqlPool) -> Result, sqlx::Error> { - let users: Vec = sqlx::query!(" + let users: Vec = sqlx::query!( + " SELECT UserId From Users - ") - .fetch_all(pool) - .await? - .iter() - .map(|row| row.UserId) - .collect(); + " + ) + .fetch_all(pool) + .await? + .iter() + .map(|row| row.UserId) + .collect(); + + // Reset the SQL playlist representation + sqlx::query!("UPDATE Tracks SET InPlaylist = 0") + .execute(pool) + .await?; - let mut uris: Vec = Vec::new(); let num: u32 = (100 / users.len()) as u32; for user in users { - let mut uri_list: Vec = sqlx::query!(" - SELECT Uri FROM Tracks + sqlx::query!( + " + UPDATE Tracks SET InPlaylist = 1 + WHERE TrackID In + ( + SELECT TrackId FROM Tracks WHERE UserId = ? + ) ORDER BY RAND() LIMIT ? - ", user, num) - .fetch_all(pool) - .await? - .iter() - .map(|row| Uri(row.Uri.clone())) - .collect(); - uris.append(&mut uri_list); + ", + user, + num + ) + .execute(pool) + .await?; } + let uris = sqlx::query!( + " + SELECT Uri FROM Tracks + WHERE InPlaylist = 1 + " + ) + .fetch_all(pool) + .await? + .iter() + .map(|row| Uri(row.Uri.clone())) + .collect(); Ok(uris) } diff --git a/server/src/sql_users.rs b/server/src/sql_users.rs index 05dd879..4e1e6e0 100644 --- a/server/src/sql_users.rs +++ b/server/src/sql_users.rs @@ -1,38 +1,48 @@ -use sqlx::{MySqlPool, mysql::MySqlQueryResult}; +use sqlx::{mysql::MySqlQueryResult, MySqlPool}; use crate::spotify_auth::Token; -async fn add_user(email: &str, spotify_id: &str, refresh_token: &Token, pool: &MySqlPool) - -> Result { - Ok(sqlx::query!( +async fn add_user( + email: &str, + spotify_id: &str, + refresh_token: &Token, + pool: &MySqlPool, +) -> Result { + sqlx::query!( "INSERT INTO spotify.Users (Email, SpotifyId, RefreshToken) VALUES (?, ?, ?)", email.to_owned(), spotify_id.to_owned(), refresh_token.0.to_owned() - ).execute(pool).await?) + ) + .execute(pool) + .await } #[derive(Debug, PartialEq, Eq)] pub enum AddUserSuccess { New, - Duplicate + Duplicate, } -pub async fn add_user_dont_care(email: &str, spotify_id: &str, refresh_token: &Token, pool: &MySqlPool) - -> Result { - let q = add_user(email, spotify_id, refresh_token, pool).await; - match q { - Ok(_) => Ok(AddUserSuccess::New), - Err(sqlx::Error::Database(err)) => { - if err.code().map(|e| e.into_owned()) == Some("23000".to_owned()) { - Ok(AddUserSuccess::Duplicate) - } else { - Err(sqlx::Error::Database(err)) - } - }, - Err(_) => Err(q.unwrap_err()) +pub async fn add_user_dont_care( + email: &str, + spotify_id: &str, + refresh_token: &Token, + pool: &MySqlPool, +) -> Result { + let q = add_user(email, spotify_id, refresh_token, pool).await; + match q { + Ok(_) => Ok(AddUserSuccess::New), + Err(sqlx::Error::Database(err)) => { + if err.code().map(|e| e.into_owned()) == Some("23000".to_owned()) { + Ok(AddUserSuccess::Duplicate) + } else { + Err(sqlx::Error::Database(err)) + } } + Err(_) => Err(q.unwrap_err()), + } } pub enum GetRefreshTokenByIdErr { @@ -46,43 +56,51 @@ impl From for GetRefreshTokenByIdErr { } #[allow(non_snake_case)] // Fixes warning for macro -pub async fn get_refresh_token_by_id(spotify_id: &str, pool: &MySqlPool) - -> Result { - let res = sqlx::query!("SELECT RefreshToken FROM Users WHERE SpotifyId LIKE ?", - spotify_id.to_owned()) - .fetch_one(pool) - .await?; +pub async fn get_refresh_token_by_id( + spotify_id: &str, + pool: &MySqlPool, +) -> Result { + let res = sqlx::query!( + "SELECT RefreshToken FROM Users WHERE SpotifyId LIKE ?", + spotify_id.to_owned() + ) + .fetch_one(pool) + .await?; match res.RefreshToken { Some(token) => Ok(Token(token)), - None => Err(GetRefreshTokenByIdErr::Empty) + None => Err(GetRefreshTokenByIdErr::Empty), } } #[allow(non_snake_case)] // Fixes warning for macro -pub async fn get_refreshed_by_id(spotify_id: &str, pool: &MySqlPool) - -> Result { - let res = sqlx::query!("SELECT LastRefreshed FROM Users WHERE SpotifyID LIKE ?", - spotify_id.to_owned()) - .fetch_one(pool) - .await?; - Ok(res.LastRefreshed) +pub async fn get_refreshed_by_id( + spotify_id: &str, + pool: &MySqlPool, +) -> Result { + let res = sqlx::query!( + "SELECT LastRefreshed FROM Users WHERE SpotifyID LIKE ?", + spotify_id.to_owned() + ) + .fetch_one(pool) + .await?; + Ok(res.LastRefreshed) } #[allow(non_snake_case)] // Fixes warning for macro -pub async fn get_internal_id_by_id(spotify_id: &str, pool: &MySqlPool) - -> Result { - let res = sqlx::query!("SELECT UserId FROM Users WHERE SpotifyID LIKE ?", - spotify_id.to_owned()) - .fetch_one(pool) - .await?; - Ok(res.UserId) +pub async fn get_internal_id_by_id(spotify_id: &str, pool: &MySqlPool) -> Result { + let res = sqlx::query!( + "SELECT UserId FROM Users WHERE SpotifyID LIKE ?", + spotify_id.to_owned() + ) + .fetch_one(pool) + .await?; + Ok(res.UserId) } #[allow(non_snake_case)] // Fixes warning for macro -pub async fn get_emilia_spotify_id(pool: &MySqlPool) - -> Result { - let res = sqlx::query!("SELECT SpotifyId FROM Users WHERE IsEmilia = 1") - .fetch_one(pool) - .await?; - Ok(res.SpotifyId) +pub async fn get_emilia_spotify_id(pool: &MySqlPool) -> Result { + let res = sqlx::query!("SELECT SpotifyId FROM Users WHERE IsEmilia = 1") + .fetch_one(pool) + .await?; + Ok(res.SpotifyId) } diff --git a/server/src/status.rs b/server/src/status.rs new file mode 100644 index 0000000..6bf3d83 --- /dev/null +++ b/server/src/status.rs @@ -0,0 +1,62 @@ +use crate::{spotify_auth, spotify_tracks, spotify_types::*, spotify_users}; +use actix_web::{get, web, HttpResponse, Responder}; +use sqlx::MySqlPool; + + + +use crate::joint_err::JointErr; + +#[get("/status")] +pub async fn status_manager(state: web::Data) -> impl Responder { + let pool = &state.pool; + + // Not doing this in an if let because either this works or we exit early + let token = spotify_auth::get_client_credentials().await; + if token.is_err() { + log::error!("Couldn't get a token:\n{:?}", token.unwrap_err()); + return HttpResponse::InternalServerError().body("i don't have a token???"); + } + let token = token.unwrap(); + + if let Ok(entries) = get_current_tracks(pool).await { + let tracks: Vec<&Uri> = entries + .iter() + .map(|tuple| &tuple.0) // I only want the track Uri here + .collect(); + let users: Vec<&SpotifyId> = entries.iter().map(|tuple| &tuple.1).collect(); + + let track_objects = spotify_tracks::get_tracks(&token, &tracks).await; + let user_objects = spotify_users::get_users(&token, &users).await; + if track_objects.is_err() { + log::error!( + "Failed to get tracks with:\n{:?}", + track_objects.unwrap_err() + ); + return HttpResponse::InternalServerError() + .body("Spotify did not like my request for track info"); + } + if user_objects.is_err() { + log::error!("Failed to get users with:\n{:?}", user_objects.unwrap_err()); + return HttpResponse::InternalServerError() + .body("Spotify did not like my request for user info"); + } + + let track_objects = track_objects.unwrap(); + let user_objects = user_objects.unwrap(); + let combined: Vec<(TrackObject, UserProfile)> = + std::iter::zip(track_objects, user_objects).collect(); + HttpResponse::Ok().json(combined) + } else { + HttpResponse::InternalServerError().body("I couldn't find the tracks, sorry") + } +} + +#[allow(non_snake_case)] +async fn get_current_tracks(pool: &MySqlPool) -> Result, JointErr> { + Ok(sqlx::query!("SELECT Uri, SpotifyId FROM CurrentPlaylist") + .fetch_all(pool) + .await? + .iter() + .map(|row| (Uri(row.Uri.clone()), SpotifyId(row.SpotifyId.clone()))) + .collect()) +}