diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/database.rs | 164 | ||||
-rw-r--r-- | src/errors.rs | 64 | ||||
-rw-r--r-- | src/main.rs | 50 | ||||
-rw-r--r-- | src/serve.rs | 223 |
4 files changed, 329 insertions, 172 deletions
diff --git a/src/database.rs b/src/database.rs new file mode 100644 index 0000000..326c1e0 --- /dev/null +++ b/src/database.rs @@ -0,0 +1,164 @@ +use crate::errors::Error; +use postgres::{Client, NoTls}; +use std::sync::{Arc, Mutex}; + +lazy_static! { + static ref CLIENT: Arc<Mutex<Client>> = Arc::new(Mutex::new(Client::connect( + "host=track_db user=postgres password=example", + NoTls + )?)); +} + +fn initialize_db() -> Result<(), Error> { + let mut client = CLIENT.lock()?; + client.batch_execute( + r#" + CREATE TABLE IF NOT EXISTS track ( + track_id SERIAL PRIMARY KEY, + track_code TEXT NOT NULL UNIQUE, + name TEXT NOT NULL, + artist TEXT NOT NULL, + popularity int DEFAULT 50 + ); + CREATE TABLE IF NOT EXISTS suser ( + user_id SERIAL PRIMARY KEY, + user_name TEXT NOT NULL UNIQUE + ); + CREATE TABLE IF NOT EXISTS user_track_raw ( + track_id int REFERENCES track (track_id) ON UPDATE CASCADE ON DELETE CASCADE, + user_id int REFERENCES suser (user_id) ON UPDATE CASCADE ON DELETE CASCADE, + count int NOT NULL DEFAULT 1, + CONSTRAINT track_user_pkey PRIMARY KEY (track_id, user_id) + ); + + CREATE OR REPLACE VIEW user_track AS ( + SELECT *, "count" / ( + SELECT SUM("count") + FROM user_track ut + WHERE ut.user_id = user_id + )::decimal AS score + FROM user_track + ); + "#, + )?; + Ok(()) +} + +use rspotify::spotify::model::track::FullTrack; +fn insert_track(user_id: i32, track: FullTrack) -> Result<(), Error> { + let mut client = crate::CLIENT.lock()?; + + if track.id.is_none() { + println!("{:#?}", track); + return Err("failed to load get track information".into()); + } + print!(" {} ", track.id.clone()?); + client.execute( + "INSERT INTO track (track_code, name, artist, popularity) + VALUES ($1, $2, $3, $4) + ON CONFLICT DO NOTHING + ", + &[ + &(track.id.clone()?), + &track.name, + &track.artists[0].name, + &(track.popularity as i32), + ], + )?; + let track_id: i32 = client.query( + "SELECT track_id FROM track where track_code = $1;", + &[&(track.id?)], + )?[0] + .get(0); + println!("uid: {} tid: {}", user_id, track_id); + client.batch_execute( + " + INSERT INTO user_track_raw (track_id, user_id, count) + VALUES ($1, $2, $3) + ON CONFLICT + ON CONSTRAINT track_user_pkey + DO NOTHING; + UPDATE user_track SET count = count + 1 WHERE track_id = $1 AND user_id = $2; + ", + &[&track_id, &user_id, &0], + )?; + Ok(()) +} + +fn insert_user(name: &str) -> Result<(i32, String), Error> { + let mut client = crate::CLIENT.lock()?; + client.batch_execute( + " + INSERT INTO suser (user_name) VALUES ($1) ON CONFLICT (user_name) DO NOTHING; + DELETE FROM user_track WHERE user_name = $1; + ", + &[&user_id], + )?; + let user_id: i32 = client + .query_one( + "SELECT user_id FROM suser WHERE user_name = $1;", + &[&user_id], + )? + .get(0); + //reset user_track relation + Ok((user_id, name)) +} + +fn get_uid(name: &str, client: &mut postgres::Client) -> Result<i32, Error> { + let x: i32 = client + .query_one("SELECT user_id FROM suser where user_name = $1;", &[&name])? + .get(0); + Ok(x) +} + +lazy_static! { + static ref CACHE: Arc<Mutex<HashMap<String, SpotifyOAuth>>> = + Arc::new(Mutex::new(HashMap::new())); +} + +pub fn match_users(name1: String, name2: String) -> Result<String, Error> { + let mut client = crate::CLIENT.lock()?; + let mut songs = String::new(); + for row in client.query( + " + WITH users AS ( + SELECT * + FROM ( VALUES + + ($1), ($2) + + ) AS _ (user_id) + ) + SELECT track_id, name, artist + FROM track + JOIN ( + SELECT track_id + FROM user_track + JOIN suser USING (user_id) + JOIN track USING (track_id) + WHERE suser.user_name IN (SELECT * FROM users) + GROUP BY track_id + HAVING COUNT(track_id) = (SELECT COUNT(*) FROM users) + ORDER BY SUM(score) DESC + ) AS _ USING (track_id) + ; + ", + &[&name1.as_str(), &name2.as_str()], + )? { + let name: String = row.get(1); + let artist: String = row.get(2); + songs = format!("{}{} by {}\n", songs, name, artist); + } + Ok(songs) +} + +#[get("/user")] +pub fn get_users() -> Result<String, Error> { + let mut client = crate::CLIENT.lock()?; + let mut users = String::new(); + for row in client.query("SELECT user_name FROM suser", &[])? { + let user: String = row.get(0); + users = format!("{}{}\n", users, user); + } + Ok(users) +} diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..d9893ba --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,64 @@ +use rocket::http::ContentType; +use rocket::request::Request; +use rocket::response::{self, Responder, Response}; +use rspotify::spotify::client::ApiError; +use rspotify::spotify::oauth2::SpotifyOAuth; +use std::io::Cursor; +use std::sync::{MutexGuard, PoisonError}; +use tokio_postgres::error::Error as DbError; + +#[derive(Debug)] +pub enum Error { + Postgres(DbError), + Spotify(ApiError), + Misc(String), +} +impl From<DbError> for Error { + fn from(error: DbError) -> Self { + Error::Postgres(error) + } +} +impl From<ApiError> for Error { + fn from(error: ApiError) -> Self { + Error::Spotify(error) + } +} +impl From<&str> for Error { + fn from(error: &str) -> Self { + Error::Misc(error.to_owned()) + } +} +impl From<String> for Error { + fn from(error: String) -> Self { + Error::Misc(error) + } +} +impl<'a> From<PoisonError<MutexGuard<'a, HashMap<String, SpotifyOAuth>>>> for Error { + fn from(error: PoisonError<MutexGuard<'a, HashMap<String, SpotifyOAuth>>>) -> Self { + Error::Misc(format!("failed to lock the client mutex: {:?}", error)) + } +} +impl<'a> From<PoisonError<MutexGuard<'a, postgres::Client>>> for Error { + fn from(error: PoisonError<MutexGuard<'a, postgres::Client>>) -> Self { + Error::Misc(format!("failed to lock the client mutex: {:?}", error)) + } +} +impl<'a> From<std::option::NoneError> for Error { + fn from(error: std::option::NoneError) -> Self { + Error::Misc(format!("tried to unwrap none at: {:?}", error)) + } +} +impl<'a> Responder<'a> for Error { + fn respond_to(self, _: &Request) -> response::Result<'a> { + let response = match self { + Error::Postgres(e) => format!("DB Error: {:?}", e), + Error::Spotify(e) => format!("Spotify Error: {:?}", e), + Error::Misc(e) => format!("Error: {}", e), + }; + Response::build() + .header(ContentType::Plain) + .status(rocket::http::Status::raw(500)) + .sized_body(Cursor::new(response)) + .ok() + } +} diff --git a/src/main.rs b/src/main.rs index d1ebe7b..93a22a6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,21 +1,17 @@ #![feature(proc_macro_hygiene, decl_macro)] +#![feature(try_trait)] #[macro_use] extern crate rocket; #[macro_use] extern crate lazy_static; +mod database; +mod errors; mod serve; -use postgres::{Client, NoTls}; -use std::sync::{Arc, Mutex}; - -lazy_static! { - static ref CLIENT: Arc<Mutex<Client>> = Arc::new(Mutex::new( - Client::connect("host=track_db user=postgres password=example", NoTls).unwrap() - )); -} +mod spotify; fn main() { - //initialize_db().unwrap(); + //database::initialize_db().unwrap(); println!("connected with db"); rocket::ignite() .mount( @@ -29,39 +25,3 @@ fn main() { ) .launch(); } - -fn initialize_db() -> Result<(), postgres::Error> { - let mut client = CLIENT.lock().unwrap(); - //let _ = client.batch_execute("DROP TABLE user_track; DROP TABLE suser; DROP TABLE track;"); - client.batch_execute( - r#" - CREATE TABLE track IF NOT EXISTS ( - track_id SERIAL PRIMARY KEY, - track_code TEXT NOT NULL UNIQUE, - name TEXT NOT NULL, - artist TEXT NOT NULL, - popularity int DEFAULT 50 - ); - CREATE TABLE suser IF NOT EXISTS ( - user_id SERIAL PRIMARY KEY, - user_name TEXT NOT NULL UNIQUE - ); - CREATE TABLE user_track_raw IF NOT EXISTS ( - track_id int REFERENCES track (track_id) ON UPDATE CASCADE ON DELETE CASCADE, - user_id int REFERENCES suser (user_id) ON UPDATE CASCADE ON DELETE CASCADE, - count int NOT NULL DEFAULT 1, - CONSTRAINT track_user_pkey PRIMARY KEY (track_id, user_id) - ); - - CREATE OR REPLACE VIEW user_track AS ( - SELECT *, "count" / ( - SELECT SUM("count") - FROM user_track ut - WHERE ut.user_id = user_id - )::decimal AS score - FROM user_track - ); - "#, - )?; - Ok(()) -} diff --git a/src/serve.rs b/src/serve.rs index 95ce9b1..7014641 100644 --- a/src/serve.rs +++ b/src/serve.rs @@ -1,6 +1,6 @@ +use crate::errors::Error; use lazy_static::lazy_static; -use rocket::http::Status; -use rocket::response::{status, Redirect}; +use rocket::response::Redirect; use rspotify::spotify::client::{ApiError, Spotify}; use rspotify::spotify::oauth2::{SpotifyClientCredentials, SpotifyOAuth}; use rspotify::spotify::util::process_token; @@ -13,47 +13,12 @@ lazy_static! { } #[get("/callback/<name>/<url>")] -pub fn get_tracks(name: String, url: String) -> Result<(), status::Custom<String>> { - let mut guard = CACHE.lock().or(Err(status::Custom( - Status::InternalServerError, - String::from("failed to lock cache mutex"), - )))?; - let mut oauth = guard.remove(&name).ok_or(status::Custom( - Status::NotFound, - String::from("uuid not found in cache"), - ))?; - println!("auth: {:?} url: {}", oauth, url); - let token_info = process_token(&mut oauth, &mut ("?code=".to_owned() + url.as_ref())); - let client_credential = SpotifyClientCredentials::default() - .token_info(token_info.unwrap()) - .build(); - - let spotify = Spotify::default() - .client_credentials_manager(client_credential) - .build(); - let user_id = spotify.current_user().unwrap().id; - let mut client = crate::CLIENT.lock().or(Err(status::Custom( - Status::InternalServerError, - String::from("failed to lock cache mutex"), - )))?; - client - .execute( - "INSERT INTO suser (user_name) VALUES ($1) ON CONFLICT (user_name) DO NOTHING;", - &[&user_id], - ) - .unwrap(); - let uid = get_uid(user_id.as_ref(), &mut client).ok_or(status::Custom( - Status::NotFound, - format!("username {} not found", user_id), - ))?; - client - .execute("DELETE FROM user_track WHERE user_id = $1;", &[&uid]) - .unwrap(); - drop(client); +pub fn get_tracks(name: String, url: String) -> Result<(), Error> { + let (uid, spotify_uid, spotify) = autenth_user(name.as_ref(), url)?; let chunk_size = 50; let mut playlist_index = 0; loop { - match spotify.user_playlists(user_id.as_ref(), Some(chunk_size), Some(playlist_index)) { + match spotify.user_playlists(spotify_uid.as_ref(), Some(chunk_size), Some(playlist_index)) { Ok(playlists) => { playlist_index += chunk_size; if playlists.items.is_empty() { @@ -65,7 +30,7 @@ pub fn get_tracks(name: String, url: String) -> Result<(), status::Custom<String loop { match spotify.user_playlist_tracks( - user_id.as_ref(), + spotify_uid.as_ref(), &playlist.id, None, Some(chunk_size), @@ -78,49 +43,9 @@ pub fn get_tracks(name: String, url: String) -> Result<(), status::Custom<String break; } for track in tracks.items { - //println!("{:?}", track.track.name); - let mut client = crate::CLIENT.lock().or(Err( - status::Custom(Status::InternalServerError, String::new()), - ))?; - - if track.track.id.is_none() { - println!("{:#?}", track); - continue; - } - print!(" {} ", track.track.id.clone().unwrap()); - client - .execute( - "INSERT INTO track (track_code, name, artist, popularity) - VALUES ($1, $2, $3, $4) - ON CONFLICT DO NOTHING - ", - &[&(track.track.id.clone().unwrap()), &track.track.name, &track.track.artists[0].name, &(track.track.popularity as i32)], - ) - .unwrap(); - let tid: i32 = client - .query( - "SELECT track_id FROM track where track_code = $1;", - &[&(track.track.id.clone().unwrap())], - ) - .unwrap()[0] - .get(0); - println!("uid: {} tid: {}", uid, tid); - client - .execute( - "INSERT INTO user_track (track_id, user_id, count) - VALUES ($1, $2, $3) - ON CONFLICT - ON CONSTRAINT track_user_pkey - DO NOTHING;", - &[&tid, &uid, &0], - ) - .unwrap(); - client - .execute( - "UPDATE user_track SET count = count + 1 WHERE track_id = $1 AND user_id = $2;", - &[&tid, &uid], - ) - .unwrap(); + if let Err(e) = insert_track(uid, track.track) { + println!("failed to load track to db: {:?}", e) + }; } } Err(e) => match e.downcast::<ApiError>() { @@ -152,52 +77,100 @@ pub fn get_tracks(name: String, url: String) -> Result<(), status::Custom<String Ok(()) } +fn autenth_user(name: &str, url: String) -> Result<(i32, String, Spotify), Error> { + let mut guard = CACHE.lock()?; + let mut oauth = guard.remove(name)?; + println!("auth: {:?} url: {}", oauth, url); + let token_info = process_token(&mut oauth, &mut ("?code=".to_owned() + url.as_ref())); + let client_credential = SpotifyClientCredentials::default() + .token_info(token_info?) + .build(); + + let spotify = Spotify::default() + .client_credentials_manager(client_credential) + .build(); + let user_id = spotify + .current_user() + .map_err(|e| format!("failed to load currentuser {:?}", e))? + .id; + let mut client = crate::CLIENT.lock()?; + client.execute( + "INSERT INTO suser (user_name) VALUES ($1) ON CONFLICT (user_name) DO NOTHING;", + &[&user_id], + )?; + let uid = get_uid(user_id.as_ref(), &mut client)?; + //reset user_track relation + client.execute("DELETE FROM user_track WHERE user_id = $1;", &[&uid])?; + Ok((uid, user_id, spotify)) +} + +use rspotify::spotify::model::track::FullTrack; +fn insert_track(user_id: i32, track: FullTrack) -> Result<(), Error> { + let mut client = crate::CLIENT.lock()?; + + if track.id.is_none() { + println!("{:#?}", track); + return Err("failed to load get track information".into()); + } + print!(" {} ", track.id.clone()?); + client.execute( + "INSERT INTO track (track_code, name, artist, popularity) + VALUES ($1, $2, $3, $4) + ON CONFLICT DO NOTHING + ", + &[ + &(track.id.clone()?), + &track.name, + &track.artists[0].name, + &(track.popularity as i32), + ], + )?; + let track_id: i32 = client.query( + "SELECT track_id FROM track where track_code = $1;", + &[&(track.id?)], + )?[0] + .get(0); + println!("uid: {} tid: {}", user_id, track_id); + client.execute( + " + INSERT INTO user_track_raw (track_id, user_id, count) + VALUES ($1, $2, $3) + ON CONFLICT + ON CONSTRAINT track_user_pkey + DO NOTHING; + UPDATE user_track SET count = count + 1 WHERE track_id = $1 AND user_id = $2; + ", + &[&track_id, &user_id, &0], + )?; + Ok(()) +} + #[get("/token/<name>")] -pub fn token(name: String) -> Result<Redirect, status::Custom<String>> { +pub fn token(name: String) -> Result<Redirect, Error> { let state = rspotify::spotify::util::generate_random_string(16); let oauth = SpotifyOAuth::default(); - //let callback = oauth.redirect_uri.clone(); let oauth = oauth .scope("playlist-read-private, playlist-read-collaborative, user-read-private, user-follow-read, user-library-read") - //.redirect_uri(format!("{}/{}", callback, &state).as_ref()) .build(); let auth_url = oauth.get_authorize_url(Some(&state), None); - match CACHE.lock() { - Ok(mut guard) => { - guard.insert(name, oauth); - Ok(Redirect::to(auth_url)) - } - Err(_) => Err(status::Custom( - Status::ImATeapot, - "Internal Server Error".to_owned(), - )), - } + let mut guard = CACHE.lock()?; + guard.insert(name, oauth); + Ok(Redirect::to(auth_url)) } -fn get_uid(name: &str, client: &mut postgres::Client) -> Option<i32> { - match client.query("SELECT user_id FROM suser where user_name = $1;", &[&name]) { - Ok(rows) => match rows.len() { - 0 => None, - x => { - let x: i32 = rows[0].get(0); - Some(x) - } - }, - _ => None, - } +fn get_uid(name: &str, client: &mut postgres::Client) -> Result<i32, Error> { + let x: i32 = client + .query_one("SELECT user_id FROM suser where user_name = $1;", &[&name])? + .get(0); + Ok(x) } #[get("/match/<name1>/<name2>")] -pub fn match_users(name1: String, name2: String) -> Result<String, status::NotFound<String>> { - let mut client = crate::CLIENT.lock().unwrap(); - let uid1 = get_uid(name1.as_ref(), &mut client) - .ok_or(status::NotFound(format!("username {} not found", name1)))?; - let uid2 = get_uid(name2.as_ref(), &mut client) - .ok_or(status::NotFound(format!("username {} not found", name2)))?; +pub fn match_users(name1: String, name2: String) -> Result<String, Error> { + let mut client = crate::CLIENT.lock()?; let mut songs = String::new(); - for row in client - .query( - " + for row in client.query( + " WITH users AS ( SELECT * FROM ( VALUES @@ -213,19 +186,15 @@ pub fn match_users(name1: String, name2: String) -> Result<String, status::NotFo FROM user_track JOIN suser USING (user_id) JOIN track USING (track_id) - WHERE suser.user_id IN (SELECT * FROM users) + WHERE suser.user_name IN (SELECT * FROM users) GROUP BY track_id HAVING COUNT(track_id) = (SELECT COUNT(*) FROM users) ORDER BY SUM(score) DESC ) AS _ USING (track_id) ; ", - &[&uid1, &uid2], - ) - .unwrap() - { - //let song = row.get(0); - println!("{:?}", row.columns()); + &[&name1.as_str(), &name2.as_str()], + )? { let name: String = row.get(1); let artist: String = row.get(2); songs = format!("{}{} by {}\n", songs, name, artist); @@ -234,10 +203,10 @@ pub fn match_users(name1: String, name2: String) -> Result<String, status::NotFo } #[get("/user")] -pub fn get_users() -> Result<String, status::NotFound<String>> { - let mut client = crate::CLIENT.lock().unwrap(); +pub fn get_users() -> Result<String, Error> { + let mut client = crate::CLIENT.lock()?; let mut users = String::new(); - for row in client.query("SELECT user_name FROM suser", &[]).unwrap() { + for row in client.query("SELECT user_name FROM suser", &[])? { let user: String = row.get(0); users = format!("{}{}\n", users, user); } |