Compare commits
No commits in common. "main" and "v0.1.0" have entirely different histories.
|
@ -1,3 +1,2 @@
|
|||
/target
|
||||
.env
|
||||
dist/
|
||||
|
|
|
@ -370,21 +370,6 @@ dependencies = [
|
|||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cool_spotify_server"
|
||||
version = "0.1.1"
|
||||
dependencies = [
|
||||
"actix-web",
|
||||
"dotenvy",
|
||||
"env_logger",
|
||||
"log",
|
||||
"openssl",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sqlx",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.3"
|
||||
|
@ -620,17 +605,6 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "front"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"gloo-net",
|
||||
"js-sys",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-logger",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
|
@ -705,40 +679,6 @@ dependencies = [
|
|||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gloo-net"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3000ef231a67d5bfee6b35f2c0f6f5c8d45b3381ef5bbbea603690ec4e539762"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"gloo-utils",
|
||||
"http",
|
||||
"js-sys",
|
||||
"pin-project",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gloo-utils"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.3.20"
|
||||
|
@ -1313,26 +1253,6 @@ version = "2.3.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "030ad2bc4db10a8944cb0d837f158bdfec4d4a4873ab701a95046770d11f8842"
|
||||
dependencies = [
|
||||
"pin-project-internal",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-internal"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.22",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.9"
|
||||
|
@ -1635,6 +1555,21 @@ 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"
|
||||
|
@ -2158,17 +2093,6 @@ version = "0.2.87"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-logger"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "074649a66bb306c8f2068c9016395fa65d8e08d2affcbf95acf3c24c3ab19718"
|
||||
dependencies = [
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.64"
|
||||
|
|
|
@ -6,27 +6,3 @@ edition = "2021"
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
wasm-bindgen = "0.2.74"
|
||||
js-sys = "0.3.51"
|
||||
wasm-bindgen-futures = "0.4.24"
|
||||
|
||||
gloo-net = { version = "0.3", features = ["http"] }
|
||||
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
||||
log = "0.4"
|
||||
wasm-logger = "0.2"
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3.4"
|
||||
features = [
|
||||
'Window',
|
||||
'Document',
|
||||
'Element',
|
||||
'HtmlElement',
|
||||
'Node',
|
||||
'HtmlAnchorElement',
|
||||
'HtmlImageElement',
|
||||
]
|
||||
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
[build]
|
||||
release = true
|
||||
public_url = "/cool-stuff/cool-spotify-blend/"
|
|
@ -1,86 +0,0 @@
|
|||
@use "sass:math";
|
||||
|
||||
$card-height: 8vh;
|
||||
$card-gap: 1.5vh;
|
||||
|
||||
body {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
font-family: sans-serif;
|
||||
color: black;
|
||||
}
|
||||
|
||||
img {
|
||||
}
|
||||
|
||||
.outer-div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.inner-div {
|
||||
height: $card-height;
|
||||
margin-top: ($card-gap * 0.5);
|
||||
margin-bottom: ($card-gap * 0.5);
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
|
||||
|
||||
&:nth-child(odd) {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
}
|
||||
|
||||
.track-info {
|
||||
height: 100%;
|
||||
width: 70%;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: ($card-height * 1.1) 1fr;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
align-items: center;
|
||||
|
||||
img {
|
||||
height: $card-height;
|
||||
grid-column: 1 / 2;
|
||||
grid-row: 1 / span 2;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.track_name {
|
||||
grid-column: 2 / span 1;
|
||||
grid-row: 1 / span 1;
|
||||
}
|
||||
.track_album {
|
||||
grid-column: 2 / span 1;
|
||||
grid-row: 2 / span 1;
|
||||
}
|
||||
}
|
||||
|
||||
.user-info {
|
||||
height: 100%;
|
||||
width: 30%;
|
||||
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
|
||||
img {
|
||||
height: $card-height;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
margin-right: ($card-height * 0.1);
|
||||
padding: 0;
|
||||
|
||||
text-align: right;
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link data-trunk rel="scss" href="assets/scss/index.scss">
|
||||
<title>Cool Spotify Blend</title>
|
||||
</head>
|
||||
</html>
|
|
@ -1,11 +0,0 @@
|
|||
use gloo_net::http::Request;
|
||||
|
||||
use crate::spotify_types::*;
|
||||
|
||||
pub async fn fetch() -> Result<Vec<(TrackObject, UserProfile)>, gloo_net::Error> {
|
||||
Ok(Request::get("https://spotify.api.ilia.moe/status")
|
||||
.send()
|
||||
.await?
|
||||
.json()
|
||||
.await?)
|
||||
}
|
|
@ -1,144 +1,3 @@
|
|||
mod fetch;
|
||||
mod spotify_types; // Symlinked from server because I do not want to separate this into another
|
||||
// crate right now.
|
||||
// This is a stupid way to do this.
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen_futures::spawn_local;
|
||||
use web_sys::{Document, HtmlAnchorElement, HtmlImageElement};
|
||||
|
||||
use crate::spotify_types::*;
|
||||
|
||||
// Convenience alias
|
||||
type TrackUserTuple = (TrackObject, UserProfile);
|
||||
|
||||
fn main() {
|
||||
wasm_logger::init(wasm_logger::Config::default());
|
||||
|
||||
log::info!("wasm executing now :)");
|
||||
spawn_local(app());
|
||||
}
|
||||
|
||||
async fn app() {
|
||||
let window = web_sys::window().expect("We have to have a window.");
|
||||
let document = window.document().expect("Window should have a document.");
|
||||
let body = document.body().expect("Document should have a body.");
|
||||
|
||||
let outer_div = generate_outer_div(&document).unwrap();
|
||||
body.append_child(&outer_div).unwrap();
|
||||
|
||||
let data = fetch::fetch().await;
|
||||
if data.is_err() {
|
||||
log::error!("Could not fetch data, nothing to do.");
|
||||
log::error!("{:?}", data.unwrap_err());
|
||||
return (); // This is actually a graceful exit!
|
||||
}
|
||||
let data = data.unwrap(); // We know this is fine from above
|
||||
let rows = data.iter()
|
||||
.map(|row| generate_row(&document, &row))
|
||||
.flatten(); // Intentionally not collecting--next step is to consume.
|
||||
for row in rows {
|
||||
outer_div.append_child(&row).unwrap();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fn generate_outer_div(document: &Document) -> Result<web_sys::Element, JsValue> {
|
||||
let outer_div = document.create_element("div")?;
|
||||
outer_div.set_class_name("outer-div");
|
||||
Ok(outer_div)
|
||||
}
|
||||
|
||||
fn generate_row(document: &Document, data: &TrackUserTuple) -> Result<web_sys::Element, JsValue> {
|
||||
let container = document.create_element("div")?;
|
||||
container.set_class_name("inner-div");
|
||||
|
||||
let track_info = generate_track_info(document, data)?;
|
||||
let user_info = generate_user_info(document, data)?;
|
||||
|
||||
container.append_child(&track_info).unwrap();
|
||||
container.append_child(&user_info).unwrap();
|
||||
|
||||
Ok(container)
|
||||
}
|
||||
|
||||
fn generate_track_info(document: &Document, data: &TrackUserTuple) -> Result<web_sys::Element, JsValue> {
|
||||
let track_info = document.create_element("div")?;
|
||||
track_info.set_class_name("track-info");
|
||||
|
||||
// Scoping these to prevent accidentally referencing the wrong thing
|
||||
// and to free memory sooner maybe
|
||||
{
|
||||
let track_name = document.create_element("p")?;
|
||||
track_name.set_class_name("track_name");
|
||||
let track_name_a: HtmlAnchorElement = document.create_element("a")?.dyn_into().unwrap();
|
||||
track_name_a.set_text_content(Some(&data.0.name));
|
||||
track_name_a.set_href(&create_track_link(&data.0.uri));
|
||||
track_name.append_child(&track_name_a).unwrap();
|
||||
track_info.append_child(&track_name).unwrap();
|
||||
}
|
||||
|
||||
if let Some(album) = &data.0.album {
|
||||
{
|
||||
let track_album = document.create_element("p")?;
|
||||
track_album.set_class_name("track_album");
|
||||
let track_album_a: HtmlAnchorElement = document.create_element("a")?.dyn_into().unwrap();
|
||||
track_album_a.set_text_content(Some(&album.name));
|
||||
track_album_a.set_href(&create_album_link(&album.uri));
|
||||
|
||||
track_album.append_child(&track_album_a).unwrap();
|
||||
|
||||
track_info.append_child(&track_album).unwrap();
|
||||
}
|
||||
if let Some(image) = &album.images.get(0) {
|
||||
let track_album_img: HtmlImageElement = document.create_element("img")?.dyn_into().unwrap();
|
||||
track_album_img.set_alt("Album cover");
|
||||
track_album_img.set_src(&image.url);
|
||||
|
||||
track_info.append_child(&track_album_img).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(track_info)
|
||||
}
|
||||
|
||||
fn generate_user_info(document: &Document, data: &TrackUserTuple) -> Result<web_sys::Element, JsValue> {
|
||||
let user_info = document.create_element("div")?;
|
||||
user_info.set_class_name("user-info");
|
||||
|
||||
{
|
||||
let user_name = document.create_element("p")?;
|
||||
user_name.set_class_name("user_name");
|
||||
|
||||
let user_name_a: HtmlAnchorElement = document.create_element("a")?.dyn_into().unwrap();
|
||||
if let Some(display_name) = &data.1.display_name {
|
||||
user_name_a.set_text_content(Some(&display_name));
|
||||
} else {
|
||||
user_name_a.set_text_content(Some(&data.1.id));
|
||||
}
|
||||
user_name_a.set_href(&create_user_link(&data.1.id));
|
||||
user_name.append_child(&user_name_a).unwrap();
|
||||
|
||||
user_info.append_child(&user_name).unwrap();
|
||||
}
|
||||
|
||||
if let Some(Some(image)) = data.1.images.as_ref().map(|imgs| imgs.get(0)) {
|
||||
let user_img: HtmlImageElement = document.create_element("img")?.dyn_into().unwrap();
|
||||
user_img.set_alt("Profile picture");
|
||||
user_img.set_src(&image.url);
|
||||
|
||||
user_info.append_child(&user_img).unwrap();
|
||||
}
|
||||
|
||||
Ok(user_info)
|
||||
}
|
||||
|
||||
fn create_track_link(uri: &Uri) -> String {
|
||||
"https://open.spotify.com/track/".to_owned() + &uri.get_suffix()
|
||||
}
|
||||
fn create_album_link(uri: &Uri) -> String {
|
||||
"https://open.spotify.com/album/".to_owned() + &uri.get_suffix()
|
||||
}
|
||||
fn create_user_link(id: &str) -> String {
|
||||
"https://open.spotify.com/user/".to_owned() + id
|
||||
println!("Hello, world!");
|
||||
}
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
../../server/src/spotify_types.rs
|
|
@ -1,9 +1,12 @@
|
|||
[package]
|
||||
name = "cool_spotify_server"
|
||||
version = "0.1.1"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["Emilia Allison"]
|
||||
|
||||
[[bin]]
|
||||
name = "server"
|
||||
|
||||
[dependencies]
|
||||
actix-web = "4"
|
||||
dotenvy = "0.15"
|
||||
|
|
|
@ -1,98 +0,0 @@
|
|||
-- MariaDB dump 10.19-11.0.2-MariaDB, for Linux (x86_64)
|
||||
--
|
||||
-- Host: localhost Database: spotify
|
||||
-- ------------------------------------------------------
|
||||
-- Server version 11.0.2-MariaDB
|
||||
|
||||
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
|
||||
/*!40101 SET NAMES utf8mb4 */;
|
||||
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
|
||||
/*!40103 SET TIME_ZONE='+00:00' */;
|
||||
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
|
||||
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
|
||||
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
|
||||
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
|
||||
|
||||
--
|
||||
-- Temporary table structure for view `CurrentPlaylist`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `CurrentPlaylist`;
|
||||
/*!50001 DROP VIEW IF EXISTS `CurrentPlaylist`*/;
|
||||
SET @saved_cs_client = @@character_set_client;
|
||||
SET character_set_client = utf8;
|
||||
/*!50001 CREATE VIEW `CurrentPlaylist` AS SELECT
|
||||
1 AS `TrackId`,
|
||||
1 AS `Uri`,
|
||||
1 AS `SpotifyId` */;
|
||||
SET character_set_client = @saved_cs_client;
|
||||
|
||||
--
|
||||
-- Table structure for table `Tracks`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `Tracks`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `Tracks` (
|
||||
`TrackId` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`UserId` int(10) unsigned NOT NULL,
|
||||
`Uri` varchar(100) NOT NULL,
|
||||
`DateAdded` date NOT NULL,
|
||||
`InPlaylist` tinyint(1) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`TrackId`),
|
||||
UNIQUE KEY `Tracks_UN` (`Uri`),
|
||||
KEY `Tracks_FK` (`UserId`),
|
||||
CONSTRAINT `Tracks_FK` FOREIGN KEY (`UserId`) REFERENCES `Users` (`UserId`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=602 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Table structure for table `Users`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `Users`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `Users` (
|
||||
`UserId` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`Email` varchar(150) NOT NULL,
|
||||
`RefreshToken` varchar(200) DEFAULT NULL,
|
||||
`SpotifyId` varchar(100) NOT NULL,
|
||||
`LastRefreshed` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||
`IsEmilia` tinyint(1) DEFAULT NULL,
|
||||
PRIMARY KEY (`UserId`),
|
||||
UNIQUE KEY `Users_UN` (`Email`,`SpotifyId`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Final view structure for view `CurrentPlaylist`
|
||||
--
|
||||
|
||||
/*!50001 DROP VIEW IF EXISTS `CurrentPlaylist`*/;
|
||||
/*!50001 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50001 SET @saved_cs_results = @@character_set_results */;
|
||||
/*!50001 SET @saved_col_connection = @@collation_connection */;
|
||||
/*!50001 SET character_set_client = utf8mb3 */;
|
||||
/*!50001 SET character_set_results = utf8mb3 */;
|
||||
/*!50001 SET collation_connection = utf8mb3_general_ci */;
|
||||
/*!50001 CREATE ALGORITHM=UNDEFINED */
|
||||
/*!50013 DEFINER=`root`@`localhost` SQL SECURITY DEFINER */
|
||||
/*!50001 VIEW `CurrentPlaylist` AS select `t`.`TrackId` AS `TrackId`,`t`.`Uri` AS `Uri`,`u`.`SpotifyId` AS `SpotifyId` from (`Tracks` `t` join `Users` `u` on(`t`.`UserId` = `u`.`UserId`)) where `t`.`InPlaylist` = 1 */;
|
||||
/*!50001 SET character_set_client = @saved_cs_client */;
|
||||
/*!50001 SET character_set_results = @saved_cs_results */;
|
||||
/*!50001 SET collation_connection = @saved_col_connection */;
|
||||
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
|
||||
|
||||
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
|
||||
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
|
||||
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
|
||||
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
||||
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
|
||||
|
||||
-- Dump completed on 2023-07-12 22:28:00
|
|
@ -1,33 +1,29 @@
|
|||
mod spotify_auth;
|
||||
mod spotify_get_top;
|
||||
mod spotify_playlist;
|
||||
mod spotify_tracks;
|
||||
mod spotify_types;
|
||||
mod spotify_users;
|
||||
|
||||
mod sql_tracks;
|
||||
mod sql_users;
|
||||
mod sql_tracks;
|
||||
|
||||
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::{get, web, App, HttpResponse, HttpServer, Responder};
|
||||
use actix_web::{App, HttpServer, HttpResponse, get, Responder, web};
|
||||
use dotenvy::dotenv;
|
||||
use status::status_manager;
|
||||
|
||||
use std::env;
|
||||
|
||||
const BASE_URL: &str = "https://api.spotify.com/v1";
|
||||
const BASE_URL: &'static str = "https://api.spotify.com/v1";
|
||||
|
||||
#[get("/")]
|
||||
async fn root(data: web::Data<state::AppState>) -> impl Responder {
|
||||
log::warn!("Access to root");
|
||||
HttpResponse::Ok().body(format!("hi, pls go to:\n {}\n k thx", data.auth_url))
|
||||
HttpResponse::Ok().body(format!("hi, pls go to: {}\n k thx", data.auth_url))
|
||||
}
|
||||
|
||||
#[get("/failed")]
|
||||
|
@ -49,8 +45,7 @@ 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");
|
||||
|
@ -72,7 +67,6 @@ async fn main() -> std::io::Result<()> {
|
|||
.service(failed)
|
||||
.service(refresh_manager)
|
||||
.service(refresh_all_manager)
|
||||
.service(status_manager)
|
||||
})
|
||||
.bind((address, port))?
|
||||
.run()
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
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::joint_err::JointErr;
|
||||
use crate::spotify_get_top::get_top_tracks;
|
||||
use crate::{spotify_auth, spotify_playlist, sql_tracks, sql_users};
|
||||
use crate::{sql_users, spotify_auth, sql_tracks, spotify_playlist};
|
||||
use crate::joint_err::JointErr;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct RefreshData {
|
||||
|
@ -15,8 +16,9 @@ pub struct RefreshData {
|
|||
#[get("/refresh")]
|
||||
pub async fn refresh_manager(
|
||||
data: web::Query<RefreshData>,
|
||||
state: web::Data<crate::state::AppState>,
|
||||
) -> impl Responder {
|
||||
state: web::Data<crate::state::AppState>
|
||||
) -> impl Responder
|
||||
{
|
||||
let pool = &state.pool;
|
||||
|
||||
if let Some(spotify_id) = &data.spotify_id {
|
||||
|
@ -39,55 +41,44 @@ pub async fn refresh_manager(
|
|||
|
||||
#[allow(non_snake_case)]
|
||||
#[get("/refresh_all")]
|
||||
pub async fn refresh_all_manager(state: web::Data<crate::state::AppState>) -> impl Responder {
|
||||
pub async fn refresh_all_manager(state: web::Data<crate::state::AppState>)
|
||||
-> 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(e) = refresh(&user.SpotifyId, pool).await {
|
||||
log::error!(
|
||||
"Tried to refresh for {}, failed with\n{:?}",
|
||||
&user.SpotifyId,
|
||||
e
|
||||
);
|
||||
if let Err(_) = refresh(&user.SpotifyId, pool).await {
|
||||
log::error!("Tried to refresh for {}, failed", &user.SpotifyId);
|
||||
}
|
||||
}
|
||||
HttpResponse::Ok().body("yeah ok")
|
||||
return HttpResponse::Ok().body("yeah ok");
|
||||
} else {
|
||||
HttpResponse::InternalServerError().body("where the users at")
|
||||
return 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
|
||||
|
|
|
@ -1,27 +1,25 @@
|
|||
use actix_web::web::Redirect;
|
||||
use actix_web::{get, web, Responder};
|
||||
use actix_web::{HttpResponse, get, Responder, web};
|
||||
use serde::Deserialize;
|
||||
|
||||
|
||||
use serde_json::json;
|
||||
use sqlx::MySqlPool;
|
||||
use std::{collections::HashMap, env};
|
||||
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 crate::joint_err::JointErr;
|
||||
|
||||
use crate::sql_users::{add_user_dont_care, get_refresh_token_by_id, GetRefreshTokenByIdErr, AddUserSuccess};
|
||||
|
||||
|
||||
|
||||
use crate::sql_users::{
|
||||
add_user_dont_care, get_refresh_token_by_id, AddUserSuccess,
|
||||
};
|
||||
|
||||
const URL: &str = "https://accounts.spotify.com/api/token";
|
||||
const URL: &'static 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");
|
||||
(id, secret, url)
|
||||
return (id, secret, url)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
@ -30,10 +28,7 @@ pub struct CallbackData {
|
|||
}
|
||||
|
||||
#[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");
|
||||
let pool = &state.pool;
|
||||
|
||||
|
@ -42,34 +37,29 @@ pub async fn callback_manager(
|
|||
log::info!("{:?}", tokens.refresh_token);
|
||||
let user_profile = get_user_email(&tokens.access_token).await;
|
||||
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 {
|
||||
Ok(AddUserSuccess::New) => {
|
||||
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)]
|
||||
|
@ -85,7 +75,7 @@ pub struct Token(pub String);
|
|||
async fn get_auth_token(code: &str) -> Result<AuthorizationRequestResponse, reqwest::Error> {
|
||||
// Consider using env for REDIRECT_URI
|
||||
//const REDIRECT_URI: &'static str = "https://ilia.moe/cool-stuff/cool-spotify-blend/callback";
|
||||
const GRANT_TYPE: &str = "authorization_code";
|
||||
const GRANT_TYPE: &'static str = "authorization_code";
|
||||
|
||||
let (id, secret, redirect_uri) = read_from_env();
|
||||
|
||||
|
@ -97,8 +87,7 @@ async fn get_auth_token(code: &str) -> Result<AuthorizationRequestResponse, reqw
|
|||
params.insert("client_secret", secret);
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let res = client
|
||||
.post(URL.to_owned())
|
||||
let res = client.post(URL.to_owned() )
|
||||
.query(¶ms)
|
||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||
.header("Content-Length", 0)
|
||||
|
@ -110,9 +99,10 @@ async fn get_auth_token(code: &str) -> Result<AuthorizationRequestResponse, reqw
|
|||
Ok(res)
|
||||
}
|
||||
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct RefreshTokenForIdRes {
|
||||
access_token: Token,
|
||||
access_token: Token
|
||||
}
|
||||
|
||||
pub async fn refresh_token_for_id(spotify_id: &str, pool: &MySqlPool) -> Result<Token, JointErr> {
|
||||
|
@ -127,8 +117,7 @@ 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)
|
||||
|
@ -148,8 +137,7 @@ struct UserProfile {
|
|||
|
||||
async fn get_user_email(token: &Token) -> Result<UserProfile, reqwest::Error> {
|
||||
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?
|
||||
|
@ -158,30 +146,3 @@ async fn get_user_email(token: &Token) -> Result<UserProfile, reqwest::Error> {
|
|||
|
||||
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,10 +1,12 @@
|
|||
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,
|
||||
|
@ -20,9 +22,9 @@ pub async fn get_top_tracks(token: &Token) -> Result<TopSongsResponse, reqwest::
|
|||
let mut params = HashMap::new();
|
||||
params.insert("time_range", "short_term");
|
||||
|
||||
|
||||
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)
|
||||
.header("Authorization", "Bearer ".to_owned() + &token.0)
|
||||
.send()
|
||||
|
@ -31,4 +33,5 @@ pub async fn get_top_tracks(token: &Token) -> Result<TopSongsResponse, reqwest::
|
|||
.await?;
|
||||
|
||||
Ok(res)
|
||||
|
||||
}
|
||||
|
|
|
@ -2,25 +2,23 @@ use std::collections::HashMap;
|
|||
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use crate::{spotify_auth::Token, spotify_types::*};
|
||||
use crate::{spotify_types::*, spotify_auth::Token};
|
||||
|
||||
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();
|
||||
params.insert("position", "0"); // Insert at top, we could remove to append
|
||||
|
||||
let mut body: HashMap<String, Vec<String>> = HashMap::new();
|
||||
let uris: Vec<String> = uris.iter().map(|u| &u.0).cloned().collect();
|
||||
let uris: Vec<String> = 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)
|
||||
|
@ -31,19 +29,18 @@ pub async fn add_to_playlist(
|
|||
}
|
||||
|
||||
// WE CANNOT SET MORE THAN 100 AT ONCE!!!
|
||||
pub async fn set_playlist(
|
||||
id: &str,
|
||||
uris: Vec<Uri>,
|
||||
token: &Token,
|
||||
_pool: &MySqlPool,
|
||||
) -> Result<(), reqwest::Error> {
|
||||
pub async fn set_playlist(id: &str, uris: Vec<Uri>, token: &Token, pool: &MySqlPool)
|
||||
-> Result<(), reqwest::Error> {
|
||||
|
||||
let mut body: HashMap<String, Vec<String>> = HashMap::new();
|
||||
let uris: Vec<String> = uris.iter().map(|u| &u.0).cloned().collect();
|
||||
let uris: Vec<String> = 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()
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
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, Serialize};
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
#[non_exhaustive]
|
||||
pub struct TrackObject {
|
||||
pub album: Option<AlbumObject>,
|
||||
|
@ -11,7 +11,7 @@ pub struct TrackObject {
|
|||
pub uri: Uri,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
#[non_exhaustive]
|
||||
pub struct AlbumObject {
|
||||
pub album_type: String,
|
||||
|
@ -22,10 +22,10 @@ pub struct AlbumObject {
|
|||
pub release_date: String,
|
||||
pub uri: Uri,
|
||||
pub genres: Option<String>,
|
||||
pub images: Vec<ImageObject>,
|
||||
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
#[non_exhaustive]
|
||||
pub struct ArtistObject {
|
||||
pub href: String,
|
||||
|
@ -33,32 +33,5 @@ pub struct ArtistObject {
|
|||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[derive(Deserialize, 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 {
|
||||
pub id: String,
|
||||
pub display_name: Option<String>,
|
||||
pub images: Option<Vec<ImageObject>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[non_exhaustive]
|
||||
pub struct ImageObject {
|
||||
pub url: String,
|
||||
}
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
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,56 +1,33 @@
|
|||
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<Vec<Uri>, sqlx::Error> {
|
||||
let users: Vec<u32> = sqlx::query!(
|
||||
"
|
||||
let users: Vec<u32> = sqlx::query!("
|
||||
SELECT UserId From Users
|
||||
"
|
||||
)
|
||||
.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?;
|
||||
")
|
||||
.fetch_all(pool)
|
||||
.await?
|
||||
.iter()
|
||||
.map(|row| row.UserId)
|
||||
.collect();
|
||||
|
||||
let mut uris: Vec<Uri> = Vec::new();
|
||||
let num: u32 = (100 / users.len()) as u32;
|
||||
for user in users {
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE Tracks SET InPlaylist = 1
|
||||
WHERE TrackID In
|
||||
(
|
||||
SELECT TrackId FROM Tracks
|
||||
let mut uri_list: Vec<Uri> = sqlx::query!("
|
||||
SELECT Uri FROM Tracks
|
||||
WHERE UserId = ?
|
||||
)
|
||||
ORDER BY RAND()
|
||||
LIMIT ?
|
||||
",
|
||||
user,
|
||||
num
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
", user, num)
|
||||
.fetch_all(pool)
|
||||
.await?
|
||||
.iter()
|
||||
.map(|row| Uri(row.Uri.clone()))
|
||||
.collect();
|
||||
uris.append(&mut uri_list);
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -1,48 +1,38 @@
|
|||
use sqlx::{mysql::MySqlQueryResult, MySqlPool};
|
||||
use sqlx::{MySqlPool, mysql::MySqlQueryResult};
|
||||
|
||||
use crate::spotify_auth::Token;
|
||||
|
||||
async fn add_user(
|
||||
email: &str,
|
||||
spotify_id: &str,
|
||||
refresh_token: &Token,
|
||||
pool: &MySqlPool,
|
||||
) -> Result<MySqlQueryResult, sqlx::Error> {
|
||||
sqlx::query!(
|
||||
async fn add_user(email: &str, spotify_id: &str, refresh_token: &Token, pool: &MySqlPool)
|
||||
-> Result<MySqlQueryResult, sqlx::Error> {
|
||||
Ok(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<AddUserSuccess, sqlx::Error> {
|
||||
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))
|
||||
}
|
||||
pub async fn add_user_dont_care(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;
|
||||
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())
|
||||
}
|
||||
Err(_) => Err(q.unwrap_err()),
|
||||
}
|
||||
}
|
||||
|
||||
pub enum GetRefreshTokenByIdErr {
|
||||
|
@ -56,51 +46,43 @@ impl From<sqlx::Error> for GetRefreshTokenByIdErr {
|
|||
}
|
||||
|
||||
#[allow(non_snake_case)] // Fixes warning for macro
|
||||
pub async fn get_refresh_token_by_id(
|
||||
spotify_id: &str,
|
||||
pool: &MySqlPool,
|
||||
) -> Result<Token, GetRefreshTokenByIdErr> {
|
||||
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<Token, GetRefreshTokenByIdErr> {
|
||||
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<sqlx::types::time::OffsetDateTime, sqlx::Error> {
|
||||
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<sqlx::types::time::OffsetDateTime, sqlx::Error> {
|
||||
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<u32, sqlx::Error> {
|
||||
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<u32, sqlx::Error> {
|
||||
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<String, sqlx::Error> {
|
||||
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<String, sqlx::Error> {
|
||||
let res = sqlx::query!("SELECT SpotifyId FROM Users WHERE IsEmilia = 1")
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
Ok(res.SpotifyId)
|
||||
}
|
||||
|
|
|
@ -1,62 +0,0 @@
|
|||
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