[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"
|
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",
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
$$;
|
||||||
|
|
|
@ -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)
|
||||||
);
|
);
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
);
|
);
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
)),
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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::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,
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
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 auth;
|
||||||
|
pub mod import;
|
||||||
pub mod me;
|
pub mod me;
|
||||||
pub mod playlists;
|
pub mod playlists;
|
||||||
pub mod search;
|
pub mod search;
|
||||||
|
|
|
@ -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>>(),
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
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