[API-1] feat: search route

This commit is contained in:
Miguel da Mota 2024-01-01 23:58:26 +01:00
parent 57588c7e13
commit 96a091f068
24 changed files with 388 additions and 127 deletions

26
Cargo.lock generated
View file

@ -653,6 +653,12 @@ version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
[[package]]
name = "futures-io"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
[[package]] [[package]]
name = "futures-sink" name = "futures-sink"
version = "0.3.30" version = "0.3.30"
@ -672,9 +678,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-io",
"futures-task", "futures-task",
"memchr",
"pin-project-lite", "pin-project-lite",
"pin-utils", "pin-utils",
"slab",
] ]
[[package]] [[package]]
@ -731,6 +740,12 @@ version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
[[package]]
name = "hermit-abi"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7"
[[package]] [[package]]
name = "hmac" name = "hmac"
version = "0.12.1" version = "0.12.1"
@ -1067,6 +1082,16 @@ dependencies = [
"autocfg", "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]] [[package]]
name = "object" name = "object"
version = "0.32.2" version = "0.32.2"
@ -1708,6 +1733,7 @@ dependencies = [
"bytes", "bytes",
"libc", "libc",
"mio", "mio",
"num_cpus",
"parking_lot", "parking_lot",
"pin-project-lite", "pin-project-lite",
"signal-hook-registry", "signal-hook-registry",

View file

@ -15,7 +15,7 @@ diesel = { version = "2", features = ["r2d2", "postgres", "chrono"] }
diesel_migrations = "2" diesel_migrations = "2"
dotenvy = "*" dotenvy = "*"
hmac = "0.12" hmac = "0.12"
reqwest = { version = "0.11", features = ["json"] } reqwest = { version = "0.11", features = ["blocking", "json"] }
sha2 = "0.10" sha2 = "0.10"
lazy_static = "1" lazy_static = "1"
jsonwebtoken = "9" jsonwebtoken = "9"

View file

@ -34,3 +34,99 @@ BEGIN
RETURN NEW; RETURN NEW;
END; END;
$$ LANGUAGE plpgsql; $$ 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
$$;

View file

@ -7,7 +7,7 @@ create table if not exists users
password text not null, password text not null,
updated_at timestamp, updated_at timestamp,
created_at timestamp default now() not null, created_at timestamp default now(),
primary key (id) primary key (id)
); );

View file

