Add new status endpoint
Returns JSON and attributes songs in the playlist to users. Note change to database schema!!!
This commit is contained in:
parent
39e098c4ca
commit
c7f34f416a
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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?;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(¶ms)
|
.query(¶ms)
|
||||||
.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(¶ms)
|
.query(¶ms)
|
||||||
|
@ -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(¶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)
|
||||||
|
}
|
||||||
|
|
|
@ -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(¶ms)
|
.query(¶ms)
|
||||||
.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)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(¶ms)
|
.query(¶ms)
|
||||||
.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()
|
||||||
|
|
|
@ -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(¶ms)
|
||||||
|
.bearer_auth(token.0.clone())
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.json::<TracksResult>()
|
||||||
|
.await?
|
||||||
|
.tracks;
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
|
@ -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,
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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?;
|
||||||
|
|
|
@ -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())
|
||||||
|
}
|
Loading…
Reference in New Issue