From 96a091f0684a2a15d72fc79e72a2f76eb96e5c46 Mon Sep 17 00:00:00 2001 From: Miguel da Mota Date: Mon, 1 Jan 2024 23:58:26 +0100 Subject: [PATCH] [API-1] feat: search route --- Cargo.lock | 26 +++++ Cargo.toml | 2 +- .../up.sql | 96 +++++++++++++++++++ migrations/2023-12-29-191547_users/up.sql | 2 +- migrations/2023-12-30-162032_playlists/up.sql | 4 +- migrations/2023-12-30-192105_tracks/up.sql | 4 +- migrations/2023-12-31-104738_artists/up.sql | 9 +- src/main.rs | 9 +- src/middlewares/error.rs | 9 ++ src/middlewares/user.rs | 25 +++-- src/models/artists.rs | 11 ++- src/models/playlists.rs | 8 +- src/models/spotify.rs | 43 ++++++++- src/models/tracks.rs | 59 +++++++----- src/models/user.rs | 4 +- src/routes/auth.rs | 34 +++---- src/routes/import.rs | 12 +++ src/routes/mod.rs | 1 + src/routes/search.rs | 31 ++++-- src/routes/users.rs | 17 +--- src/schema.rs | 13 +-- src/services/service.rs | 4 +- src/services/spotify.rs | 89 +++++++++++++---- src/utils.rs | 3 + 24 files changed, 388 insertions(+), 127 deletions(-) create mode 100644 src/routes/import.rs create mode 100644 src/utils.rs diff --git a/Cargo.lock b/Cargo.lock index f8730de..d994919 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -653,6 +653,12 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + [[package]] name = "futures-sink" version = "0.3.30" @@ -672,9 +678,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-core", + "futures-io", "futures-task", + "memchr", "pin-project-lite", "pin-utils", + "slab", ] [[package]] @@ -731,6 +740,12 @@ version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +[[package]] +name = "hermit-abi" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" + [[package]] name = "hmac" version = "0.12.1" @@ -1067,6 +1082,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "object" version = "0.32.2" @@ -1708,6 +1733,7 @@ dependencies = [ "bytes", "libc", "mio", + "num_cpus", "parking_lot", "pin-project-lite", "signal-hook-registry", diff --git a/Cargo.toml b/Cargo.toml index 40fae80..6c1cd8e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ diesel = { version = "2", features = ["r2d2", "postgres", "chrono"] } diesel_migrations = "2" dotenvy = "*" hmac = "0.12" -reqwest = { version = "0.11", features = ["json"] } +reqwest = { version = "0.11", features = ["blocking", "json"] } sha2 = "0.10" lazy_static = "1" jsonwebtoken = "9" diff --git a/migrations/00000000000000_diesel_initial_setup/up.sql b/migrations/00000000000000_diesel_initial_setup/up.sql index d68895b..7e95698 100644 --- a/migrations/00000000000000_diesel_initial_setup/up.sql +++ b/migrations/00000000000000_diesel_initial_setup/up.sql @@ -34,3 +34,99 @@ BEGIN RETURN NEW; END; $$ LANGUAGE plpgsql; + +-- nanoid function +CREATE EXTENSION IF NOT EXISTS pgcrypto; + +-- The `nanoid()` function generates a compact, URL-friendly unique identifier. +-- Based on the given size and alphabet, it creates a randomized string that's ideal for +-- use-cases requiring small, unpredictable IDs (e.g., URL shorteners, generated file names, etc.). +-- While it comes with a default configuration, the function is designed to be flexible, +-- allowing for customization to meet specific needs. +DROP FUNCTION IF EXISTS nanoid(int, text, float); +CREATE OR REPLACE FUNCTION nanoid( + size int DEFAULT 21, -- The number of symbols in the NanoId String. Must be greater than 0. + alphabet text DEFAULT '_-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', -- The symbols used in the NanoId String. Must contain between 1 and 255 symbols. + additionalBytesFactor float DEFAULT 1.6 -- The additional bytes factor used for calculating the step size. Must be equal or greater then 1. +) + RETURNS text -- A randomly generated NanoId String + LANGUAGE plpgsql + VOLATILE + LEAKPROOF + PARALLEL SAFE +AS +$$ +DECLARE + alphabetArray text[]; + alphabetLength int := 64; + mask int := 63; + step int := 34; +BEGIN + IF size IS NULL OR size < 1 THEN + RAISE EXCEPTION 'The size must be defined and greater than 0!'; + END IF; + + IF alphabet IS NULL OR length(alphabet) = 0 OR length(alphabet) > 255 THEN + RAISE EXCEPTION 'The alphabet can''t be undefined, zero or bigger than 255 symbols!'; + END IF; + + IF additionalBytesFactor IS NULL OR additionalBytesFactor < 1 THEN + RAISE EXCEPTION 'The additional bytes factor can''t be less than 1!'; + END IF; + + alphabetArray := regexp_split_to_array(alphabet, ''); + alphabetLength := array_length(alphabetArray, 1); + mask := (2 << cast(floor(log(alphabetLength - 1) / log(2)) as int)) - 1; + step := cast(ceil(additionalBytesFactor * mask * size / alphabetLength) AS int); + + IF step > 1024 THEN + step := 1024; -- The step size % can''t be bigger then 1024! + END IF; + + RETURN nanoid_optimized(size, alphabet, mask, step); +END +$$; + +-- Generates an optimized random string of a specified size using the given alphabet, mask, and step. +-- This optimized version is designed for higher performance and lower memory overhead. +-- No checks are performed! Use it only if you really know what you are doing. +DROP FUNCTION IF EXISTS nanoid_optimized(int, text, int, int); +CREATE OR REPLACE FUNCTION nanoid_optimized( + size int, -- The desired length of the generated string. + alphabet text, -- The set of characters to choose from for generating the string. + mask int, -- The mask used for mapping random bytes to alphabet indices. Should be `(2^n) - 1` where `n` is a power of 2 less than or equal to the alphabet size. + step int -- The number of random bytes to generate in each iteration. A larger value may speed up the function but increase memory usage. +) + RETURNS text -- A randomly generated NanoId String + LANGUAGE plpgsql + VOLATILE + LEAKPROOF + PARALLEL SAFE +AS +$$ +DECLARE + idBuilder text := ''; + counter int := 0; + bytes bytea; + alphabetIndex int; + alphabetArray text[]; + alphabetLength int := 64; +BEGIN + alphabetArray := regexp_split_to_array(alphabet, ''); + alphabetLength := array_length(alphabetArray, 1); + + LOOP + bytes := gen_random_bytes(step); + FOR counter IN 0..step - 1 + LOOP + alphabetIndex := (get_byte(bytes, counter) & mask) + 1; + IF alphabetIndex <= alphabetLength THEN + idBuilder := idBuilder || alphabetArray[alphabetIndex]; + IF length(idBuilder) = size THEN + RETURN idBuilder; + END IF; + END IF; + END LOOP; + END LOOP; +END +$$; diff --git a/migrations/2023-12-29-191547_users/up.sql b/migrations/2023-12-29-191547_users/up.sql index 56dc507..65812bc 100644 --- a/migrations/2023-12-29-191547_users/up.sql +++ b/migrations/2023-12-29-191547_users/up.sql @@ -7,7 +7,7 @@ create table if not exists users password text not null, updated_at timestamp, - created_at timestamp default now() not null, + created_at timestamp default now(), primary key (id) ); diff --git a/migrations/2023-12-30-162032_playlists/up.sql b/migrations/2023-12-30-162032_playlists/up.sql index 18789f0..6f496d7 100644 --- a/migrations/2023-12-30-162032_playlists/up.sql +++ b/migrations/2023-12-30-162032_playlists/up.sql @@ -2,11 +2,11 @@ create table if not exists playlists ( id varchar(24) default nanoid(24), name varchar(255) not null, - public bool default false, + public bool default false not null, creator_id varchar(24) not null, - created_at timestamp default now() not null, + created_at timestamp default now(), updated_at timestamp, primary key (id), diff --git a/migrations/2023-12-30-192105_tracks/up.sql b/migrations/2023-12-30-192105_tracks/up.sql index 3951c1e..ed9f00a 100644 --- a/migrations/2023-12-30-192105_tracks/up.sql +++ b/migrations/2023-12-30-192105_tracks/up.sql @@ -5,11 +5,11 @@ create table if not exists tracks title varchar(255) not null, duration_ms int not null default 0, - created_at timestamp default now() not null, + created_at timestamp default now(), updated_at timestamp, -- music services - spotify_id varchar(21) unique, + spotify_id varchar(22) unique, tidal_id varchar(10) unique, primary key (id) diff --git a/migrations/2023-12-31-104738_artists/up.sql b/migrations/2023-12-31-104738_artists/up.sql index 0deb8da..ef2ce81 100644 --- a/migrations/2023-12-31-104738_artists/up.sql +++ b/migrations/2023-12-31-104738_artists/up.sql @@ -3,13 +3,14 @@ create table if not exists artists ( id varchar(24) default nanoid(24), name varchar(255) NOT NULL, + slug varchar unique not null generated always as (lower(replace(name, ' ', '-'))) stored, - created_at TIMESTAMP DEFAULT now() NOT NULL, - updated_at TIMESTAMP, + created_at timestamp default now(), + updated_at timestamp, -- music services - spotify_id VARCHAR(21) UNIQUE, - tidal_id VARCHAR(10) UNIQUE, + spotify_id varchar(22) unique, + tidal_id varchar(10) unique, primary key (id) ); diff --git a/src/main.rs b/src/main.rs index 8705e46..abc0a34 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,21 +6,26 @@ mod models; mod routes; mod schema; mod services; +mod utils; use crate::helpers::db; -use actix_web::{web, App, HttpServer}; #[actix_web::main] async fn main() -> std::io::Result<()> { + use actix_web::{web, App, HttpServer}; + dotenvy::dotenv().expect("No .env file found"); std::env::set_var("RUST_LOG", "debug"); + // Register services. + let _ = services::spotify::instance().await; + db::init(); HttpServer::new(move || { App::new() .service(web::scope("/playlists").service(routes::playlists::get_playlist)) - .service(routes::auth::login) + .service(routes::auth::routes()) .service(routes::me::routes()) .service(routes::users::routes()) .service(routes::search::routes()) diff --git a/src/middlewares/error.rs b/src/middlewares/error.rs index e1bdcfa..6b060fa 100644 --- a/src/middlewares/error.rs +++ b/src/middlewares/error.rs @@ -9,6 +9,15 @@ pub struct ErrorResponse { pub status: StatusCode, } +impl ErrorResponse { + pub fn new(message: &str, status: StatusCode) -> Self { + ErrorResponse { + message: message.to_string(), + status, + } + } +} + impl Display for ErrorResponse { fn fmt(&self, f: &mut Formatter) -> Result { write!(f, "{}: {}", self.status, self.message) diff --git a/src/middlewares/user.rs b/src/middlewares/user.rs index d4b91d4..84c229f 100644 --- a/src/middlewares/user.rs +++ b/src/middlewares/user.rs @@ -19,23 +19,20 @@ pub fn get_user(req: HttpRequest) -> Result { } Err(e) => { return Err(match e.kind() { - jsonwebtoken::errors::ErrorKind::ExpiredSignature => ErrorResponse { - message: "Not Authorized".to_string(), - status: StatusCode::UNAUTHORIZED, - }, - _ => ErrorResponse { - message: e.to_string(), - status: StatusCode::INTERNAL_SERVER_ERROR, - }, + jsonwebtoken::errors::ErrorKind::ExpiredSignature => { + ErrorResponse::new("Not Authorized", StatusCode::UNAUTHORIZED) + } + _ => ErrorResponse::new( + e.to_string().as_str(), + StatusCode::INTERNAL_SERVER_ERROR, + ), }) } } } - None => { - return Err(ErrorResponse { - message: "Not Authorized".to_string(), - status: StatusCode::UNAUTHORIZED, - }); - } + None => Err(ErrorResponse::new( + "Not Authorized", + StatusCode::UNAUTHORIZED, + )), } } diff --git a/src/models/artists.rs b/src/models/artists.rs index 0e82a11..d931681 100644 --- a/src/models/artists.rs +++ b/src/models/artists.rs @@ -13,9 +13,10 @@ use serde::{Deserialize, Serialize}; pub struct Artist { pub id: String, pub name: String, + pub slug: String, - created_at: NaiveDateTime, - updated_at: Option, + pub created_at: Option, + pub updated_at: Option, pub spotify_id: Option, pub tidal_id: Option, @@ -25,9 +26,10 @@ pub struct Artist { pub struct Artists { pub id: String, pub title: String, + pub slug: String, - created_at: NaiveDateTime, - updated_at: Option, + pub created_at: Option, + pub updated_at: Option, pub spotify_id: Option, pub tidal_id: Option, @@ -68,6 +70,7 @@ impl Artist { Artist { id: artist.id, name: artist.name, + slug: artist.slug, created_at: artist.created_at, updated_at: artist.updated_at, diff --git a/src/models/playlists.rs b/src/models/playlists.rs index 5986d67..22a7982 100644 --- a/src/models/playlists.rs +++ b/src/models/playlists.rs @@ -1,5 +1,5 @@ use crate::helpers::db; -use crate::models::tracks::{Track, Tracks, TracksWithArtists}; +use crate::models::tracks::{Tracks, TracksWithArtists}; use crate::models::user::Users; use crate::schema::playlists; use chrono::NaiveDateTime; @@ -25,7 +25,7 @@ pub struct Playlist { pub public: bool, pub creator_id: String, - pub created_at: NaiveDateTime, + pub created_at: Option, pub updated_at: Option, } @@ -36,7 +36,7 @@ pub struct Playlists { pub public: bool, pub creator_id: String, - pub created_at: NaiveDateTime, + pub created_at: Option, pub updated_at: Option, } @@ -74,7 +74,7 @@ impl Playlists { let tracks = Tracks::find_by_playlist(&self.id)?; let tracks: Vec = tracks .into_iter() - .map(Track::with_artists) + .map(Tracks::with_artists) .collect::, _>>()?; Ok(tracks) diff --git a/src/models/spotify.rs b/src/models/spotify.rs index 0145ab1..98c5340 100644 --- a/src/models/spotify.rs +++ b/src/models/spotify.rs @@ -1,5 +1,42 @@ -pub struct Track {} +use serde::{Deserialize, Serialize}; -pub struct Artist {} +#[derive(Deserialize, Serialize)] +pub struct Track { + pub id: String, + pub name: String, + pub duration_ms: i32, + pub artists: Vec, + pub album: Album, -pub struct Album {} + pub external_ids: ExternalIds, +} + +#[derive(Deserialize, Serialize)] +pub struct ExternalIds { + pub isrc: String, +} + +#[derive(Deserialize, Serialize)] +pub struct Artist { + pub id: String, + pub name: String, +} + +#[derive(Deserialize, Serialize)] +pub struct Album { + pub id: String, + pub name: String, + pub artists: Vec, +} + +#[derive(Deserialize, Serialize)] +pub struct APISearchResponse { + pub albums: APISearchItems, + pub artists: APISearchItems, + pub tracks: APISearchItems, +} + +#[derive(Deserialize, Serialize)] +pub struct APISearchItems { + pub items: Vec, +} diff --git a/src/models/tracks.rs b/src/models/tracks.rs index a1bbdc5..aa4e0bd 100644 --- a/src/models/tracks.rs +++ b/src/models/tracks.rs @@ -1,5 +1,5 @@ use crate::helpers::db; -use crate::models::artists::Artists; +use crate::models::{artists::Artists, spotify}; use crate::schema::tracks; use chrono::NaiveDateTime; use diesel::result::Error; @@ -10,28 +10,15 @@ use serde::{Deserialize, Serialize}; use crate::schema::playlists_tracks; -#[derive(AsChangeset, Insertable, Queryable, Selectable, Deserialize, Serialize)] +#[derive(AsChangeset, Clone, Insertable, Queryable, Selectable, Deserialize, Serialize)] #[diesel(table_name = crate::schema::tracks)] #[diesel(check_for_backend(diesel::pg::Pg))] -pub struct Track { - pub id: String, - pub title: String, - pub duration_ms: i32, - - pub created_at: NaiveDateTime, - pub updated_at: Option, - - pub spotify_id: Option, - pub tidal_id: Option, -} - -#[derive(Debug, Deserialize, Queryable, Serialize)] pub struct Tracks { pub id: String, pub title: String, pub duration_ms: i32, - pub created_at: NaiveDateTime, + pub created_at: Option, pub updated_at: Option, pub spotify_id: Option, @@ -45,10 +32,10 @@ impl Tracks { Ok(playlist) } - pub fn create(track: Track) -> Result { + pub fn create(track: Tracks) -> Result { let conn = &mut db::connection()?; let playlist = diesel::insert_into(tracks::table) - .values(Track::from(track)) + .values(Tracks::from(track)) .get_result(conn)?; Ok(playlist) } @@ -61,24 +48,30 @@ impl Tracks { let tracks = tracks .into_iter() - .map(|(playlist_id, track_id)| { - println!("{}: {}", playlist_id, track_id); - Tracks::find(track_id).unwrap() - }) + .map(|(_, track_id)| Tracks::find(track_id).unwrap()) .collect::>(); Ok(tracks) } + pub fn create_or_update(track: Tracks) -> Result { + let conn = &mut db::connection()?; + let playlist = diesel::insert_into(tracks::table) + .values(track.clone()) + .on_conflict(tracks::id) + .do_update() + .set(track) + .get_result(conn)?; + Ok(playlist) + } + pub fn get_artists(&self) -> Result, Error> { let artists = Artists::find_by_track(&self.id)?; Ok(artists) } -} -impl Track { - fn from(track: Track) -> Track { - Track { + fn from(track: Tracks) -> Self { + Tracks { id: track.id, title: track.title, duration_ms: track.duration_ms, @@ -107,6 +100,20 @@ impl Track { } } +impl From for Tracks { + fn from(value: spotify::Track) -> Tracks { + Tracks { + id: value.external_ids.isrc, + title: value.name, + duration_ms: value.duration_ms, + spotify_id: Some(value.id), + tidal_id: None, + created_at: None, + updated_at: None, + } + } +} + #[derive(Deserialize, Serialize)] pub struct TracksWithArtists { pub id: String, diff --git a/src/models/user.rs b/src/models/user.rs index 7147ba8..99d242c 100644 --- a/src/models/user.rs +++ b/src/models/user.rs @@ -18,8 +18,8 @@ pub struct User { pub password: String, + pub created_at: Option, pub updated_at: Option, - pub created_at: NaiveDateTime, } #[derive(Debug, Deserialize, Queryable, Serialize)] @@ -30,8 +30,8 @@ pub struct Users { pub password: String, + pub created_at: Option, pub updated_at: Option, - pub created_at: NaiveDateTime, } impl Users { diff --git a/src/routes/auth.rs b/src/routes/auth.rs index 70c446a..f376a69 100644 --- a/src/routes/auth.rs +++ b/src/routes/auth.rs @@ -2,15 +2,19 @@ use crate::helpers::jwt::get_encoding_key; use crate::middlewares::error::ErrorResponse; use crate::models::user::Users; use actix_web::http::StatusCode; -use actix_web::{post, web, HttpResponse}; +use actix_web::{post, web, HttpResponse, Scope}; use chrono::{Days, Utc}; use jsonwebtoken::{encode, Header}; use serde::{Deserialize, Serialize}; +pub fn routes() -> Scope { + web::scope("/auth").service(login) +} + #[derive(Debug, Deserialize, Serialize)] pub struct JWTClaims { pub user_id: String, - pub exp: usize, + pub exp: i64, } #[post("/auth/login")] @@ -18,7 +22,7 @@ async fn login(body: web::Json) -> Result) -> Result { - return Err(ErrorResponse { - message: "Invalid credentials.".to_string(), - status: StatusCode::BAD_REQUEST, - }) - } + Err(_e) => Err(ErrorResponse::new( + "Invalid credentials", + StatusCode::BAD_REQUEST, + )), }, - Err(_err) => { - return Err(ErrorResponse { - message: "Invalid credentials.".to_string(), - status: StatusCode::BAD_REQUEST, - }) - } + Err(_err) => Err(ErrorResponse::new( + "Invalid credentials", + StatusCode::BAD_REQUEST, + )), } } diff --git a/src/routes/import.rs b/src/routes/import.rs new file mode 100644 index 0000000..51a04cb --- /dev/null +++ b/src/routes/import.rs @@ -0,0 +1,12 @@ +use actix_web::{get, web, HttpResponse, Result, Scope}; + +use crate::middlewares::error::ErrorResponse; + +pub fn routes() -> Scope { + web::scope("/import") +} + +#[get("/spotify/{playlist_id}")] +async fn import_spotify() -> Result { + Ok(HttpResponse::Ok().finish()) +} diff --git a/src/routes/mod.rs b/src/routes/mod.rs index f643e8b..1d7bac6 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -1,4 +1,5 @@ pub mod auth; +pub mod import; pub mod me; pub mod playlists; pub mod search; diff --git a/src/routes/search.rs b/src/routes/search.rs index 6528aed..f4f3b7c 100644 --- a/src/routes/search.rs +++ b/src/routes/search.rs @@ -1,5 +1,6 @@ -use crate::middlewares::error::ErrorResponse; -use crate::services::spotify; +use crate::models::spotify; +use crate::services::spotify as Spotify; +use crate::{middlewares::error::ErrorResponse, models::tracks::Tracks}; use actix_web::{get, web, HttpResponse, Result, Scope}; use serde::{Deserialize, Serialize}; @@ -14,13 +15,31 @@ struct SearchQuery { #[derive(Serialize)] struct SearchResponse { - track: (), + tracks: Vec, +} + +#[derive(Serialize)] +struct SearchTracks { + pub id: String, +} + +impl From for SearchTracks { + fn from(value: spotify::Track) -> SearchTracks { + SearchTracks { id: value.id } + } } #[get("")] async fn search(query: web::Query) -> Result { - let spotify = spotify::instance().await; - let track = spotify.search(&query.q).await; + let spotify = Spotify::instance().await; + let search = spotify.search(&query.q).await.unwrap(); - Ok(HttpResponse::Ok().json(SearchResponse { track })) + Ok(HttpResponse::Ok().json(SearchResponse { + tracks: search + .tracks + .items + .into_iter() + .map(SearchTracks::from) + .collect::>(), + })) } diff --git a/src/routes/users.rs b/src/routes/users.rs index e4bc288..85fa1d7 100644 --- a/src/routes/users.rs +++ b/src/routes/users.rs @@ -24,18 +24,11 @@ fn get_a_user(user_id: &str) -> Result { match user { Ok(user) => Ok(user), - Err(DBError::NotFound) => { - return Err(ErrorResponse { - message: "User not found".to_string(), - status: StatusCode::NOT_FOUND, - }) - } - _ => { - return Err(ErrorResponse { - message: "Unknown error".to_string(), - status: StatusCode::INTERNAL_SERVER_ERROR, - }) - } + Err(DBError::NotFound) => Err(ErrorResponse::new("User not found", StatusCode::NOT_FOUND)), + _ => Err(ErrorResponse::new( + "Unknown error", + StatusCode::INTERNAL_SERVER_ERROR, + )), } } diff --git a/src/schema.rs b/src/schema.rs index 3df305e..17fb37c 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -6,9 +6,10 @@ diesel::table! { id -> Varchar, #[max_length = 255] name -> Varchar, - created_at -> Timestamp, + slug -> Varchar, + created_at -> Nullable, updated_at -> Nullable, - #[max_length = 21] + #[max_length = 22] spotify_id -> Nullable, #[max_length = 10] tidal_id -> Nullable, @@ -33,7 +34,7 @@ diesel::table! { public -> Bool, #[max_length = 24] creator_id -> Varchar, - created_at -> Timestamp, + created_at -> Nullable, updated_at -> Nullable, } } @@ -54,9 +55,9 @@ diesel::table! { #[max_length = 255] title -> Varchar, duration_ms -> Int4, - created_at -> Timestamp, + created_at -> Nullable, updated_at -> Nullable, - #[max_length = 21] + #[max_length = 22] spotify_id -> Nullable, #[max_length = 10] tidal_id -> Nullable, @@ -73,7 +74,7 @@ diesel::table! { email -> Varchar, password -> Text, updated_at -> Nullable, - created_at -> Timestamp, + created_at -> Nullable, } } diff --git a/src/services/service.rs b/src/services/service.rs index 9f6807b..24f7956 100644 --- a/src/services/service.rs +++ b/src/services/service.rs @@ -4,6 +4,8 @@ use async_trait::async_trait; pub trait Service: Send + Sync { fn get_token(&self) -> &String; - async fn fetch_token(&mut self) -> &String; + async fn fetch_token(&mut self); // async fn get_track_by_isrc(&self, isrc: &str); + + fn is_expired(&self) -> bool; } diff --git a/src/services/spotify.rs b/src/services/spotify.rs index 0cfd332..251737b 100644 --- a/src/services/spotify.rs +++ b/src/services/spotify.rs @@ -1,37 +1,50 @@ -use crate::services::service::Service; +use crate::{models::spotify::APISearchResponse, services::service::Service, utils::get_env}; use actix_web::http::Method; use async_trait::async_trait; +use chrono::{Duration, Utc}; +use lazy_static::lazy_static; use reqwest::{Client, RequestBuilder}; use serde::Deserialize; +use std::sync::Mutex; #[derive(Deserialize)] struct SpotifyResponse { access_token: String, + expires_in: i64, } #[derive(Clone)] pub struct Spotify { token: String, + expires_at: i64, } -pub async fn instance<'a>() -> Spotify { - Spotify::new().await +lazy_static! { + static ref SPOTIFY_INSTANCE: Mutex = Mutex::new(Spotify::new()); +} + +pub async fn instance() -> Spotify { + let mut spotify = SPOTIFY_INSTANCE.lock().unwrap().clone(); + + // Fetch token if expired + spotify.fetch_token().await; + + spotify } impl Spotify { - pub async fn new() -> Spotify { - let mut spotify = Spotify { + pub fn new() -> Spotify { + Spotify { token: String::new(), - }; - spotify.fetch_token().await; - spotify + expires_at: 0, + } } fn api(&self, url: &str, method: Method) -> RequestBuilder { let client = Client::new(); client - .request(method, &format!("https://api.spotify.com/v1{url}")) + .request(method, format!("https://api.spotify.com/v1{url}")) .bearer_auth(self.get_token()) } @@ -39,11 +52,33 @@ impl Spotify { // let spotify = &SPOTIFY; // } - pub async fn get_artist(self, spotify_id: &str) {} + // pub async fn get_artist(self, spotify_id: &str) {} - pub async fn get_track_by_isrc(self, isrc: &str) {} + // pub async fn get_track_by_isrc(self, isrc: &str) {} - pub async fn search(self, search: &str) {} + pub async fn search(&self, search: &str) -> Result { + let response = self + .api("/search", Method::GET) + .query(&[("q", search), ("type", "album,artist,track")]) + .send() + .await; + + match response { + Ok(res) => { + let data = res.json::().await; + + match data { + Ok(data) => Ok(data), + Err(_) => { + panic!("Invalid response (Spotify Search)") + } + } + } + Err(_) => { + panic!("Invalid response (Spotify Search)") + } + } + } } #[async_trait] @@ -52,7 +87,15 @@ impl Service for Spotify { &self.token } - async fn fetch_token(&mut self) -> &String { + fn is_expired(&self) -> bool { + self.expires_at < chrono::Utc::now().timestamp() + } + + async fn fetch_token(&mut self) { + if !self.is_expired() { + return; + } + let response = Client::new() .post("https://accounts.spotify.com/api/token") .basic_auth( @@ -63,12 +106,22 @@ impl Service for Spotify { .send() .await; + println!("Hello"); + match response { Ok(res) => { - let data: Result = res.json().await; + let data = res.json::().await; - self.token = data.unwrap().access_token.into(); - &self.token + match data { + Ok(data) => { + self.token = data.access_token; + self.expires_at = + (Utc::now() + Duration::seconds(data.expires_in)).timestamp(); + } + Err(_) => { + panic!("Invalid response (Spotify Token)") + } + } } Err(_) => { panic!("Invalid response (Spotify Token)") @@ -76,7 +129,3 @@ impl Service for Spotify { } } } - -fn get_env(key: &str) -> String { - std::env::var(key).unwrap() -} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..c68c3e4 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,3 @@ +pub fn get_env(key: &str) -> String { + std::env::var(key).unwrap() +}