@ -2,11 +2,11 @@ create table if not exists playlists
( (
id varchar(24) default nanoid(24), id varchar(24) default nanoid(24),
name varchar(255) not null, name varchar(255) not null,
public bool default false, public bool default false not null,
creator_id varchar(24) not null, creator_id varchar(24) not null,
created_at timestamp default now() not null, created_at timestamp default now(),
updated_at timestamp, updated_at timestamp,
primary key (id), primary key (id),

View file

@ -5,11 +5,11 @@ create table if not exists tracks
title varchar(255) not null, title varchar(255) not null,
duration_ms int not null default 0, duration_ms int not null default 0,
created_at timestamp default now() not null, created_at timestamp default now(),
updated_at timestamp, updated_at timestamp,
-- music services -- music services
spotify_id varchar(21) unique, spotify_id varchar(22) unique,
tidal_id varchar(10) unique, tidal_id varchar(10) unique,
primary key (id) primary key (id)

View file

@ -3,13 +3,14 @@ create table if not exists artists
( (
id varchar(24) default nanoid(24), id varchar(24) default nanoid(24),
name varchar(255) NOT NULL, name varchar(255) NOT NULL,
slug varchar unique not null generated always as (lower(replace(name, ' ', '-'))) stored,
created_at TIMESTAMP DEFAULT now() NOT NULL, created_at timestamp default now(),
updated_at TIMESTAMP, updated_at timestamp,
-- music services -- music services
spotify_id VARCHAR(21) UNIQUE, spotify_id varchar(22) unique,
tidal_id VARCHAR(10) UNIQUE, tidal_id varchar(10) unique,
primary key (id) primary key (id)
); );

View file

@ -6,21 +6,26 @@ mod models;
mod routes; mod routes;
mod schema; mod schema;
mod services; mod services;
mod utils;
use crate::helpers::db; use crate::helpers::db;
use actix_web::{web, App, HttpServer};
#[actix_web::main] #[actix_web::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
use actix_web::{web, App, HttpServer};
dotenvy::dotenv().expect("No .env file found"); dotenvy::dotenv().expect("No .env file found");
std::env::set_var("RUST_LOG", "debug"); std::env::set_var("RUST_LOG", "debug");
// Register services.
let _ = services::spotify::instance().await;
db::init(); db::init();
HttpServer::new(move || { HttpServer::new(move || {
App::new() App::new()
.service(web::scope("/playlists").service(routes::playlists::get_playlist)) .service(web::scope("/playlists").service(routes::playlists::get_playlist))
.service(routes::auth::login) .service(routes::auth::routes())
.service(routes::me::routes()) .service(routes::me::routes())
.service(routes::users::routes()) .service(routes::users::routes())
.service(routes::search::routes()) .service(routes::search::routes())

View file

@ -9,6 +9,15 @@ pub struct ErrorResponse {
pub status: StatusCode, pub status: StatusCode,
} }
impl ErrorResponse {
pub fn new(message: &str, status: StatusCode) -> Self {
ErrorResponse {
message: message.to_string(),
status,
}
}
}
impl Display for ErrorResponse { impl Display for ErrorResponse {
fn fmt(&self, f: &mut Formatter) -> Result { fn fmt(&self, f: &mut Formatter) -> Result {
write!(f, "{}: {}", self.status, self.message) write!(f, "{}: {}", self.status, self.message)

View file

@ -19,23 +19,20 @@ pub fn get_user(req: HttpRequest) -> Result<Users, ErrorResponse> {
} }
Err(e) => { Err(e) => {
return Err(match e.kind() { return Err(match e.kind() {
jsonwebtoken::errors::ErrorKind::ExpiredSignature => ErrorResponse { jsonwebtoken::errors::ErrorKind::ExpiredSignature => {
message: "Not Authorized".to_string(), ErrorResponse::new("Not Authorized", StatusCode::UNAUTHORIZED)
status: StatusCode::UNAUTHORIZED, }
}, _ => ErrorResponse::new(
_ => ErrorResponse { e.to_string().as_str(),
message: e.to_string(), StatusCode::INTERNAL_SERVER_ERROR,
status: StatusCode::INTERNAL_SERVER_ERROR, ),
},
}) })
} }
} }
} }
None => { None => Err(ErrorResponse::new(
return Err(ErrorResponse { "Not Authorized",
message: "Not Authorized".to_string(), StatusCode::UNAUTHORIZED,
status: StatusCode::UNAUTHORIZED, )),
});
}
} }
} }

View file

