Add new status endpoint

Returns JSON and attributes
songs in the playlist to
users.
Note change to database schema!!!
This commit is contained in:
Emilia Allison 2023-07-12 22:26:25 -04:00
parent 39e098c4ca
commit c7f34f416a
Signed by: emilia
GPG Key ID: 7A3F8997BFE894E0
13 changed files with 438 additions and 162 deletions

30
Cargo.lock generated
View File

@ -370,6 +370,21 @@ dependencies = [
"version_check", "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]] [[package]]
name = "core-foundation" name = "core-foundation"
version = "0.9.3" version = "0.9.3"
@ -1555,21 +1570,6 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "server"
version = "0.1.0"
dependencies = [
"actix-web",
"dotenvy",
"env_logger",
"log",
"openssl",
"reqwest",
"serde",
"serde_json",
"sqlx",
]
[[package]] [[package]]
name = "sha1" name = "sha1"
version = "0.10.5" version = "0.10.5"

View File

@ -4,9 +4,6 @@ version = "0.1.0"
edition = "2021" edition = "2021"
authors = ["Emilia Allison"] authors = ["Emilia Allison"]
[[bin]]
name = "server"
[dependencies] [dependencies]
actix-web = "4" actix-web = "4"
dotenvy = "0.15" dotenvy = "0.15"

View File

@ -1,29 +1,33 @@
mod spotify_auth; mod spotify_auth;
mod spotify_get_top; mod spotify_get_top;
mod spotify_playlist; mod spotify_playlist;
mod spotify_tracks;
mod spotify_types; mod spotify_types;
mod spotify_users;
mod sql_users;
mod sql_tracks; mod sql_tracks;
mod sql_users;
mod state;
mod joint_err; mod joint_err;
mod refresh; mod refresh;
mod state;
mod status;
use refresh::{refresh_all_manager, refresh_manager};
use spotify_auth::callback_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 dotenvy::dotenv;
use status::status_manager;
use std::env; use std::env;
const BASE_URL: &'static str = "https://api.spotify.com/v1"; const BASE_URL: &str = "https://api.spotify.com/v1";
#[get("/")] #[get("/")]
async fn root(data: web::Data<state::AppState>) -> impl Responder { async fn root(data: web::Data<state::AppState>) -> impl Responder {
log::warn!("Access to root"); 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")] #[get("/failed")]
@ -45,7 +49,8 @@ async fn main() -> std::io::Result<()> {
dotenv().ok(); dotenv().ok();
let port: u16 = std::env::var("PORT") let port: u16 = std::env::var("PORT")
.expect("Cannot proceed without 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 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"); 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(failed)
.service(refresh_manager) .service(refresh_manager)
.service(refresh_all_manager) .service(refresh_all_manager)
.service(status_manager)
}) })
.bind((address, port))? .bind((address, port))?
.run() .run()

View File

