[API-1] feat: search route
This commit is contained in:
parent
57588c7e13
commit
96a091f068
24 changed files with 388 additions and 127 deletions
26
Cargo.lock
generated
26
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
$$;
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -19,23 +19,20 @@ pub fn get_user(req: HttpRequest) -> Result<Users, ErrorResponse> {
|
|||
}
|
||||
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,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<NaiveDateTime>,
|
||||
pub created_at: Option<NaiveDateTime>,
|
||||
pub updated_at: Option<NaiveDateTime>,
|
||||
|
||||
pub spotify_id: Option<String>,
|
||||
pub tidal_id: Option<String>,
|
||||
|
@ -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<NaiveDateTime>,
|
||||
pub created_at: Option<NaiveDateTime>,
|
||||
pub updated_at: Option<NaiveDateTime>,
|
||||
|
||||
pub spotify_id: Option<String>,
|
||||
pub tidal_id: Option<String>,
|
||||
|
@ -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,
|
||||
|
|
|
@ -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<NaiveDateTime>,
|
||||
pub updated_at: Option<NaiveDateTime>,
|
||||
}
|
||||
|
||||
|
@ -36,7 +36,7 @@ pub struct Playlists {
|
|||
pub public: bool,
|
||||
pub creator_id: String,
|
||||
|
||||
pub created_at: NaiveDateTime,
|
||||
pub created_at: Option<NaiveDateTime>,
|
||||
pub updated_at: Option<NaiveDateTime>,
|
||||
}
|
||||
|
||||
|
@ -74,7 +74,7 @@ impl Playlists {
|
|||
let tracks = Tracks::find_by_playlist(&self.id)?;
|
||||
let tracks: Vec<TracksWithArtists> = tracks
|
||||
.into_iter()
|
||||
.map(Track::with_artists)
|
||||
.map(Tracks::with_artists)
|
||||
.collect::<Result<Vec<TracksWithArtists>, _>>()?;
|
||||
|
||||
Ok(tracks)
|
||||
|
|
|
@ -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>,
|
||||
}
|
||||
|
|
|
@ -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<NaiveDateTime>,
|
||||
|
||||
pub spotify_id: Option<String>,
|
||||
pub tidal_id: Option<String>,
|
||||
}
|
||||
|
||||
#[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<NaiveDateTime>,
|
||||
pub updated_at: Option<NaiveDateTime>,
|
||||
|
||||
pub spotify_id: Option<String>,
|
||||
|
@ -45,10 +32,10 @@ impl Tracks {
|
|||
Ok(playlist)
|
||||
}
|
||||
|
||||
pub fn create(track: Track) -> Result<Self, Error> {
|
||||
pub fn create(track: Tracks) -> Result<Self, Error> {
|
||||
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::<Vec<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> {
|
||||
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<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)]
|
||||
pub struct TracksWithArtists {
|
||||
pub id: String,
|
||||
|
|
|
@ -18,8 +18,8 @@ pub struct User {
|
|||
|
||||
pub password: String,
|
||||
|
||||
pub created_at: Option<NaiveDateTime>,
|
||||
pub updated_at: Option<NaiveDateTime>,
|
||||
pub created_at: NaiveDateTime,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Queryable, Serialize)]
|
||||
|
@ -30,8 +30,8 @@ pub struct Users {
|
|||
|
||||
pub password: String,
|
||||
|
||||
pub created_at: Option<NaiveDateTime>,
|
||||
pub updated_at: Option<NaiveDateTime>,
|
||||
pub created_at: NaiveDateTime,
|
||||
}
|
||||
|
||||
impl Users {
|
||||
|
|
|
@ -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<LoginBody>) -> Result<HttpResponse, ErrorResponse
|
|||
#[derive(Deserialize, Serialize)]
|
||||
struct Response {
|
||||
access_token: String,
|
||||
exp: usize,
|
||||
exp: i64,
|
||||
}
|
||||
|
||||
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 {
|
||||
user_id: user.id.to_string(),
|
||||
exp: exp as usize,
|
||||
exp,
|
||||
};
|
||||
let token = encode(&Header::default(), &claims, &get_encoding_key()).unwrap();
|
||||
|
||||
Ok(HttpResponse::Ok().json(Response {
|
||||
access_token: token,
|
||||
exp: exp as usize,
|
||||
exp,
|
||||
}))
|
||||
}
|
||||
Err(_e) => {
|
||||
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,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
12
src/routes/import.rs
Normal file
12
src/routes/import.rs
Normal 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())
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
pub mod auth;
|
||||
pub mod import;
|
||||
pub mod me;
|
||||
pub mod playlists;
|
||||
pub mod search;
|
||||
|
|
|
@ -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<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("")]
|
||||
async fn search(query: web::Query<SearchQuery>) -> Result<HttpResponse, ErrorResponse> {
|
||||
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::<Vec<SearchTracks>>(),
|
||||
}))
|
||||
}
|
||||
|
|
|
@ -24,18 +24,11 @@ fn get_a_user(user_id: &str) -> Result<Users, ErrorResponse> {
|
|||
|
||||
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,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,9 +6,10 @@ diesel::table! {
|
|||
id -> Varchar,
|
||||
#[max_length = 255]
|
||||
name -> Varchar,
|
||||
created_at -> Timestamp,
|
||||
slug -> Varchar,
|
||||
created_at -> Nullable<Timestamp>,
|
||||
updated_at -> Nullable<Timestamp>,
|
||||
#[max_length = 21]
|
||||
#[max_length = 22]
|
||||
spotify_id -> Nullable<Varchar>,
|
||||
#[max_length = 10]
|
||||
tidal_id -> Nullable<Varchar>,
|
||||
|
@ -33,7 +34,7 @@ diesel::table! {
|
|||
public -> Bool,
|
||||
#[max_length = 24]
|
||||
creator_id -> Varchar,
|
||||
created_at -> Timestamp,
|
||||
created_at -> Nullable<Timestamp>,
|
||||
updated_at -> Nullable<Timestamp>,
|
||||
}
|
||||
}
|
||||
|
@ -54,9 +55,9 @@ diesel::table! {
|
|||
#[max_length = 255]
|
||||
title -> Varchar,
|
||||
duration_ms -> Int4,
|
||||
created_at -> Timestamp,
|
||||
created_at -> Nullable<Timestamp>,
|
||||
updated_at -> Nullable<Timestamp>,
|
||||
#[max_length = 21]
|
||||
#[max_length = 22]
|
||||
spotify_id -> Nullable<Varchar>,
|
||||
#[max_length = 10]
|
||||
tidal_id -> Nullable<Varchar>,
|
||||
|
@ -73,7 +74,7 @@ diesel::table! {
|
|||
email -> Varchar,
|
||||
password -> Text,
|
||||
updated_at -> Nullable<Timestamp>,
|
||||
created_at -> Timestamp,
|
||||
created_at -> Nullable<Timestamp>,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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<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 {
|
||||
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<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]
|
||||
|
@ -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<SpotifyResponse, _> = res.json().await;
|
||||
let data = res.json::<SpotifyResponse>().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()
|
||||
}
|
||||
|
|
3
src/utils.rs
Normal file
3
src/utils.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
pub fn get_env(key: &str) -> String {
|
||||
std::env::var(key).unwrap()
|
||||
}
|
Loading…
Reference in a new issue