@ -13,9 +13,10 @@ use serde::{Deserialize, Serialize};
pub struct Artist { pub struct Artist {
pub id: String, pub id: String,
pub name: String, pub name: String,
pub slug: String,
created_at: NaiveDateTime, pub created_at: Option<NaiveDateTime>,
updated_at: Option<NaiveDateTime>, pub updated_at: Option<NaiveDateTime>,
pub spotify_id: Option<String>, pub spotify_id: Option<String>,
pub tidal_id: Option<String>, pub tidal_id: Option<String>,
@ -25,9 +26,10 @@ pub struct Artist {
pub struct Artists { pub struct Artists {
pub id: String, pub id: String,
pub title: String, pub title: String,
pub slug: String,
created_at: NaiveDateTime, pub created_at: Option<NaiveDateTime>,
updated_at: Option<NaiveDateTime>, pub updated_at: Option<NaiveDateTime>,
pub spotify_id: Option<String>, pub spotify_id: Option<String>,
pub tidal_id: Option<String>, pub tidal_id: Option<String>,
@ -68,6 +70,7 @@ impl Artist {
Artist { Artist {
id: artist.id, id: artist.id,
name: artist.name, name: artist.name,
slug: artist.slug,
created_at: artist.created_at, created_at: artist.created_at,
updated_at: artist.updated_at, updated_at: artist.updated_at,

View file

@ -1,5 +1,5 @@
use crate::helpers::db; use crate::helpers::db;
use crate::models::tracks::{Track, Tracks, TracksWithArtists}; use crate::models::tracks::{Tracks, TracksWithArtists};
use crate::models::user::Users; use crate::models::user::Users;
use crate::schema::playlists; use crate::schema::playlists;
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
@ -25,7 +25,7 @@ pub struct Playlist {
pub public: bool, pub public: bool,
pub creator_id: String, pub creator_id: String,
pub created_at: NaiveDateTime, pub created_at: Option<NaiveDateTime>,
pub updated_at: Option<NaiveDateTime>, pub updated_at: Option<NaiveDateTime>,
} }
@ -36,7 +36,7 @@ pub struct Playlists {
pub public: bool, pub public: bool,
pub creator_id: String, pub creator_id: String,
pub created_at: NaiveDateTime, pub created_at: Option<NaiveDateTime>,
pub updated_at: Option<NaiveDateTime>, pub updated_at: Option<NaiveDateTime>,
} }
@ -74,7 +74,7 @@ impl Playlists {
let tracks = Tracks::find_by_playlist(&self.id)?; let tracks = Tracks::find_by_playlist(&self.id)?;
let tracks: Vec<TracksWithArtists> = tracks let tracks: Vec<TracksWithArtists> = tracks
.into_iter() .into_iter()
.map(Track::with_artists) .map(Tracks::with_artists)
.collect::<Result<Vec<TracksWithArtists>, _>>()?; .collect::<Result<Vec<TracksWithArtists>, _>>()?;
Ok(tracks) Ok(tracks)

View file

@ -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<Artist>,
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<Artist>,
}
#[derive(Deserialize, Serialize)]
pub struct APISearchResponse {
pub albums: APISearchItems<Album>,
pub artists: APISearchItems<Artist>,
pub tracks: APISearchItems<Track>,
}
#[derive(Deserialize, Serialize)]
pub struct APISearchItems<T> {
pub items: Vec<T>,
}

View file

@ -1,5 +1,5 @@
use crate::helpers::db; use crate::helpers::db;
use crate::models::artists::Artists; use crate::models::{artists::Artists, spotify};
use crate::schema::tracks; use crate::schema::tracks;
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use diesel::result::Error; use diesel::result::Error;
@ -10,28 +10,15 @@ use serde::{Deserialize, Serialize};
use crate::schema::playlists_tracks; 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(table_name = crate::schema::tracks)]
#[diesel(check_for_backend(diesel::pg::Pg))] #[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<NaiveDateTime>,
pub spotify_id: Option<String>,
pub tidal_id: Option<String>,
}
#[derive(Debug, Deserialize, Queryable, Serialize)]
pub struct Tracks { pub struct Tracks {
pub id: String, pub id: String,
pub title: String, pub title: String,
pub duration_ms: i32, pub duration_ms: i32,
pub created_at: NaiveDateTime, pub created_at: Option<NaiveDateTime>,
pub updated_at: Option<NaiveDateTime>, pub updated_at: Option<NaiveDateTime>,
pub spotify_id: Option<String>, pub spotify_id: Option<String>,
@ -45,10 +32,10 @@ impl Tracks {
Ok(playlist) Ok(playlist)
} }
pub fn create(track: Track) -> Result<Self, Error> { pub fn create(track: Tracks) -> Result<Self, Error> {
let conn = &mut db::connection()?; let conn = &mut db::connection()?;
let playlist = diesel::insert_into(tracks::table) let playlist = diesel::insert_into(tracks::table)
.values(Track::from(track)) .values(Tracks::from(track))
.get_result(conn)?; .get_result(conn)?;
Ok(playlist) Ok(playlist)
} }
@ -61,24 +48,30 @@ impl Tracks {
let tracks = tracks let tracks = tracks
.into_iter() .into_iter()
.map(|(playlist_id, track_id)| { .map(|(_, track_id)| Tracks::find(track_id).unwrap())
println!("{}: {}", playlist_id, track_id);
Tracks::find(track_id).unwrap()
})
.collect::<Vec<Tracks>>(); .collect::<Vec<Tracks>>();
Ok(tracks) Ok(tracks)
} }
pub fn create_or_update(track: Tracks) -> Result<Self, Error> {
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<Vec<Artists>, Error> { pub fn get_artists(&self) -> Result<Vec<Artists>, Error> {
let artists = Artists::find_by_track(&self.id)?; let artists = Artists::find_by_track(&self.id)?;
Ok(artists) Ok(artists)
} }
}
impl Track { fn from(track: Tracks) -> Self {
fn from(track: Track) -> Track { Tracks {
Track {
id: track.id, id: track.id,
title: track.title, title: track.title,
duration_ms: track.duration_ms, duration_ms: track.duration_ms,
@ -107,6 +100,20 @@ impl Track {
} }
} }
impl From<spotify::Track> 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)] #[derive(Deserialize, Serialize)]
pub struct TracksWithArtists { pub struct TracksWithArtists {
pub id: String, pub id: String,

View file

@ -18,8 +18,8 @@ pub struct User {
pub password: String, pub password: String,
pub created_at: Option<NaiveDateTime>,
pub updated_at: Option<NaiveDateTime>, pub updated_at: Option<NaiveDateTime>,
pub created_at: NaiveDateTime,
} }
#[derive(Debug, Deserialize, Queryable, Serialize)] #[derive(Debug, Deserialize, Queryable, Serialize)]
@ -30,8 +30,8 @@ pub struct Users {
pub password: String, pub password: String,
pub created_at: Option<NaiveDateTime>,
pub updated_at: Option<NaiveDateTime>, pub updated_at: Option<NaiveDateTime>,
pub created_at: NaiveDateTime,
} }
impl Users { impl Users {

View file

@ -2,15 +2,19 @@ use crate::helpers::jwt::get_encoding_key;
use crate::middlewares::error::ErrorResponse; use crate::middlewares::error::ErrorResponse;
use crate::models::user::Users; use crate::models::user::Users;
use actix_web::http::StatusCode; use actix_web::http::StatusCode;
use actix_web::{post, web, HttpResponse}; use actix_web::{post, web, HttpResponse, Scope};
use chrono::{Days, Utc}; use chrono::{Days, Utc};
use jsonwebtoken::{encode, Header}; use jsonwebtoken::{encode, Header};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
pub fn routes() -> Scope {
web::scope("/auth").service(login)
}
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct JWTClaims { pub struct JWTClaims {
pub user_id: String, pub user_id: String,
pub exp: usize, pub exp: i64,
} }
#[post("/auth/login")] #[post("/auth/login")]
@ -18,7 +22,7 @@ async fn login(body: web::Json<LoginBody>) -> Result<HttpResponse, ErrorResponse
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
struct Response { struct Response {
access_token: String, access_token: String,
exp: usize, exp: i64,
} }
let user = Users::find_by_email(&body.email); let user = Users::find_by_email(&body.email);
@ -32,28 +36,24 @@ async fn login(body: web::Json<LoginBody>) -> Result<HttpResponse, ErrorResponse
let claims = JWTClaims { let claims = JWTClaims {
user_id: user.id.to_string(), user_id: user.id.to_string(),
exp: exp as usize, exp,
}; };
let token = encode(&Header::default(), &claims, &get_encoding_key()).unwrap(); let token = encode(&Header::default(), &claims, &get_encoding_key()).unwrap();
Ok(HttpResponse::Ok().json(Response { Ok(HttpResponse::Ok().json(Response {
access_token: token, access_token: token,
exp: exp as usize, exp,
})) }))
} }
Err(_e) => { Err(_e) => Err(ErrorResponse::new(
return Err(ErrorResponse { "Invalid credentials",
message: "Invalid credentials.".to_string(), StatusCode::BAD_REQUEST,
status: StatusCode::BAD_REQUEST, )),
})
}
}, },
Err(_err) => { Err(_err) => Err(ErrorResponse::new(
return Err(ErrorResponse { "Invalid credentials",
message: "Invalid credentials.".to_string(), StatusCode::BAD_REQUEST,
status: StatusCode::BAD_REQUEST, )),
})
}
} }
} }