@ -1,11 +1,10 @@
use actix_web::{get, web, HttpResponse, Responder};
use serde::Deserialize; use serde::Deserialize;
use actix_web::web::Redirect;
use actix_web::{HttpResponse, get, Responder, web};
use sqlx::MySqlPool; 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::joint_err::JointErr;
use crate::spotify_get_top::get_top_tracks;
use crate::{spotify_auth, spotify_playlist, sql_tracks, sql_users};
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct RefreshData { pub struct RefreshData {
@ -16,9 +15,8 @@ pub struct RefreshData {
#[get("/refresh")] #[get("/refresh")]
pub async fn refresh_manager( pub async fn refresh_manager(
data: web::Query<RefreshData>, data: web::Query<RefreshData>,
state: web::Data<crate::state::AppState> state: web::Data<crate::state::AppState>,
) -> impl Responder ) -> impl Responder {
{
let pool = &state.pool; let pool = &state.pool;
if let Some(spotify_id) = &data.spotify_id { if let Some(spotify_id) = &data.spotify_id {
@ -41,42 +39,53 @@ pub async fn refresh_manager(
#[allow(non_snake_case)] #[allow(non_snake_case)]
#[get("/refresh_all")] #[get("/refresh_all")]
pub async fn refresh_all_manager(state: web::Data<crate::state::AppState>) pub async fn refresh_all_manager(state: web::Data<crate::state::AppState>) -> impl Responder {
-> impl Responder {
let pool = &state.pool; let pool = &state.pool;
let users = sqlx::query!(" let users = sqlx::query!(
"
SELECT SpotifyId FROM Users SELECT SpotifyId FROM Users
WHERE LastRefreshed < CURDATE() - INTERVAL 7 DAY WHERE LastRefreshed < CURDATE() - INTERVAL 7 DAY
") "
)
.fetch_all(pool) .fetch_all(pool)
.await; .await;
if let Ok(users) = users { if let Ok(users) = users {
for user in users { for user in users {
if let Err(_) = refresh(&user.SpotifyId, pool).await { if let Err(e) = refresh(&user.SpotifyId, pool).await {
log::error!("Tried to refresh for {}, failed", &user.SpotifyId); log::error!(
"Tried to refresh for {}, failed with\n{:?}",
&user.SpotifyId,
e
);
} }
} }
return HttpResponse::Ok().body("yeah ok"); HttpResponse::Ok().body("yeah ok")
} else { } 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> { async fn refresh(spotify_id: &str, pool: &MySqlPool) -> Result<(), JointErr> {
let token = spotify_auth::refresh_token_for_id(spotify_id, pool).await?; 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 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() .iter()
.map(|track| track.uri.clone()) .map(|track| track.uri.clone())
.collect(); .collect();
for track in top { for track in top {
log::info!("Trying to insert {}", track.0); log::info!("Trying to insert {}", track.0);
sqlx::query!(" sqlx::query!(
"
INSERT INTO Tracks (UserId, Uri, DateAdded) INSERT INTO Tracks (UserId, Uri, DateAdded)
VALUES (?, ?, CURDATE()) VALUES (?, ?, CURDATE())
ON DUPLICATE KEY UPDATE DateAdded = CURDATE() ON DUPLICATE KEY UPDATE DateAdded = CURDATE()
", internal_id, track.0) ",
internal_id,
track.0
)
.execute(pool) .execute(pool)
.await?; .await?;
} }

View File

@ -1,25 +1,27 @@
use actix_web::web::Redirect; use actix_web::web::Redirect;
use actix_web::{HttpResponse, get, Responder, web}; use actix_web::{get, web, Responder};
use serde::Deserialize; 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 sqlx::MySqlPool;
use crate::spotify_types::*; use std::{collections::HashMap, env};
use crate::joint_err::JointErr; 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) { fn read_from_env() -> (String, String, String) {
let id = env::var("CLIENT_ID").expect("Cannot proceed without client id"); 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 secret = env::var("CLIENT_SECRET").expect("Cannot proceed without client secret");
let url = env::var("REDIRECT_URL").expect("Cannot proceed without redirect url"); let url = env::var("REDIRECT_URL").expect("Cannot proceed without redirect url");
return (id, secret, url) (id, secret, url)
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@ -28,7 +30,10 @@ pub struct CallbackData {
} }
#[get("/callback")] #[get("/callback")]
pub async fn callback_manager(data: web::Query<CallbackData>, state: web::Data<crate::state::AppState>) -> impl Responder { pub async fn callback_manager(
data: web::Query<CallbackData>,
state: web::Data<crate::state::AppState>,
) -> impl Responder {
log::warn!("Access to /callback"); log::warn!("Access to /callback");
let pool = &state.pool; let pool = &state.pool;
@ -37,29 +42,34 @@ pub async fn callback_manager(data: web::Query<CallbackData>, state: web::Data<c
log::info!("{:?}", tokens.refresh_token); log::info!("{:?}", tokens.refresh_token);
let user_profile = get_user_email(&tokens.access_token).await; let user_profile = get_user_email(&tokens.access_token).await;
if let Ok(up) = user_profile { if let Ok(up) = user_profile {
let q = add_user_dont_care(&up.email, &up.id, &tokens.refresh_token, &pool).await; let q = add_user_dont_care(&up.email, &up.id, &tokens.refresh_token, pool).await;
match q { match q {
Ok(AddUserSuccess::New) => { Ok(AddUserSuccess::New) => {
log::warn!("Added a new user: {}", up.id); log::warn!("Added a new user: {}", up.id);
Redirect::to(format!("/refresh?spotify_id={}&force=true", up.id)) Redirect::to(format!("/refresh?spotify_id={}&force=true", up.id))
}, }
Ok(AddUserSuccess::Duplicate) => { Ok(AddUserSuccess::Duplicate) => {
Redirect::to(format!("/refresh?spotify_id={}", up.id)) Redirect::to(format!("/refresh?spotify_id={}", up.id))
}, }
_ => { _ => {
log::error!("SQL Query Failed: couldn't add new user\n{:?}", q); log::error!("SQL Query Failed: couldn't add new user\n{:?}", q);
Redirect::to("/failed") Redirect::to("/failed")
} }
} }
} else { } 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") Redirect::to("/failed")
} }
} else { } else {
log::error!("We failed to get tokens, the error was:\n{:?}", tokens.unwrap_err()); log::error!(
"We failed to get tokens, the error was:\n{:?}",
tokens.unwrap_err()
);
Redirect::to("/failed") Redirect::to("/failed")
} }
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
@ -75,7 +85,7 @@ pub struct Token(pub String);
async fn get_auth_token(code: &str) -> Result<AuthorizationRequestResponse, reqwest::Error> { async fn get_auth_token(code: &str) -> Result<AuthorizationRequestResponse, reqwest::Error> {
// Consider using env for REDIRECT_URI // Consider using env for REDIRECT_URI
//const REDIRECT_URI: &'static str = "https://ilia.moe/cool-stuff/cool-spotify-blend/callback"; //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(); let (id, secret, redirect_uri) = read_from_env();
@ -87,7 +97,8 @@ async fn get_auth_token(code: &str) -> Result<AuthorizationRequestResponse, reqw
params.insert("client_secret", secret); params.insert("client_secret", secret);
let client = reqwest::Client::new(); let client = reqwest::Client::new();
let res = client.post(URL.to_owned() ) let res = client
.post(URL.to_owned())
.query(&params) .query(&params)
.header("Content-Type", "application/x-www-form-urlencoded") .header("Content-Type", "application/x-www-form-urlencoded")
.header("Content-Length", 0) .header("Content-Length", 0)
@ -99,10 +110,9 @@ async fn get_auth_token(code: &str) -> Result<AuthorizationRequestResponse, reqw
Ok(res) Ok(res)
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
struct RefreshTokenForIdRes { struct RefreshTokenForIdRes {
access_token: Token access_token: Token,
} }
pub async fn refresh_token_for_id(spotify_id: &str, pool: &MySqlPool) -> Result<Token, JointErr> { pub async fn refresh_token_for_id(spotify_id: &str, pool: &MySqlPool) -> Result<Token, JointErr> {
@ -117,7 +127,8 @@ pub async fn refresh_token_for_id(spotify_id: &str, pool: &MySqlPool) -> Result<
params.insert("client_secret", &secret); params.insert("client_secret", &secret);
let client = reqwest::Client::new(); 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-Type", "application/x-www-form-urlencoded")
.header("Content-Length", 0) .header("Content-Length", 0)
.query(&params) .query(&params)
@ -137,7 +148,8 @@ struct UserProfile {
async fn get_user_email(token: &Token) -> Result<UserProfile, reqwest::Error> { async fn get_user_email(token: &Token) -> Result<UserProfile, reqwest::Error> {
let client = reqwest::Client::new(); 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) .header("Authorization", "Bearer ".to_owned() + &token.0)
.send() .send()
.await? .await?
@ -146,3 +158,30 @@ async fn get_user_email(token: &Token) -> Result<UserProfile, reqwest::Error> {
Ok(res) Ok(res)
} }
#[derive(Debug, Deserialize)]
#[non_exhaustive]
struct ClientCredentialsResponse {
pub access_token: Token,
}
pub async fn get_client_credentials() -> Result<Token, reqwest::Error> {
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(&params)
.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)
}

View File

@ -1,12 +1,10 @@
use std::collections::HashMap; use std::collections::HashMap;
use serde::Deserialize; use serde::Deserialize;
use crate::spotify_auth::Token; use crate::spotify_auth::Token;
use crate::spotify_types::*; use crate::spotify_types::*;
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct TopSongsResponse { pub struct TopSongsResponse {
pub href: String, pub href: String,
@ -22,9 +20,9 @@ pub async fn get_top_tracks(token: &Token) -> Result<TopSongsResponse, reqwest::
let mut params = HashMap::new(); let mut params = HashMap::new();
params.insert("time_range", "short_term"); params.insert("time_range", "short_term");
let client = reqwest::Client::new(); let client = reqwest::Client::new();
let res: TopSongsResponse = client.get(crate::BASE_URL.to_owned() + "/me/top/tracks") let res: TopSongsResponse = client
.get(crate::BASE_URL.to_owned() + "/me/top/tracks")
.query(&params) .query(&params)
.header("Authorization", "Bearer ".to_owned() + &token.0) .header("Authorization", "Bearer ".to_owned() + &token.0)
.send() .send()
@ -33,5 +31,4 @@ pub async fn get_top_tracks(token: &Token) -> Result<TopSongsResponse, reqwest::
.await?; .await?;
Ok(res) Ok(res)
} }

View File

@ -2,23 +2,25 @@ use std::collections::HashMap;
use sqlx::MySqlPool; use sqlx::MySqlPool;
use crate::{spotify_types::*, spotify_auth::Token}; use crate::{spotify_auth::Token, spotify_types::*};
pub async fn add_to_playlist(id: &str, uris: Vec<Uri>, token: &Token) -> Result<(), reqwest::Error> { pub async fn add_to_playlist(
id: &str,
uris: Vec<Uri>,
token: &Token,
) -> Result<(), reqwest::Error> {
let mut params = HashMap::new(); let mut params = HashMap::new();
params.insert("position", "0"); // Insert at top, we could remove to append params.insert("position", "0"); // Insert at top, we could remove to append
let mut body: HashMap<String, Vec<String>> = HashMap::new(); let mut body: HashMap<String, Vec<String>> = HashMap::new();
let uris: Vec<String> = uris.iter() let uris: Vec<String> = uris.iter().map(|u| &u.0).cloned().collect();
.map(|u| &u.0)
.cloned()
.collect();
body.insert("uris".to_owned(), uris); body.insert("uris".to_owned(), uris);
log::info!("Uri body: {:?}", body); log::info!("Uri body: {:?}", body);
let client = reqwest::Client::new(); 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(&params) .query(&params)
.json(&body) .json(&body)
.header("Authorization", "Bearer ".to_owned() + &token.0) .header("Authorization", "Bearer ".to_owned() + &token.0)
@ -29,18 +31,19 @@ pub async fn add_to_playlist(id: &str, uris: Vec<Uri>, token: &Token) -> Result<
} }
// WE CANNOT SET MORE THAN 100 AT ONCE!!! // WE CANNOT SET MORE THAN 100 AT ONCE!!!
pub async fn set_playlist(id: &str, uris: Vec<Uri>, token: &Token, pool: &MySqlPool) pub async fn set_playlist(
-> Result<(), reqwest::Error> { id: &str,
uris: Vec<Uri>,
token: &Token,
_pool: &MySqlPool,
) -> Result<(), reqwest::Error> {
let mut body: HashMap<String, Vec<String>> = HashMap::new(); let mut body: HashMap<String, Vec<String>> = HashMap::new();
let uris: Vec<String> = uris.iter() let uris: Vec<String> = uris.iter().map(|u| &u.0).cloned().collect();
.map(|u| &u.0)
.cloned()
.collect();
body.insert("uris".to_owned(), uris); body.insert("uris".to_owned(), uris);
let client = reqwest::Client::new(); 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) .json(&body)
.header("Authorization", "Bearer ".to_owned() + &token.0) .header("Authorization", "Bearer ".to_owned() + &token.0)
.send() .send()

View File

@ -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<Vec<TrackObject>, reqwest::Error> {
let pieces = tracks.chunks(50);
let mut out = Vec::<TrackObject>::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<TrackObject>,
}
// We can only get 50 at a time!
async fn get_tracks_helper(
token: &Token,
tracks: &[&Uri],
) -> Result<Vec<TrackObject>, reqwest::Error> {
let mut params: HashMap<&str, String> = HashMap::new();
let tracks_str: String = tracks
.iter()
.map(|uri| uri.get_suffix())
.collect::<Vec<String>>()
.join(",");
params.insert("ids", tracks_str);
let client = reqwest::Client::new();
let res = client
.get(BASE_URL.to_owned() + "/tracks")
.query(&params)
.bearer_auth(token.0.clone())
.send()
.await?
.json::<TracksResult>()
.await?
.tracks;
Ok(res)
}

View File

@ -1,6 +1,6 @@
use serde::Deserialize; use serde::{Deserialize, Serialize};
#[derive(Deserialize, Debug, Clone)] #[derive(Deserialize, Serialize, Debug, Clone)]
#[non_exhaustive] #[non_exhaustive]
pub struct TrackObject { pub struct TrackObject {
pub album: Option<AlbumObject>, pub album: Option<AlbumObject>,
@ -11,7 +11,7 @@ pub struct TrackObject {
pub uri: Uri, pub uri: Uri,
} }
#[derive(Deserialize, Debug, Clone)] #[derive(Deserialize, Serialize, Debug, Clone)]
#[non_exhaustive] #[non_exhaustive]
pub struct AlbumObject { pub struct AlbumObject {
pub album_type: String, pub album_type: String,
@ -22,10 +22,9 @@ pub struct AlbumObject {
pub release_date: String, pub release_date: String,
pub uri: Uri, pub uri: Uri,
pub genres: Option<String>, pub genres: Option<String>,
} }
#[derive(Deserialize, Debug, Clone)] #[derive(Deserialize, Serialize, Debug, Clone)]
#[non_exhaustive] #[non_exhaustive]
pub struct ArtistObject { pub struct ArtistObject {
pub href: String, pub href: String,
@ -33,5 +32,32 @@ pub struct ArtistObject {
pub name: String, pub name: String,
} }
#[derive(Deserialize, Debug, Clone)] #[derive(Deserialize, Serialize, Debug, Clone)]
pub struct Uri(pub String); 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<String>,
images: Option<Vec<ImageObject>>,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
#[non_exhaustive]
pub struct ImageObject {
pub url: String,
}

View File

@ -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<Vec<UserProfile>, 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<UserProfile> = 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<HashMap<SpotifyId, UserProfile>, reqwest::Error> {
let client = reqwest::Client::new();
let mut map: HashMap<SpotifyId, UserProfile> = 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::<UserProfile>()
.await?;
map.insert(SpotifyId(user.0.clone()), res);
}
Ok(map)
}

View File

@ -1,33 +1,56 @@
use sqlx::{MySqlPool, mysql::MySqlQueryResult};
use crate::spotify_types::*; use crate::spotify_types::*;
use sqlx::{
MySqlPool,
};
#[allow(non_snake_case)] // Fixes warning for macro #[allow(non_snake_case)] // Fixes warning for macro
pub async fn even_allocation(pool: &MySqlPool) -> Result<Vec<Uri>, sqlx::Error> { pub async fn even_allocation(pool: &MySqlPool) -> Result<Vec<Uri>, sqlx::Error> {
let users: Vec<u32> = sqlx::query!(" let users: Vec<u32> = sqlx::query!(
"
SELECT UserId From Users SELECT UserId From Users
") "
)
.fetch_all(pool) .fetch_all(pool)
.await? .await?
.iter() .iter()
.map(|row| row.UserId) .map(|row| row.UserId)
.collect(); .collect();
let mut uris: Vec<Uri> = Vec::new(); // Reset the SQL playlist representation
sqlx::query!("UPDATE Tracks SET InPlaylist = 0")
.execute(pool)
.await?;
let num: u32 = (100 / users.len()) as u32; let num: u32 = (100 / users.len()) as u32;
for user in users { for user in users {
let mut uri_list: Vec<Uri> = sqlx::query!(" sqlx::query!(
SELECT Uri FROM Tracks "
UPDATE Tracks SET InPlaylist = 1
WHERE TrackID In
(
SELECT TrackId FROM Tracks
WHERE UserId = ? WHERE UserId = ?
)
ORDER BY RAND() ORDER BY RAND()
LIMIT ? LIMIT ?
", user, num) ",
user,
num
)
.execute(pool)
.await?;
}
let uris = sqlx::query!(
"
SELECT Uri FROM Tracks
WHERE InPlaylist = 1
"
)
.fetch_all(pool) .fetch_all(pool)
.await? .await?
.iter() .iter()
.map(|row| Uri(row.Uri.clone())) .map(|row| Uri(row.Uri.clone()))
.collect(); .collect();
uris.append(&mut uri_list);
}
Ok(uris) Ok(uris)
} }

View File

@ -1,26 +1,36 @@
use sqlx::{MySqlPool, mysql::MySqlQueryResult}; use sqlx::{mysql::MySqlQueryResult, MySqlPool};
use crate::spotify_auth::Token; use crate::spotify_auth::Token;
async fn add_user(email: &str, spotify_id: &str, refresh_token: &Token, pool: &MySqlPool) async fn add_user(
-> Result<MySqlQueryResult, sqlx::Error> { email: &str,
Ok(sqlx::query!( spotify_id: &str,
refresh_token: &Token,
pool: &MySqlPool,
) -> Result<MySqlQueryResult, sqlx::Error> {
sqlx::query!(
"INSERT INTO spotify.Users (Email, SpotifyId, RefreshToken) "INSERT INTO spotify.Users (Email, SpotifyId, RefreshToken)
VALUES (?, ?, ?)", VALUES (?, ?, ?)",
email.to_owned(), email.to_owned(),
spotify_id.to_owned(), spotify_id.to_owned(),
refresh_token.0.to_owned() refresh_token.0.to_owned()
).execute(pool).await?) )
.execute(pool)
.await
} }
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub enum AddUserSuccess { pub enum AddUserSuccess {
New, New,
Duplicate Duplicate,
} }
pub async fn add_user_dont_care(email: &str, spotify_id: &str, refresh_token: &Token, pool: &MySqlPool) pub async fn add_user_dont_care(
-> Result<AddUserSuccess, sqlx::Error> { email: &str,
spotify_id: &str,
refresh_token: &Token,
pool: &MySqlPool,
) -> Result<AddUserSuccess, sqlx::Error> {
let q = add_user(email, spotify_id, refresh_token, pool).await; let q = add_user(email, spotify_id, refresh_token, pool).await;
match q { match q {
Ok(_) => Ok(AddUserSuccess::New), Ok(_) => Ok(AddUserSuccess::New),
@ -30,8 +40,8 @@ pub async fn add_user_dont_care(email: &str, spotify_id: &str, refresh_token: &T
} else { } else {
Err(sqlx::Error::Database(err)) Err(sqlx::Error::Database(err))
} }
}, }
Err(_) => Err(q.unwrap_err()) Err(_) => Err(q.unwrap_err()),
} }
} }
@ -46,41 +56,49 @@ impl From<sqlx::Error> for GetRefreshTokenByIdErr {
} }
#[allow(non_snake_case)] // Fixes warning for macro #[allow(non_snake_case)] // Fixes warning for macro
pub async fn get_refresh_token_by_id(spotify_id: &str, pool: &MySqlPool) pub async fn get_refresh_token_by_id(
-> Result<Token, GetRefreshTokenByIdErr> { spotify_id: &str,
let res = sqlx::query!("SELECT RefreshToken FROM Users WHERE SpotifyId LIKE ?", pool: &MySqlPool,
spotify_id.to_owned()) ) -> Result<Token, GetRefreshTokenByIdErr> {
let res = sqlx::query!(
"SELECT RefreshToken FROM Users WHERE SpotifyId LIKE ?",
spotify_id.to_owned()
)
.fetch_one(pool) .fetch_one(pool)
.await?; .await?;
match res.RefreshToken { match res.RefreshToken {
Some(token) => Ok(Token(token)), Some(token) => Ok(Token(token)),
None => Err(GetRefreshTokenByIdErr::Empty) None => Err(GetRefreshTokenByIdErr::Empty),
} }
} }
#[allow(non_snake_case)] // Fixes warning for macro #[allow(non_snake_case)] // Fixes warning for macro
pub async fn get_refreshed_by_id(spotify_id: &str, pool: &MySqlPool) pub async fn get_refreshed_by_id(
-> Result<sqlx::types::time::OffsetDateTime, sqlx::Error> { spotify_id: &str,
let res = sqlx::query!("SELECT LastRefreshed FROM Users WHERE SpotifyID LIKE ?", pool: &MySqlPool,
spotify_id.to_owned()) ) -> Result<sqlx::types::time::OffsetDateTime, sqlx::Error> {
let res = sqlx::query!(
"SELECT LastRefreshed FROM Users WHERE SpotifyID LIKE ?",
spotify_id.to_owned()
)
.fetch_one(pool) .fetch_one(pool)
.await?; .await?;
Ok(res.LastRefreshed) Ok(res.LastRefreshed)
} }
#[allow(non_snake_case)] // Fixes warning for macro #[allow(non_snake_case)] // Fixes warning for macro
pub async fn get_internal_id_by_id(spotify_id: &str, pool: &MySqlPool) pub async fn get_internal_id_by_id(spotify_id: &str, pool: &MySqlPool) -> Result<u32, sqlx::Error> {
-> Result<u32, sqlx::Error> { let res = sqlx::query!(
let res = sqlx::query!("SELECT UserId FROM Users WHERE SpotifyID LIKE ?", "SELECT UserId FROM Users WHERE SpotifyID LIKE ?",
spotify_id.to_owned()) spotify_id.to_owned()
)
.fetch_one(pool) .fetch_one(pool)
.await?; .await?;
Ok(res.UserId) Ok(res.UserId)
} }
#[allow(non_snake_case)] // Fixes warning for macro #[allow(non_snake_case)] // Fixes warning for macro
pub async fn get_emilia_spotify_id(pool: &MySqlPool) pub async fn get_emilia_spotify_id(pool: &MySqlPool) -> Result<String, sqlx::Error> {
-> Result<String, sqlx::Error> {
let res = sqlx::query!("SELECT SpotifyId FROM Users WHERE IsEmilia = 1") let res = sqlx::query!("SELECT SpotifyId FROM Users WHERE IsEmilia = 1")
.fetch_one(pool) .fetch_one(pool)
.await?; .await?;

62
server/src/status.rs Normal file
View File

@ -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<crate::state::AppState>) -> 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<Vec<(Uri, SpotifyId)>, 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())
}