12
src/routes/import.rs Normal file
View file

@ -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<HttpResponse, ErrorResponse> {
Ok(HttpResponse::Ok().finish())
}

View file

@ -1,4 +1,5 @@
pub mod auth; pub mod auth;
pub mod import;
pub mod me; pub mod me;
pub mod playlists; pub mod playlists;
pub mod search; pub mod search;

View file

@ -1,5 +1,6 @@
use crate::middlewares::error::ErrorResponse; use crate::models::spotify;
use crate::services::spotify; use crate::services::spotify as Spotify;
use crate::{middlewares::error::ErrorResponse, models::tracks::Tracks};
use actix_web::{get, web, HttpResponse, Result, Scope}; use actix_web::{get, web, HttpResponse, Result, Scope};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -14,13 +15,31 @@ struct SearchQuery {
#[derive(Serialize)] #[derive(Serialize)]
struct SearchResponse { struct SearchResponse {
track: (), tracks: Vec<SearchTracks>,
}
#[derive(Serialize)]
struct SearchTracks {
pub id: String,
}
impl From<spotify::Track> for SearchTracks {
fn from(value: spotify::Track) -> SearchTracks {
SearchTracks { id: value.id }
}
} }
#[get("")] #[get("")]
async fn search(query: web::Query<SearchQuery>) -> Result<HttpResponse, ErrorResponse> { async fn search(query: web::Query<SearchQuery>) -> Result<HttpResponse, ErrorResponse> {
let spotify = spotify::instance().await; let spotify = Spotify::instance().await;
let track = spotify.search(&query.q).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::<Vec<SearchTracks>>(),
}))
} }

View file

@ -24,18 +24,11 @@ fn get_a_user(user_id: &str) -> Result<Users, ErrorResponse> {
match user { match user {
Ok(user) => Ok(user), Ok(user) => Ok(user),
Err(DBError::NotFound) => { Err(DBError::NotFound) => Err(ErrorResponse::new("User not found", StatusCode::NOT_FOUND)),
return Err(ErrorResponse { _ => Err(ErrorResponse::new(
message: "User not found".to_string(), "Unknown error",
status: StatusCode::NOT_FOUND, StatusCode::INTERNAL_SERVER_ERROR,
}) )),
}
_ => {
return Err(ErrorResponse {
message: "Unknown error".to_string(),
status: StatusCode::INTERNAL_SERVER_ERROR,
})
}
} }
} }

View file

@ -6,9 +6,10 @@ diesel::table! {
id -> Varchar, id -> Varchar,
#[max_length = 255] #[max_length = 255]
name -> Varchar, name -> Varchar,
created_at -> Timestamp, slug -> Varchar,
created_at -> Nullable<Timestamp>,
updated_at -> Nullable<Timestamp>, updated_at -> Nullable<Timestamp>,
#[max_length = 21] #[max_length = 22]
spotify_id -> Nullable<Varchar>, spotify_id -> Nullable<Varchar>,
#[max_length = 10] #[max_length = 10]
tidal_id -> Nullable<Varchar>, tidal_id -> Nullable<Varchar>,
@ -33,7 +34,7 @@ diesel::table! {
public -> Bool, public -> Bool,
#[max_length = 24] #[max_length = 24]
creator_id -> Varchar, creator_id -> Varchar,
created_at -> Timestamp, created_at -> Nullable<Timestamp>,
updated_at -> Nullable<Timestamp>, updated_at -> Nullable<Timestamp>,
} }
} }
@ -54,9 +55,9 @@ diesel::table! {
#[max_length = 255] #[max_length = 255]
title -> Varchar, title -> Varchar,
duration_ms -> Int4, duration_ms -> Int4,
created_at -> Timestamp, created_at -> Nullable<Timestamp>,
updated_at -> Nullable<Timestamp>, updated_at -> Nullable<Timestamp>,
#[max_length = 21] #[max_length = 22]
spotify_id -> Nullable<Varchar>, spotify_id -> Nullable<Varchar>,
#[max_length = 10] #[max_length = 10]
tidal_id -> Nullable<Varchar>, tidal_id -> Nullable<Varchar>,
@ -73,7 +74,7 @@ diesel::table! {
email -> Varchar, email -> Varchar,
password -> Text, password -> Text,
updated_at -> Nullable<Timestamp>, updated_at -> Nullable<Timestamp>,
created_at -> Timestamp, created_at -> Nullable<Timestamp>,
} }
} }

View file

@ -4,6 +4,8 @@ use async_trait::async_trait;
pub trait Service: Send + Sync { pub trait Service: Send + Sync {
fn get_token(&self) -> &String; 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); // async fn get_track_by_isrc(&self, isrc: &str);
fn is_expired(&self) -> bool;
} }

View file

@ -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 actix_web::http::Method;
use async_trait::async_trait; use async_trait::async_trait;
use chrono::{Duration, Utc};
use lazy_static::lazy_static;
use reqwest::{Client, RequestBuilder}; use reqwest::{Client, RequestBuilder};
use serde::Deserialize; use serde::Deserialize;
use std::sync::Mutex;
#[derive(Deserialize)] #[derive(Deserialize)]
struct SpotifyResponse { struct SpotifyResponse {
access_token: String, access_token: String,
expires_in: i64,
} }
#[derive(Clone)] #[derive(Clone)]
pub struct Spotify { pub struct Spotify {
token: String, token: String,
expires_at: i64,
} }
pub async fn instance<'a>() -> Spotify { lazy_static! {
Spotify::new().await static ref SPOTIFY_INSTANCE: Mutex<Spotify> = 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 { impl Spotify {
pub async fn new() -> Spotify { pub fn new() -> Spotify {
let mut spotify = Spotify { Spotify {
token: String::new(), token: String::new(),
}; expires_at: 0,
spotify.fetch_token().await; }
spotify
} }
fn api(&self, url: &str, method: Method) -> RequestBuilder { fn api(&self, url: &str, method: Method) -> RequestBuilder {
let client = Client::new(); let client = Client::new();
client client
.request(method, &format!("https://api.spotify.com/v1{url}")) .request(method, format!("https://api.spotify.com/v1{url}"))
.bearer_auth(self.get_token()) .bearer_auth(self.get_token())
} }
@ -39,11 +52,33 @@ impl Spotify {
// let spotify = &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<APISearchResponse, ()> {
let response = self
.api("/search", Method::GET)
.query(&[("q", search), ("type", "album,artist,track")])
.send()
.await;
match response {
Ok(res) => {
let data = res.json::<APISearchResponse>().await;
match data {
Ok(data) => Ok(data),
Err(_) => {
panic!("Invalid response (Spotify Search)")
}
}
}
Err(_) => {
panic!("Invalid response (Spotify Search)")
}
}
}
} }
#[async_trait] #[async_trait]
@ -52,7 +87,15 @@ impl Service for Spotify {
&self.token &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() let response = Client::new()
.post("https://accounts.spotify.com/api/token") .post("https://accounts.spotify.com/api/token")
.basic_auth( .basic_auth(
@ -63,12 +106,22 @@ impl Service for Spotify {
.send() .send()
.await; .await;
println!("Hello");
match response { match response {
Ok(res) => { Ok(res) => {
let data: Result<SpotifyResponse, _> = res.json().await; let data = res.json::<SpotifyResponse>().await;
self.token = data.unwrap().access_token.into(); match data {
&self.token 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(_) => { Err(_) => {
panic!("Invalid response (Spotify Token)") panic!("Invalid response (Spotify Token)")
@ -76,7 +129,3 @@ impl Service for Spotify {
} }
} }
} }
fn get_env(key: &str) -> String {
std::env::var(key).unwrap()
}

3
src/utils.rs Normal file
View file

@ -0,0 +1,3 @@
pub fn get_env(key: &str) -> String {
std::env::var(key).unwrap()
}