From 57588c7e1321e168298137316d14108f27cc3359 Mon Sep 17 00:00:00 2001 From: Miguel da Mota Date: Mon, 1 Jan 2024 16:26:44 +0100 Subject: [PATCH 1/4] [API-1] feat: search and spotify service --- Cargo.lock | 370 +++++++++++++++++++++++++++++++++++++++- Cargo.toml | 1 + src/main.rs | 2 + src/models/mod.rs | 1 + src/models/spotify.rs | 5 + src/routes/mod.rs | 1 + src/routes/search.rs | 26 +++ src/services/mod.rs | 2 + src/services/service.rs | 9 + src/services/spotify.rs | 82 +++++++++ 10 files changed, 495 insertions(+), 4 deletions(-) create mode 100644 src/models/spotify.rs create mode 100644 src/routes/search.rs create mode 100644 src/services/mod.rs create mode 100644 src/services/service.rs create mode 100644 src/services/spotify.rs diff --git a/Cargo.lock b/Cargo.lock index 38e001b..f8730de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -437,6 +437,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.6" @@ -572,6 +582,22 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + [[package]] name = "flate2" version = "1.0.28" @@ -588,6 +614,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -597,6 +638,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", +] + [[package]] name = "futures-core" version = "0.3.30" @@ -701,6 +751,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + [[package]] name = "httparse" version = "1.8.0" @@ -713,6 +774,43 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hyper" +version = "0.14.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + [[package]] name = "iana-time-zone" version = "0.1.59" @@ -765,6 +863,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + [[package]] name = "itoa" version = "1.0.10" @@ -822,6 +926,12 @@ version = "0.2.151" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" +[[package]] +name = "linux-raw-sys" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" + [[package]] name = "local-channel" version = "0.1.5" @@ -906,7 +1016,25 @@ dependencies = [ "libc", "log", "wasi", - "windows-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", ] [[package]] @@ -954,6 +1082,50 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "openssl" +version = "0.10.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cde4d2d9200ad5909f8dac647e29482e07c3a35de8a13fce7c9c7747ad9f671" +dependencies = [ + "bitflags 2.4.1", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.44", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1665caf8ab2dc9aef43d1c0023bd904633a6a05cb30b0ad59bec2ae986e57a7" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "parking_lot" version = "0.12.1" @@ -1135,6 +1307,44 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +[[package]] +name = "reqwest" +version = "0.11.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + [[package]] name = "ring" version = "0.17.7" @@ -1146,7 +1356,7 @@ dependencies = [ "libc", "spin", "untrusted", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1164,12 +1374,34 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.38.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" +dependencies = [ + "bitflags 2.4.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + [[package]] name = "ryu" version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" +[[package]] +name = "schannel" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "scheduled-thread-pool" version = "0.2.7" @@ -1185,6 +1417,29 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.20" @@ -1308,7 +1563,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1345,6 +1600,40 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall", + "rustix", + "windows-sys 0.52.0", +] + [[package]] name = "thiserror" version = "1.0.53" @@ -1423,7 +1712,17 @@ dependencies = [ "pin-project-lite", "signal-hook-registry", "socket2", - "windows-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", ] [[package]] @@ -1474,6 +1773,12 @@ dependencies = [ "winnow", ] +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + [[package]] name = "tracing" version = "0.1.40" @@ -1494,6 +1799,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "typenum" version = "1.17.0" @@ -1565,11 +1876,21 @@ dependencies = [ "hmac", "jsonwebtoken", "lazy_static", + "reqwest", "serde", "serde_json", "sha2", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -1601,6 +1922,18 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac36a15a220124ac510204aec1c3e5db8a22ab06fd6706d881dc6149f8ed9a12" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.89" @@ -1630,6 +1963,16 @@ version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" +[[package]] +name = "web-sys" +version = "0.3.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50c24a44ec86bb68fbecd1b3efed7e85ea5621b39b35ef2766b66cd984f8010f" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "windows-core" version = "0.52.0" @@ -1648,6 +1991,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -1771,6 +2123,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "zerocopy" version = "0.7.32" diff --git a/Cargo.toml b/Cargo.toml index c8a5e4e..40fae80 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ diesel = { version = "2", features = ["r2d2", "postgres", "chrono"] } diesel_migrations = "2" dotenvy = "*" hmac = "0.12" +reqwest = { version = "0.11", features = ["json"] } sha2 = "0.10" lazy_static = "1" jsonwebtoken = "9" diff --git a/src/main.rs b/src/main.rs index aaa20f1..8705e46 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ mod middlewares; mod models; mod routes; mod schema; +mod services; use crate::helpers::db; use actix_web::{web, App, HttpServer}; @@ -22,6 +23,7 @@ async fn main() -> std::io::Result<()> { .service(routes::auth::login) .service(routes::me::routes()) .service(routes::users::routes()) + .service(routes::search::routes()) }) .bind(("127.0.0.1", 9000))? .run() diff --git a/src/models/mod.rs b/src/models/mod.rs index 048b749..62087e1 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,4 +1,5 @@ pub mod artists; pub mod playlists; +pub mod spotify; pub mod tracks; pub mod user; diff --git a/src/models/spotify.rs b/src/models/spotify.rs new file mode 100644 index 0000000..0145ab1 --- /dev/null +++ b/src/models/spotify.rs @@ -0,0 +1,5 @@ +pub struct Track {} + +pub struct Artist {} + +pub struct Album {} diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 339a10d..f643e8b 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -1,4 +1,5 @@ pub mod auth; pub mod me; pub mod playlists; +pub mod search; pub mod users; diff --git a/src/routes/search.rs b/src/routes/search.rs new file mode 100644 index 0000000..6528aed --- /dev/null +++ b/src/routes/search.rs @@ -0,0 +1,26 @@ +use crate::middlewares::error::ErrorResponse; +use crate::services::spotify; +use actix_web::{get, web, HttpResponse, Result, Scope}; +use serde::{Deserialize, Serialize}; + +pub fn routes() -> Scope { + web::scope("/search").service(search) +} + +#[derive(Deserialize)] +struct SearchQuery { + q: String, +} + +#[derive(Serialize)] +struct SearchResponse { + track: (), +} + +#[get("")] +async fn search(query: web::Query) -> Result { + let spotify = spotify::instance().await; + let track = spotify.search(&query.q).await; + + Ok(HttpResponse::Ok().json(SearchResponse { track })) +} diff --git a/src/services/mod.rs b/src/services/mod.rs new file mode 100644 index 0000000..1204d8d --- /dev/null +++ b/src/services/mod.rs @@ -0,0 +1,2 @@ +mod service; +pub mod spotify; diff --git a/src/services/service.rs b/src/services/service.rs new file mode 100644 index 0000000..9f6807b --- /dev/null +++ b/src/services/service.rs @@ -0,0 +1,9 @@ +use async_trait::async_trait; + +#[async_trait] +pub trait Service: Send + Sync { + fn get_token(&self) -> &String; + + async fn fetch_token(&mut self) -> &String; + // async fn get_track_by_isrc(&self, isrc: &str); +} diff --git a/src/services/spotify.rs b/src/services/spotify.rs new file mode 100644 index 0000000..0cfd332 --- /dev/null +++ b/src/services/spotify.rs @@ -0,0 +1,82 @@ +use crate::services::service::Service; +use actix_web::http::Method; +use async_trait::async_trait; +use reqwest::{Client, RequestBuilder}; +use serde::Deserialize; + +#[derive(Deserialize)] +struct SpotifyResponse { + access_token: String, +} + +#[derive(Clone)] +pub struct Spotify { + token: String, +} + +pub async fn instance<'a>() -> Spotify { + Spotify::new().await +} + +impl Spotify { + pub async fn new() -> Spotify { + let mut spotify = Spotify { + token: String::new(), + }; + spotify.fetch_token().await; + spotify + } + + fn api(&self, url: &str, method: Method) -> RequestBuilder { + let client = Client::new(); + + client + .request(method, &format!("https://api.spotify.com/v1{url}")) + .bearer_auth(self.get_token()) + } + + // pub async fn get_track(self, spotify_id: &str) { + // let spotify = &SPOTIFY; + // } + + pub async fn get_artist(self, spotify_id: &str) {} + + pub async fn get_track_by_isrc(self, isrc: &str) {} + + pub async fn search(self, search: &str) {} +} + +#[async_trait] +impl Service for Spotify { + fn get_token(&self) -> &String { + &self.token + } + + async fn fetch_token(&mut self) -> &String { + let response = Client::new() + .post("https://accounts.spotify.com/api/token") + .basic_auth( + get_env("SPOTIFY_CLIENT_ID"), + Some(get_env("SPOTIFY_CLIENT_SECRET")), + ) + .form(&[("grant_type", "client_credentials")]) + .send() + .await; + + match response { + Ok(res) => { + let data: Result = res.json().await; + + self.token = data.unwrap().access_token.into(); + &self.token + } + Err(_) => { + panic!("Invalid response (Spotify Token)") + } + } + } +} + +fn get_env(key: &str) -> String { + std::env::var(key).unwrap() +} From 96a091f0684a2a15d72fc79e72a2f76eb96e5c46 Mon Sep 17 00:00:00 2001 From: Miguel da Mota Date: Mon, 1 Jan 2024 23:58:26 +0100 Subject: [PATCH 2/4] [API-1] feat: search route --- Cargo.lock | 26 +++++ Cargo.toml | 2 +- .../up.sql | 96 +++++++++++++++++++ migrations/2023-12-29-191547_users/up.sql | 2 +- migrations/2023-12-30-162032_playlists/up.sql | 4 +- migrations/2023-12-30-192105_tracks/up.sql | 4 +- migrations/2023-12-31-104738_artists/up.sql | 9 +- src/main.rs | 9 +- src/middlewares/error.rs | 9 ++ src/middlewares/user.rs | 25 +++-- src/models/artists.rs | 11 ++- src/models/playlists.rs | 8 +- src/models/spotify.rs | 43 ++++++++- src/models/tracks.rs | 59 +++++++----- src/models/user.rs | 4 +- src/routes/auth.rs | 34 +++---- src/routes/import.rs | 12 +++ src/routes/mod.rs | 1 + src/routes/search.rs | 31 ++++-- src/routes/users.rs | 17 +--- src/schema.rs | 13 +-- src/services/service.rs | 4 +- src/services/spotify.rs | 89 +++++++++++++---- src/utils.rs | 3 + 24 files changed, 388 insertions(+), 127 deletions(-) create mode 100644 src/routes/import.rs create mode 100644 src/utils.rs diff --git a/Cargo.lock b/Cargo.lock index f8730de..d994919 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -653,6 +653,12 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + [[package]] name = "futures-sink" version = "0.3.30" @@ -672,9 +678,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-core", + "futures-io", "futures-task", + "memchr", "pin-project-lite", "pin-utils", + "slab", ] [[package]] @@ -731,6 +740,12 @@ version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +[[package]] +name = "hermit-abi" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" + [[package]] name = "hmac" version = "0.12.1" @@ -1067,6 +1082,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "object" version = "0.32.2" @@ -1708,6 +1733,7 @@ dependencies = [ "bytes", "libc", "mio", + "num_cpus", "parking_lot", "pin-project-lite", "signal-hook-registry", diff --git a/Cargo.toml b/Cargo.toml index 40fae80..6c1cd8e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ diesel = { version = "2", features = ["r2d2", "postgres", "chrono"] } diesel_migrations = "2" dotenvy = "*" hmac = "0.12" -reqwest = { version = "0.11", features = ["json"] } +reqwest = { version = "0.11", features = ["blocking", "json"] } sha2 = "0.10" lazy_static = "1" jsonwebtoken = "9" diff --git a/migrations/00000000000000_diesel_initial_setup/up.sql b/migrations/00000000000000_diesel_initial_setup/up.sql index d68895b..7e95698 100644 --- a/migrations/00000000000000_diesel_initial_setup/up.sql +++ b/migrations/00000000000000_diesel_initial_setup/up.sql @@ -34,3 +34,99 @@ BEGIN RETURN NEW; END; $$ LANGUAGE plpgsql; + +-- nanoid function +CREATE EXTENSION IF NOT EXISTS pgcrypto; + +-- The `nanoid()` function generates a compact, URL-friendly unique identifier. +-- Based on the given size and alphabet, it creates a randomized string that's ideal for +-- use-cases requiring small, unpredictable IDs (e.g., URL shorteners, generated file names, etc.). +-- While it comes with a default configuration, the function is designed to be flexible, +-- allowing for customization to meet specific needs. +DROP FUNCTION IF EXISTS nanoid(int, text, float); +CREATE OR REPLACE FUNCTION nanoid( + size int DEFAULT 21, -- The number of symbols in the NanoId String. Must be greater than 0. + alphabet text DEFAULT '_-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', -- The symbols used in the NanoId String. Must contain between 1 and 255 symbols. + additionalBytesFactor float DEFAULT 1.6 -- The additional bytes factor used for calculating the step size. Must be equal or greater then 1. +) + RETURNS text -- A randomly generated NanoId String + LANGUAGE plpgsql + VOLATILE + LEAKPROOF + PARALLEL SAFE +AS +$$ +DECLARE + alphabetArray text[]; + alphabetLength int := 64; + mask int := 63; + step int := 34; +BEGIN + IF size IS NULL OR size < 1 THEN + RAISE EXCEPTION 'The size must be defined and greater than 0!'; + END IF; + + IF alphabet IS NULL OR length(alphabet) = 0 OR length(alphabet) > 255 THEN + RAISE EXCEPTION 'The alphabet can''t be undefined, zero or bigger than 255 symbols!'; + END IF; + + IF additionalBytesFactor IS NULL OR additionalBytesFactor < 1 THEN + RAISE EXCEPTION 'The additional bytes factor can''t be less than 1!'; + END IF; + + alphabetArray := regexp_split_to_array(alphabet, ''); + alphabetLength := array_length(alphabetArray, 1); + mask := (2 << cast(floor(log(alphabetLength - 1) / log(2)) as int)) - 1; + step := cast(ceil(additionalBytesFactor * mask * size / alphabetLength) AS int); + + IF step > 1024 THEN + step := 1024; -- The step size % can''t be bigger then 1024! + END IF; + + RETURN nanoid_optimized(size, alphabet, mask, step); +END +$$; + +-- Generates an optimized random string of a specified size using the given alphabet, mask, and step. +-- This optimized version is designed for higher performance and lower memory overhead. +-- No checks are performed! Use it only if you really know what you are doing. +DROP FUNCTION IF EXISTS nanoid_optimized(int, text, int, int); +CREATE OR REPLACE FUNCTION nanoid_optimized( + size int, -- The desired length of the generated string. + alphabet text, -- The set of characters to choose from for generating the string. + mask int, -- The mask used for mapping random bytes to alphabet indices. Should be `(2^n) - 1` where `n` is a power of 2 less than or equal to the alphabet size. + step int -- The number of random bytes to generate in each iteration. A larger value may speed up the function but increase memory usage. +) + RETURNS text -- A randomly generated NanoId String + LANGUAGE plpgsql + VOLATILE + LEAKPROOF + PARALLEL SAFE +AS +$$ +DECLARE + idBuilder text := ''; + counter int := 0; + bytes bytea; + alphabetIndex int; + alphabetArray text[]; + alphabetLength int := 64; +BEGIN + alphabetArray := regexp_split_to_array(alphabet, ''); + alphabetLength := array_length(alphabetArray, 1); + + LOOP + bytes := gen_random_bytes(step); + FOR counter IN 0..step - 1 + LOOP + alphabetIndex := (get_byte(bytes, counter) & mask) + 1; + IF alphabetIndex <= alphabetLength THEN + idBuilder := idBuilder || alphabetArray[alphabetIndex]; + IF length(idBuilder) = size THEN + RETURN idBuilder; + END IF; + END IF; + END LOOP; + END LOOP; +END +$$; diff --git a/migrations/2023-12-29-191547_users/up.sql b/migrations/2023-12-29-191547_users/up.sql index 56dc507..65812bc 100644 --- a/migrations/2023-12-29-191547_users/up.sql +++ b/migrations/2023-12-29-191547_users/up.sql @@ -7,7 +7,7 @@ create table if not exists users password text not null, updated_at timestamp, - created_at timestamp default now() not null, + created_at timestamp default now(), primary key (id) ); diff --git a/migrations/2023-12-30-162032_playlists/up.sql b/migrations/2023-12-30-162032_playlists/up.sql index 18789f0..6f496d7 100644 --- a/migrations/2023-12-30-162032_playlists/up.sql +++ b/migrations/2023-12-30-162032_playlists/up.sql @@ -2,11 +2,11 @@ create table if not exists playlists ( id varchar(24) default nanoid(24), name varchar(255) not null, - public bool default false, + public bool default false not null, creator_id varchar(24) not null, - created_at timestamp default now() not null, + created_at timestamp default now(), updated_at timestamp, primary key (id), diff --git a/migrations/2023-12-30-192105_tracks/up.sql b/migrations/2023-12-30-192105_tracks/up.sql index 3951c1e..ed9f00a 100644 --- a/migrations/2023-12-30-192105_tracks/up.sql +++ b/migrations/2023-12-30-192105_tracks/up.sql @@ -5,11 +5,11 @@ create table if not exists tracks title varchar(255) not null, duration_ms int not null default 0, - created_at timestamp default now() not null, + created_at timestamp default now(), updated_at timestamp, -- music services - spotify_id varchar(21) unique, + spotify_id varchar(22) unique, tidal_id varchar(10) unique, primary key (id) diff --git a/migrations/2023-12-31-104738_artists/up.sql b/migrations/2023-12-31-104738_artists/up.sql index 0deb8da..ef2ce81 100644 --- a/migrations/2023-12-31-104738_artists/up.sql +++ b/migrations/2023-12-31-104738_artists/up.sql @@ -3,13 +3,14 @@ create table if not exists artists ( id varchar(24) default nanoid(24), name varchar(255) NOT NULL, + slug varchar unique not null generated always as (lower(replace(name, ' ', '-'))) stored, - created_at TIMESTAMP DEFAULT now() NOT NULL, - updated_at TIMESTAMP, + created_at timestamp default now(), + updated_at timestamp, -- music services - spotify_id VARCHAR(21) UNIQUE, - tidal_id VARCHAR(10) UNIQUE, + spotify_id varchar(22) unique, + tidal_id varchar(10) unique, primary key (id) ); diff --git a/src/main.rs b/src/main.rs index 8705e46..abc0a34 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,21 +6,26 @@ mod models; mod routes; mod schema; mod services; +mod utils; use crate::helpers::db; -use actix_web::{web, App, HttpServer}; #[actix_web::main] async fn main() -> std::io::Result<()> { + use actix_web::{web, App, HttpServer}; + dotenvy::dotenv().expect("No .env file found"); std::env::set_var("RUST_LOG", "debug"); + // Register services. + let _ = services::spotify::instance().await; + db::init(); HttpServer::new(move || { App::new() .service(web::scope("/playlists").service(routes::playlists::get_playlist)) - .service(routes::auth::login) + .service(routes::auth::routes()) .service(routes::me::routes()) .service(routes::users::routes()) .service(routes::search::routes()) diff --git a/src/middlewares/error.rs b/src/middlewares/error.rs index e1bdcfa..6b060fa 100644 --- a/src/middlewares/error.rs +++ b/src/middlewares/error.rs @@ -9,6 +9,15 @@ pub struct ErrorResponse { pub status: StatusCode, } +impl ErrorResponse { + pub fn new(message: &str, status: StatusCode) -> Self { + ErrorResponse { + message: message.to_string(), + status, + } + } +} + impl Display for ErrorResponse { fn fmt(&self, f: &mut Formatter) -> Result { write!(f, "{}: {}", self.status, self.message) diff --git a/src/middlewares/user.rs b/src/middlewares/user.rs index d4b91d4..84c229f 100644 --- a/src/middlewares/user.rs +++ b/src/middlewares/user.rs @@ -19,23 +19,20 @@ pub fn get_user(req: HttpRequest) -> Result { } Err(e) => { return Err(match e.kind() { - jsonwebtoken::errors::ErrorKind::ExpiredSignature => ErrorResponse { - message: "Not Authorized".to_string(), - status: StatusCode::UNAUTHORIZED, - }, - _ => ErrorResponse { - message: e.to_string(), - status: StatusCode::INTERNAL_SERVER_ERROR, - }, + jsonwebtoken::errors::ErrorKind::ExpiredSignature => { + ErrorResponse::new("Not Authorized", StatusCode::UNAUTHORIZED) + } + _ => ErrorResponse::new( + e.to_string().as_str(), + StatusCode::INTERNAL_SERVER_ERROR, + ), }) } } } - None => { - return Err(ErrorResponse { - message: "Not Authorized".to_string(), - status: StatusCode::UNAUTHORIZED, - }); - } + None => Err(ErrorResponse::new( + "Not Authorized", + StatusCode::UNAUTHORIZED, + )), } } diff --git a/src/models/artists.rs b/src/models/artists.rs index 0e82a11..d931681 100644 --- a/src/models/artists.rs +++ b/src/models/artists.rs @@ -13,9 +13,10 @@ use serde::{Deserialize, Serialize}; pub struct Artist { pub id: String, pub name: String, + pub slug: String, - created_at: NaiveDateTime, - updated_at: Option, + pub created_at: Option, + pub updated_at: Option, pub spotify_id: Option, pub tidal_id: Option, @@ -25,9 +26,10 @@ pub struct Artist { pub struct Artists { pub id: String, pub title: String, + pub slug: String, - created_at: NaiveDateTime, - updated_at: Option, + pub created_at: Option, + pub updated_at: Option, pub spotify_id: Option, pub tidal_id: Option, @@ -68,6 +70,7 @@ impl Artist { Artist { id: artist.id, name: artist.name, + slug: artist.slug, created_at: artist.created_at, updated_at: artist.updated_at, diff --git a/src/models/playlists.rs b/src/models/playlists.rs index 5986d67..22a7982 100644 --- a/src/models/playlists.rs +++ b/src/models/playlists.rs @@ -1,5 +1,5 @@ use crate::helpers::db; -use crate::models::tracks::{Track, Tracks, TracksWithArtists}; +use crate::models::tracks::{Tracks, TracksWithArtists}; use crate::models::user::Users; use crate::schema::playlists; use chrono::NaiveDateTime; @@ -25,7 +25,7 @@ pub struct Playlist { pub public: bool, pub creator_id: String, - pub created_at: NaiveDateTime, + pub created_at: Option, pub updated_at: Option, } @@ -36,7 +36,7 @@ pub struct Playlists { pub public: bool, pub creator_id: String, - pub created_at: NaiveDateTime, + pub created_at: Option, pub updated_at: Option, } @@ -74,7 +74,7 @@ impl Playlists { let tracks = Tracks::find_by_playlist(&self.id)?; let tracks: Vec = tracks .into_iter() - .map(Track::with_artists) + .map(Tracks::with_artists) .collect::, _>>()?; Ok(tracks) diff --git a/src/models/spotify.rs b/src/models/spotify.rs index 0145ab1..98c5340 100644 --- a/src/models/spotify.rs +++ b/src/models/spotify.rs @@ -1,5 +1,42 @@ -pub struct Track {} +use serde::{Deserialize, Serialize}; -pub struct Artist {} +#[derive(Deserialize, Serialize)] +pub struct Track { + pub id: String, + pub name: String, + pub duration_ms: i32, + pub artists: Vec, + pub album: Album, -pub struct Album {} + pub external_ids: ExternalIds, +} + +#[derive(Deserialize, Serialize)] +pub struct ExternalIds { + pub isrc: String, +} + +#[derive(Deserialize, Serialize)] +pub struct Artist { + pub id: String, + pub name: String, +} + +#[derive(Deserialize, Serialize)] +pub struct Album { + pub id: String, + pub name: String, + pub artists: Vec, +} + +#[derive(Deserialize, Serialize)] +pub struct APISearchResponse { + pub albums: APISearchItems, + pub artists: APISearchItems, + pub tracks: APISearchItems, +} + +#[derive(Deserialize, Serialize)] +pub struct APISearchItems { + pub items: Vec, +} diff --git a/src/models/tracks.rs b/src/models/tracks.rs index a1bbdc5..aa4e0bd 100644 --- a/src/models/tracks.rs +++ b/src/models/tracks.rs @@ -1,5 +1,5 @@ use crate::helpers::db; -use crate::models::artists::Artists; +use crate::models::{artists::Artists, spotify}; use crate::schema::tracks; use chrono::NaiveDateTime; use diesel::result::Error; @@ -10,28 +10,15 @@ use serde::{Deserialize, Serialize}; use crate::schema::playlists_tracks; -#[derive(AsChangeset, Insertable, Queryable, Selectable, Deserialize, Serialize)] +#[derive(AsChangeset, Clone, Insertable, Queryable, Selectable, Deserialize, Serialize)] #[diesel(table_name = crate::schema::tracks)] #[diesel(check_for_backend(diesel::pg::Pg))] -pub struct Track { - pub id: String, - pub title: String, - pub duration_ms: i32, - - pub created_at: NaiveDateTime, - pub updated_at: Option, - - pub spotify_id: Option, - pub tidal_id: Option, -} - -#[derive(Debug, Deserialize, Queryable, Serialize)] pub struct Tracks { pub id: String, pub title: String, pub duration_ms: i32, - pub created_at: NaiveDateTime, + pub created_at: Option, pub updated_at: Option, pub spotify_id: Option, @@ -45,10 +32,10 @@ impl Tracks { Ok(playlist) } - pub fn create(track: Track) -> Result { + pub fn create(track: Tracks) -> Result { let conn = &mut db::connection()?; let playlist = diesel::insert_into(tracks::table) - .values(Track::from(track)) + .values(Tracks::from(track)) .get_result(conn)?; Ok(playlist) } @@ -61,24 +48,30 @@ impl Tracks { let tracks = tracks .into_iter() - .map(|(playlist_id, track_id)| { - println!("{}: {}", playlist_id, track_id); - Tracks::find(track_id).unwrap() - }) + .map(|(_, track_id)| Tracks::find(track_id).unwrap()) .collect::>(); Ok(tracks) } + pub fn create_or_update(track: Tracks) -> Result { + let conn = &mut db::connection()?; + let playlist = diesel::insert_into(tracks::table) + .values(track.clone()) + .on_conflict(tracks::id) + .do_update() + .set(track) + .get_result(conn)?; + Ok(playlist) + } + pub fn get_artists(&self) -> Result, Error> { let artists = Artists::find_by_track(&self.id)?; Ok(artists) } -} -impl Track { - fn from(track: Track) -> Track { - Track { + fn from(track: Tracks) -> Self { + Tracks { id: track.id, title: track.title, duration_ms: track.duration_ms, @@ -107,6 +100,20 @@ impl Track { } } +impl From for Tracks { + fn from(value: spotify::Track) -> Tracks { + Tracks { + id: value.external_ids.isrc, + title: value.name, + duration_ms: value.duration_ms, + spotify_id: Some(value.id), + tidal_id: None, + created_at: None, + updated_at: None, + } + } +} + #[derive(Deserialize, Serialize)] pub struct TracksWithArtists { pub id: String, diff --git a/src/models/user.rs b/src/models/user.rs index 7147ba8..99d242c 100644 --- a/src/models/user.rs +++ b/src/models/user.rs @@ -18,8 +18,8 @@ pub struct User { pub password: String, + pub created_at: Option, pub updated_at: Option, - pub created_at: NaiveDateTime, } #[derive(Debug, Deserialize, Queryable, Serialize)] @@ -30,8 +30,8 @@ pub struct Users { pub password: String, + pub created_at: Option, pub updated_at: Option, - pub created_at: NaiveDateTime, } impl Users { diff --git a/src/routes/auth.rs b/src/routes/auth.rs index 70c446a..f376a69 100644 --- a/src/routes/auth.rs +++ b/src/routes/auth.rs @@ -2,15 +2,19 @@ use crate::helpers::jwt::get_encoding_key; use crate::middlewares::error::ErrorResponse; use crate::models::user::Users; use actix_web::http::StatusCode; -use actix_web::{post, web, HttpResponse}; +use actix_web::{post, web, HttpResponse, Scope}; use chrono::{Days, Utc}; use jsonwebtoken::{encode, Header}; use serde::{Deserialize, Serialize}; +pub fn routes() -> Scope { + web::scope("/auth").service(login) +} + #[derive(Debug, Deserialize, Serialize)] pub struct JWTClaims { pub user_id: String, - pub exp: usize, + pub exp: i64, } #[post("/auth/login")] @@ -18,7 +22,7 @@ async fn login(body: web::Json) -> Result) -> Result { - return Err(ErrorResponse { - message: "Invalid credentials.".to_string(), - status: StatusCode::BAD_REQUEST, - }) - } + Err(_e) => Err(ErrorResponse::new( + "Invalid credentials", + StatusCode::BAD_REQUEST, + )), }, - Err(_err) => { - return Err(ErrorResponse { - message: "Invalid credentials.".to_string(), - status: StatusCode::BAD_REQUEST, - }) - } + Err(_err) => Err(ErrorResponse::new( + "Invalid credentials", + StatusCode::BAD_REQUEST, + )), } } diff --git a/src/routes/import.rs b/src/routes/import.rs new file mode 100644 index 0000000..51a04cb --- /dev/null +++ b/src/routes/import.rs @@ -0,0 +1,12 @@ +use actix_web::{get, web, HttpResponse, Result, Scope}; + +use crate::middlewares::error::ErrorResponse; + +pub fn routes() -> Scope { + web::scope("/import") +} + +#[get("/spotify/{playlist_id}")] +async fn import_spotify() -> Result { + Ok(HttpResponse::Ok().finish()) +} diff --git a/src/routes/mod.rs b/src/routes/mod.rs index f643e8b..1d7bac6 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -1,4 +1,5 @@ pub mod auth; +pub mod import; pub mod me; pub mod playlists; pub mod search; diff --git a/src/routes/search.rs b/src/routes/search.rs index 6528aed..f4f3b7c 100644 --- a/src/routes/search.rs +++ b/src/routes/search.rs @@ -1,5 +1,6 @@ -use crate::middlewares::error::ErrorResponse; -use crate::services::spotify; +use crate::models::spotify; +use crate::services::spotify as Spotify; +use crate::{middlewares::error::ErrorResponse, models::tracks::Tracks}; use actix_web::{get, web, HttpResponse, Result, Scope}; use serde::{Deserialize, Serialize}; @@ -14,13 +15,31 @@ struct SearchQuery { #[derive(Serialize)] struct SearchResponse { - track: (), + tracks: Vec, +} + +#[derive(Serialize)] +struct SearchTracks { + pub id: String, +} + +impl From for SearchTracks { + fn from(value: spotify::Track) -> SearchTracks { + SearchTracks { id: value.id } + } } #[get("")] async fn search(query: web::Query) -> Result { - let spotify = spotify::instance().await; - let track = spotify.search(&query.q).await; + let spotify = Spotify::instance().await; + let search = spotify.search(&query.q).await.unwrap(); - Ok(HttpResponse::Ok().json(SearchResponse { track })) + Ok(HttpResponse::Ok().json(SearchResponse { + tracks: search + .tracks + .items + .into_iter() + .map(SearchTracks::from) + .collect::>(), + })) } diff --git a/src/routes/users.rs b/src/routes/users.rs index e4bc288..85fa1d7 100644 --- a/src/routes/users.rs +++ b/src/routes/users.rs @@ -24,18 +24,11 @@ fn get_a_user(user_id: &str) -> Result { match user { Ok(user) => Ok(user), - Err(DBError::NotFound) => { - return Err(ErrorResponse { - message: "User not found".to_string(), - status: StatusCode::NOT_FOUND, - }) - } - _ => { - return Err(ErrorResponse { - message: "Unknown error".to_string(), - status: StatusCode::INTERNAL_SERVER_ERROR, - }) - } + Err(DBError::NotFound) => Err(ErrorResponse::new("User not found", StatusCode::NOT_FOUND)), + _ => Err(ErrorResponse::new( + "Unknown error", + StatusCode::INTERNAL_SERVER_ERROR, + )), } } diff --git a/src/schema.rs b/src/schema.rs index 3df305e..17fb37c 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -6,9 +6,10 @@ diesel::table! { id -> Varchar, #[max_length = 255] name -> Varchar, - created_at -> Timestamp, + slug -> Varchar, + created_at -> Nullable, updated_at -> Nullable, - #[max_length = 21] + #[max_length = 22] spotify_id -> Nullable, #[max_length = 10] tidal_id -> Nullable, @@ -33,7 +34,7 @@ diesel::table! { public -> Bool, #[max_length = 24] creator_id -> Varchar, - created_at -> Timestamp, + created_at -> Nullable, updated_at -> Nullable, } } @@ -54,9 +55,9 @@ diesel::table! { #[max_length = 255] title -> Varchar, duration_ms -> Int4, - created_at -> Timestamp, + created_at -> Nullable, updated_at -> Nullable, - #[max_length = 21] + #[max_length = 22] spotify_id -> Nullable, #[max_length = 10] tidal_id -> Nullable, @@ -73,7 +74,7 @@ diesel::table! { email -> Varchar, password -> Text, updated_at -> Nullable, - created_at -> Timestamp, + created_at -> Nullable, } } diff --git a/src/services/service.rs b/src/services/service.rs index 9f6807b..24f7956 100644 --- a/src/services/service.rs +++ b/src/services/service.rs @@ -4,6 +4,8 @@ use async_trait::async_trait; pub trait Service: Send + Sync { fn get_token(&self) -> &String; - async fn fetch_token(&mut self) -> &String; + async fn fetch_token(&mut self); // async fn get_track_by_isrc(&self, isrc: &str); + + fn is_expired(&self) -> bool; } diff --git a/src/services/spotify.rs b/src/services/spotify.rs index 0cfd332..251737b 100644 --- a/src/services/spotify.rs +++ b/src/services/spotify.rs @@ -1,37 +1,50 @@ -use crate::services::service::Service; +use crate::{models::spotify::APISearchResponse, services::service::Service, utils::get_env}; use actix_web::http::Method; use async_trait::async_trait; +use chrono::{Duration, Utc}; +use lazy_static::lazy_static; use reqwest::{Client, RequestBuilder}; use serde::Deserialize; +use std::sync::Mutex; #[derive(Deserialize)] struct SpotifyResponse { access_token: String, + expires_in: i64, } #[derive(Clone)] pub struct Spotify { token: String, + expires_at: i64, } -pub async fn instance<'a>() -> Spotify { - Spotify::new().await +lazy_static! { + static ref SPOTIFY_INSTANCE: Mutex = Mutex::new(Spotify::new()); +} + +pub async fn instance() -> Spotify { + let mut spotify = SPOTIFY_INSTANCE.lock().unwrap().clone(); + + // Fetch token if expired + spotify.fetch_token().await; + + spotify } impl Spotify { - pub async fn new() -> Spotify { - let mut spotify = Spotify { + pub fn new() -> Spotify { + Spotify { token: String::new(), - }; - spotify.fetch_token().await; - spotify + expires_at: 0, + } } fn api(&self, url: &str, method: Method) -> RequestBuilder { let client = Client::new(); client - .request(method, &format!("https://api.spotify.com/v1{url}")) + .request(method, format!("https://api.spotify.com/v1{url}")) .bearer_auth(self.get_token()) } @@ -39,11 +52,33 @@ impl Spotify { // let spotify = &SPOTIFY; // } - pub async fn get_artist(self, spotify_id: &str) {} + // pub async fn get_artist(self, spotify_id: &str) {} - pub async fn get_track_by_isrc(self, isrc: &str) {} + // pub async fn get_track_by_isrc(self, isrc: &str) {} - pub async fn search(self, search: &str) {} + pub async fn search(&self, search: &str) -> Result { + let response = self + .api("/search", Method::GET) + .query(&[("q", search), ("type", "album,artist,track")]) + .send() + .await; + + match response { + Ok(res) => { + let data = res.json::().await; + + match data { + Ok(data) => Ok(data), + Err(_) => { + panic!("Invalid response (Spotify Search)") + } + } + } + Err(_) => { + panic!("Invalid response (Spotify Search)") + } + } + } } #[async_trait] @@ -52,7 +87,15 @@ impl Service for Spotify { &self.token } - async fn fetch_token(&mut self) -> &String { + fn is_expired(&self) -> bool { + self.expires_at < chrono::Utc::now().timestamp() + } + + async fn fetch_token(&mut self) { + if !self.is_expired() { + return; + } + let response = Client::new() .post("https://accounts.spotify.com/api/token") .basic_auth( @@ -63,12 +106,22 @@ impl Service for Spotify { .send() .await; + println!("Hello"); + match response { Ok(res) => { - let data: Result = res.json().await; + let data = res.json::().await; - self.token = data.unwrap().access_token.into(); - &self.token + match data { + Ok(data) => { + self.token = data.access_token; + self.expires_at = + (Utc::now() + Duration::seconds(data.expires_in)).timestamp(); + } + Err(_) => { + panic!("Invalid response (Spotify Token)") + } + } } Err(_) => { panic!("Invalid response (Spotify Token)") @@ -76,7 +129,3 @@ impl Service for Spotify { } } } - -fn get_env(key: &str) -> String { - std::env::var(key).unwrap() -} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..c68c3e4 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,3 @@ +pub fn get_env(key: &str) -> String { + std::env::var(key).unwrap() +} From 292ff60720c1f370d300ce1dfb9d344c0c3d296b Mon Sep 17 00:00:00 2001 From: Miguel da Mota Date: Wed, 3 Jan 2024 00:10:33 +0100 Subject: [PATCH 3/4] [API-1] chore: little updates --- Cargo.lock | 132 +++++++++++++++++++++++++++++++--------- Cargo.toml | 2 + src/helpers/db.rs | 1 + src/helpers/jwt.rs | 2 +- src/routes/playlists.rs | 8 ++- src/routes/search.rs | 11 +++- src/services/spotify.rs | 2 - 7 files changed, 121 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d994919..79e1fbf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -65,7 +65,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", - "syn 2.0.44", + "syn 2.0.46", ] [[package]] @@ -179,7 +179,7 @@ dependencies = [ "actix-router", "proc-macro2", "quote", - "syn 2.0.44", + "syn 2.0.46", ] [[package]] @@ -251,13 +251,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.76" +version = "0.1.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "531b97fb4cd3dfdce92c35dedbfdc1f0b9d8091c8ca943d6dae340ef5012d514" +checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.44", + "syn 2.0.46", ] [[package]] @@ -527,7 +527,7 @@ dependencies = [ "diesel_table_macro_syntax", "proc-macro2", "quote", - "syn 2.0.44", + "syn 2.0.46", ] [[package]] @@ -547,7 +547,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5" dependencies = [ - "syn 2.0.44", + "syn 2.0.46", ] [[package]] @@ -576,6 +576,19 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "env_logger" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95b3f3e67048839cb0d0781f445682a35113da7121f7c949db0e2be96a4fbece" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -789,6 +802,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "hyper" version = "0.14.28" @@ -884,6 +903,17 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +[[package]] +name = "is-terminal" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bad00257d07be169d870ab665980b06cdb366d792ad690bf2e76876dc503455" +dependencies = [ + "hermit-abi", + "rustix", + "windows-sys 0.52.0", +] + [[package]] name = "itoa" version = "1.0.10" @@ -1130,7 +1160,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.44", + "syn 2.0.46", ] [[package]] @@ -1237,18 +1267,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.73" +version = "1.0.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dd5e8a1f1029c43224ad5898e50140c2aebb1705f19e67c918ebf5b9e797fe1" +checksum = "2de98502f212cfcea8d0bb305bd0f49d7ebdd75b64ba0a68f937d888f4e0d6db" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.34" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22a37c9326af5ed140c86a46655b5278de879853be5573c01df185b6f49a580a" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -1473,29 +1503,29 @@ checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" [[package]] name = "serde" -version = "1.0.193" +version = "1.0.194" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +checksum = "0b114498256798c94a0689e1a15fec6005dee8ac1f41de56404b67afc2a4b773" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.193" +version = "1.0.194" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +checksum = "a3385e45322e8f9931410f01b3031ec534c3947d0e94c18049af4d9f9907d4e0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.44", + "syn 2.0.46", ] [[package]] name = "serde_json" -version = "1.0.109" +version = "1.0.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb0652c533506ad7a2e353cce269330d6afd8bdfb6d75e0ace5b35aacbd7b9e9" +checksum = "6fbd975230bada99c8bb618e0c365c2eefa219158d5c6c29610fd09ff1833257" dependencies = [ "itoa", "ryu", @@ -1616,9 +1646,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.44" +version = "2.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d27c2c202598d05175a6dd3af46824b7f747f8d8e9b14c623f19fa5069735d" +checksum = "89456b690ff72fddcecf231caedbe615c59480c93358a93dfae7fc29e3ebbf0e" dependencies = [ "proc-macro2", "quote", @@ -1660,23 +1690,32 @@ dependencies = [ ] [[package]] -name = "thiserror" -version = "1.0.53" +name = "termcolor" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2cd5904763bad08ad5513ddbb12cf2ae273ca53fa9f68e843e236ec6dfccc09" +checksum = "ff1bc3d3f05aff0403e8ac0d92ced918ec05b666a43f83297ccef5bea8a3d449" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.53" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dcf4a824cce0aeacd6f38ae6f24234c8e80d68632338ebaa1443b5df9e29e19" +checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" dependencies = [ "proc-macro2", "quote", - "syn 2.0.44", + "syn 2.0.46", ] [[package]] @@ -1899,9 +1938,11 @@ dependencies = [ "diesel", "diesel_migrations", "dotenvy", + "env_logger", "hmac", "jsonwebtoken", "lazy_static", + "log", "reqwest", "serde", "serde_json", @@ -1944,7 +1985,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.44", + "syn 2.0.46", "wasm-bindgen-shared", ] @@ -1978,7 +2019,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", - "syn 2.0.44", + "syn 2.0.46", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -1999,6 +2040,37 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-core" version = "0.52.0" @@ -2176,7 +2248,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.44", + "syn 2.0.46", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 6c1cd8e..bb3a36a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,9 @@ chrono = { version = "0.4", features = ["serde"] } diesel = { version = "2", features = ["r2d2", "postgres", "chrono"] } diesel_migrations = "2" dotenvy = "*" +env_logger = "0.10" hmac = "0.12" +log = "0.4" reqwest = { version = "0.11", features = ["blocking", "json"] } sha2 = "0.10" lazy_static = "1" diff --git a/src/helpers/db.rs b/src/helpers/db.rs index c6f224e..9d03a1a 100644 --- a/src/helpers/db.rs +++ b/src/helpers/db.rs @@ -16,6 +16,7 @@ lazy_static! { } pub fn init() { + log::info!("Initializing db pool."); lazy_static::initialize(&POOL); let _conn = connection().expect("Failed to get db connection."); } diff --git a/src/helpers/jwt.rs b/src/helpers/jwt.rs index 96ac6d6..b50e2cf 100644 --- a/src/helpers/jwt.rs +++ b/src/helpers/jwt.rs @@ -5,7 +5,7 @@ use jsonwebtoken::{decode, DecodingKey, EncodingKey, TokenData, Validation}; const JWT_SECRET: &str = "secret"; pub fn get_token(token: &str) -> Result, Error> { - let token = decode::(&token, &get_decoding_key(), &Validation::default())?; + let token = decode::(token, &get_decoding_key(), &Validation::default())?; Ok(token) } diff --git a/src/routes/playlists.rs b/src/routes/playlists.rs index 5a12319..d49aed9 100644 --- a/src/routes/playlists.rs +++ b/src/routes/playlists.rs @@ -1,9 +1,13 @@ use crate::middlewares::error::ErrorResponse; use crate::models::playlists::{PlaylistCreator, Playlists}; use crate::models::tracks::TracksWithArtists; -use actix_web::{get, web, HttpResponse}; +use actix_web::{get, web, HttpResponse, Scope}; use serde::{Deserialize, Serialize}; +pub fn routes() -> Scope { + web::scope("/playlists").service(get_playlist) +} + #[derive(Deserialize, Serialize)] struct GetPlaylistResponse { pub id: String, @@ -15,7 +19,7 @@ struct GetPlaylistResponse { } #[get("/{playlist_id}")] -pub async fn get_playlist(path: web::Path) -> Result { +async fn get_playlist(path: web::Path) -> Result { let playlist_id = path.into_inner(); let playlist = Playlists::find(playlist_id.as_str())?; diff --git a/src/routes/search.rs b/src/routes/search.rs index f4f3b7c..718a3a4 100644 --- a/src/routes/search.rs +++ b/src/routes/search.rs @@ -1,6 +1,6 @@ +use crate::middlewares::error::ErrorResponse; 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}; @@ -21,11 +21,18 @@ struct SearchResponse { #[derive(Serialize)] struct SearchTracks { pub id: String, + pub title: String, + // pub artists: String, + pub duration_ms: i32, } impl From for SearchTracks { fn from(value: spotify::Track) -> SearchTracks { - SearchTracks { id: value.id } + SearchTracks { + id: value.id, + title: value.name, + duration_ms: value.duration_ms, + } } } diff --git a/src/services/spotify.rs b/src/services/spotify.rs index 251737b..2443d4f 100644 --- a/src/services/spotify.rs +++ b/src/services/spotify.rs @@ -106,8 +106,6 @@ impl Service for Spotify { .send() .await; - println!("Hello"); - match response { Ok(res) => { let data = res.json::().await; From fa8ed2b5992396a92278775f75bc3c303b490850 Mon Sep 17 00:00:00 2001 From: Miguel da Mota Date: Sun, 7 Jan 2024 15:43:56 +0100 Subject: [PATCH 4/4] updates --- Cargo.lock | 59 ++++---- Cargo.toml | 1 + .../up.sql | 2 +- migrations/2023-12-30-162032_playlists/up.sql | 4 + .../2023-12-30-214938_playlists_tracks/up.sql | 1 + src/main.rs | 8 +- src/middlewares/user.rs | 11 +- src/models/artists.rs | 46 ++---- src/models/artists_tracks.rs | 52 +++++++ src/models/mod.rs | 4 +- src/models/playlists.rs | 136 +++++++++++------- src/models/playlists_tracks.rs | 38 +++++ src/models/tracks.rs | 60 +++----- src/models/{user.rs => users.rs} | 33 ----- src/routes/auth.rs | 4 +- src/routes/me.rs | 37 +++-- src/routes/playlists.rs | 34 +++-- src/routes/users.rs | 30 +++- src/schema.rs | 5 + 19 files changed, 349 insertions(+), 216 deletions(-) create mode 100644 src/models/artists_tracks.rs create mode 100644 src/models/playlists_tracks.rs rename src/models/{user.rs => users.rs} (60%) diff --git a/Cargo.lock b/Cargo.lock index 79e1fbf..d37a210 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -65,7 +65,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -179,7 +179,7 @@ dependencies = [ "actix-router", "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -257,7 +257,7 @@ checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -455,9 +455,9 @@ checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "cpufeatures" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] @@ -527,7 +527,7 @@ dependencies = [ "diesel_table_macro_syntax", "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -547,7 +547,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5" dependencies = [ - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -967,9 +967,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.151" +version = "0.2.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" +checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" [[package]] name = "linux-raw-sys" @@ -1160,7 +1160,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -1267,9 +1267,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.74" +version = "1.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2de98502f212cfcea8d0bb305bd0f49d7ebdd75b64ba0a68f937d888f4e0d6db" +checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" dependencies = [ "unicode-ident", ] @@ -1497,35 +1497,35 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" +checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0" [[package]] name = "serde" -version = "1.0.194" +version = "1.0.195" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b114498256798c94a0689e1a15fec6005dee8ac1f41de56404b67afc2a4b773" +checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.194" +version = "1.0.195" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3385e45322e8f9931410f01b3031ec534c3947d0e94c18049af4d9f9907d4e0" +checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] name = "serde_json" -version = "1.0.110" +version = "1.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fbd975230bada99c8bb618e0c365c2eefa219158d5c6c29610fd09ff1833257" +checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" dependencies = [ "itoa", "ryu", @@ -1646,9 +1646,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.46" +version = "2.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89456b690ff72fddcecf231caedbe615c59480c93358a93dfae7fc29e3ebbf0e" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" dependencies = [ "proc-macro2", "quote", @@ -1715,7 +1715,7 @@ checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" dependencies = [ "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -1931,6 +1931,7 @@ name = "vybr-api" version = "0.0.1" dependencies = [ "actix-rt", + "actix-service", "actix-web", "async-trait", "bcrypt", @@ -1985,7 +1986,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", "wasm-bindgen-shared", ] @@ -2019,7 +2020,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2214,9 +2215,9 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] name = "winnow" -version = "0.5.31" +version = "0.5.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a4882e6b134d6c28953a387571f1acdd3496830d5e36c5e3a1075580ea641c" +checksum = "b7520bbdec7211caa7c4e682eb1fbe07abe20cee6756b6e00f537c82c11816aa" dependencies = [ "memchr", ] @@ -2248,7 +2249,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index bb3a36a..920e9a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,3 +23,4 @@ lazy_static = "1" jsonwebtoken = "9" serde = "1" serde_json = "1" +actix-service = "2.0.2" diff --git a/migrations/00000000000000_diesel_initial_setup/up.sql b/migrations/00000000000000_diesel_initial_setup/up.sql index 7e95698..fa90f26 100644 --- a/migrations/00000000000000_diesel_initial_setup/up.sql +++ b/migrations/00000000000000_diesel_initial_setup/up.sql @@ -45,7 +45,7 @@ CREATE EXTENSION IF NOT EXISTS pgcrypto; -- 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. + size int DEFAULT 24, -- 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. ) diff --git a/migrations/2023-12-30-162032_playlists/up.sql b/migrations/2023-12-30-162032_playlists/up.sql index 6f496d7..3adc732 100644 --- a/migrations/2023-12-30-162032_playlists/up.sql +++ b/migrations/2023-12-30-162032_playlists/up.sql @@ -1,10 +1,14 @@ +create type playlist_type as enum ('playlist', 'folder'); + create table if not exists playlists ( id varchar(24) default nanoid(24), name varchar(255) not null, public bool default false not null, + type playlist_type default 'playlist' not null, creator_id varchar(24) not null, + parent_id varchar(24) references playlists (id) on delete set null, created_at timestamp default now(), updated_at timestamp, diff --git a/migrations/2023-12-30-214938_playlists_tracks/up.sql b/migrations/2023-12-30-214938_playlists_tracks/up.sql index 927a236..bfe4f82 100644 --- a/migrations/2023-12-30-214938_playlists_tracks/up.sql +++ b/migrations/2023-12-30-214938_playlists_tracks/up.sql @@ -2,5 +2,6 @@ create table if not exists playlists_tracks ( playlist_id varchar(24) references playlists (id) on delete cascade, track_id varchar(24) references tracks (id) on delete cascade, + added_at timestamp default now() not null, primary key (playlist_id, track_id) ); diff --git a/src/main.rs b/src/main.rs index abc0a34..d3f1dc8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,10 +12,10 @@ use crate::helpers::db; #[actix_web::main] async fn main() -> std::io::Result<()> { - use actix_web::{web, App, HttpServer}; + use actix_web::{App, HttpServer}; dotenvy::dotenv().expect("No .env file found"); - std::env::set_var("RUST_LOG", "debug"); + env_logger::init(); // Register services. let _ = services::spotify::instance().await; @@ -24,9 +24,11 @@ async fn main() -> std::io::Result<()> { HttpServer::new(move || { App::new() - .service(web::scope("/playlists").service(routes::playlists::get_playlist)) .service(routes::auth::routes()) + // .wrap(middlewares::auth::auth()) + .service(routes::playlists::routes()) .service(routes::me::routes()) + .service(routes::import::routes()) .service(routes::users::routes()) .service(routes::search::routes()) }) diff --git a/src/middlewares/user.rs b/src/middlewares/user.rs index 84c229f..5aafd9f 100644 --- a/src/middlewares/user.rs +++ b/src/middlewares/user.rs @@ -1,6 +1,6 @@ use crate::helpers::jwt::get_token; use crate::middlewares::error::ErrorResponse; -use crate::models::user::Users; +use crate::models::users::Users; use actix_web::http::{header, StatusCode}; use actix_web::HttpRequest; @@ -36,3 +36,12 @@ pub fn get_user(req: HttpRequest) -> Result { )), } } + +pub fn get_user_option(req: HttpRequest) -> Option { + let user = get_user(req); + + match user { + Ok(user) => Some(user), + Err(_) => None, + } +} diff --git a/src/models/artists.rs b/src/models/artists.rs index d931681..6605cb6 100644 --- a/src/models/artists.rs +++ b/src/models/artists.rs @@ -1,16 +1,13 @@ use crate::helpers::db; use crate::schema::{artists, artists_tracks}; use chrono::NaiveDateTime; +use diesel::prelude::*; use diesel::result::Error; -use diesel::{ - AsChangeset, ExpressionMethods, Insertable, QueryDsl, Queryable, RunQueryDsl, Selectable, -}; use serde::{Deserialize, Serialize}; -#[derive(AsChangeset, Insertable, Queryable, Selectable, Deserialize, Serialize)] -#[diesel(table_name = crate::schema::artists)] -#[diesel(check_for_backend(diesel::pg::Pg))] -pub struct Artist { +#[derive(AsChangeset, Identifiable, Insertable, Queryable, Selectable, Deserialize, Serialize)] +#[diesel(table_name = artists)] +pub struct Artists { pub id: String, pub name: String, pub slug: String, @@ -22,14 +19,11 @@ pub struct Artist { pub tidal_id: Option, } -#[derive(Debug, Deserialize, Queryable, Serialize)] -pub struct Artists { +#[derive(Debug, Insertable, Deserialize, Serialize)] +#[diesel(table_name = artists)] +pub struct NewArtist { pub id: String, - pub title: String, - pub slug: String, - - pub created_at: Option, - pub updated_at: Option, + pub name: String, pub spotify_id: Option, pub tidal_id: Option, @@ -38,14 +32,14 @@ pub struct Artists { impl Artists { pub fn find(id: String) -> Result { let conn = &mut db::connection()?; - let playlist = artists::table.filter(artists::id.eq(id)).first(conn)?; - Ok(playlist) + let artist = artists::table.filter(artists::id.eq(id)).first(conn)?; + Ok(artist) } - pub fn create(artist: Artist) -> Result { + pub fn create(artist: NewArtist) -> Result { let conn = &mut db::connection()?; let playlist = diesel::insert_into(artists::table) - .values(Artist::from(artist)) + .values(artist) .get_result(conn)?; Ok(playlist) } @@ -64,19 +58,3 @@ impl Artists { Ok(artists) } } - -impl Artist { - fn from(artist: Artist) -> Artist { - Artist { - id: artist.id, - name: artist.name, - slug: artist.slug, - - created_at: artist.created_at, - updated_at: artist.updated_at, - - spotify_id: artist.spotify_id, - tidal_id: artist.tidal_id, - } - } -} diff --git a/src/models/artists_tracks.rs b/src/models/artists_tracks.rs new file mode 100644 index 0000000..9fb94c7 --- /dev/null +++ b/src/models/artists_tracks.rs @@ -0,0 +1,52 @@ +use diesel::prelude::*; +use diesel::result::Error; +use serde::{Deserialize, Serialize}; + +use crate::helpers::db; + +use super::artists::Artists; +use super::tracks::Tracks; + +use crate::schema::{artists, artists_tracks, tracks}; + +#[derive(Identifiable, Selectable, Queryable, Associations, Debug)] +#[diesel(belongs_to(Artists, foreign_key = artist_id))] +#[diesel(belongs_to(Tracks, foreign_key = track_id))] +#[diesel(table_name = artists_tracks)] +#[diesel(primary_key(artist_id, track_id))] +pub struct ArtistTracks { + pub artist_id: String, + pub track_id: String, +} + +impl ArtistTracks { + pub fn get_tracks(artist: &Artists) -> Result, Error> { + let conn = &mut db::connection()?; + let track_ids = ArtistTracks::belonging_to(artist).select(artists_tracks::track_id); + + let tracks: Vec = tracks::table + .filter(tracks::id.eq_any(track_ids)) + .load::(conn)?; + + Ok(tracks) + } + + pub fn get_artists(track: &Tracks) -> Result, Error> { + let conn = &mut db::connection()?; + let artist_ids = ArtistTracks::belonging_to(track).select(artists_tracks::artist_id); + + let artists = artists::table + .select((artists::id, artists::name)) + .filter(artists::id.eq_any(artist_ids)) + .load::(conn)?; + + Ok(artists) + } +} + +#[derive(Debug, Deserialize, Serialize, Queryable)] +#[diesel(table_name = artists)] +pub struct TrackArtist { + pub id: String, + pub name: String, +} diff --git a/src/models/mod.rs b/src/models/mod.rs index 62087e1..eadd19f 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,5 +1,7 @@ pub mod artists; +pub mod artists_tracks; pub mod playlists; +pub mod playlists_tracks; pub mod spotify; pub mod tracks; -pub mod user; +pub mod users; diff --git a/src/models/playlists.rs b/src/models/playlists.rs index 22a7982..bdb05af 100644 --- a/src/models/playlists.rs +++ b/src/models/playlists.rs @@ -1,43 +1,43 @@ use crate::helpers::db; -use crate::models::tracks::{Tracks, TracksWithArtists}; -use crate::models::user::Users; -use crate::schema::playlists; +use crate::schema::{playlists, playlists_tracks}; use chrono::NaiveDateTime; -use diesel::result::Error; -use diesel::{ - AsChangeset, ExpressionMethods, Insertable, QueryDsl, Queryable, RunQueryDsl, Selectable, -}; +use diesel::{prelude::*, result::Error}; use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize)] +use super::artists_tracks::{ArtistTracks, TrackArtist}; +use super::{playlists_tracks::PlaylistTracks, tracks::Tracks, users::Users}; + +#[derive(Debug, Deserialize, Serialize)] pub struct PlaylistCreator { pub id: String, pub name: String, } -#[derive(AsChangeset, Insertable, Queryable, Selectable, Deserialize, Serialize)] -#[diesel(table_name = crate::schema::playlists)] -#[diesel(belongs_to(Users))] -#[diesel(check_for_backend(diesel::pg::Pg))] -pub struct Playlist { +#[derive( + AsChangeset, Debug, Deserialize, Identifiable, Queryable, Selectable, Serialize, PartialEq, +)] +#[diesel(table_name = playlists)] +pub struct Playlists { pub id: String, pub name: String, pub public: bool, + + pub playlist_type: String, + pub parent_id: Option, + pub creator_id: String, pub created_at: Option, pub updated_at: Option, } -#[derive(Debug, Deserialize, Queryable, Serialize)] -pub struct Playlists { - pub id: String, +#[derive(Debug, Deserialize, Insertable)] +#[diesel(table_name = playlists)] +pub struct NewPlaylist { pub name: String, pub public: bool, + // pub playlist_type: String, pub creator_id: String, - - pub created_at: Option, - pub updated_at: Option, } impl Playlists { @@ -59,23 +59,26 @@ impl Playlists { let conn = &mut db::connection()?; let mut playlists = playlists::table + .distinct() + .select(Playlists::as_select()) + .order((playlists::playlist_type.desc(), playlists::parent_id.desc())) .filter(playlists::creator_id.eq(user_id)) .into_boxed(); if filter_public { - playlists = playlists.filter(playlists::public.eq(true)); + playlists = playlists.filter( + playlists::public + .eq(true) + .and(playlists::playlist_type.ne("folder")), + ); } let playlists = playlists.get_results(conn)?; Ok(playlists) } - pub fn get_tracks(&self) -> Result, Error> { - let tracks = Tracks::find_by_playlist(&self.id)?; - let tracks: Vec = tracks - .into_iter() - .map(Tracks::with_artists) - .collect::, _>>()?; + pub fn get_tracks(&self) -> Result, Error> { + let tracks: Vec = PlaylistTracks::get_tracks(self)?; Ok(tracks) } @@ -84,34 +87,69 @@ impl Playlists { let creator = Users::find(&self.creator_id)?; Ok(creator) } -} -#[derive(Debug, Insertable, Deserialize)] -#[diesel(table_name = playlists)] -pub struct NewPlaylist { - pub name: String, - pub public: bool, - pub creator_id: Option, -} - -impl Playlist { - pub fn create(name: &str, public: bool, creator_id: &str) -> NewPlaylist { - NewPlaylist { - name: name.to_string(), - public, - creator_id: Some(creator_id.to_string()), + pub fn get_data(&self, tracks: &[PlaylistTrack]) -> (usize, usize) { + if tracks.is_empty() { + return (0, 0); } + + let duration = tracks + .iter() + .map(|track| track.duration_ms) + .reduce(|a, b| a + b) + .unwrap() as usize; + + (duration, tracks.len()) } - fn from(playlist: Playlist) -> Playlist { - Playlist { - id: playlist.id, - name: playlist.name, - public: playlist.public, - creator_id: playlist.creator_id, + pub fn can_see(&self, creator: Option) -> bool { + self.public || creator.map_or(false, |user| user.id == self.creator_id) + } +} - created_at: playlist.created_at, - updated_at: playlist.updated_at, +// // Folder +// struct Folder { +// id: String, +// name: String, +// playlists: Vec, +// } + +// impl Folder { +// pub fn get_playlists(&self) -> Result, Error> { +// let playlists = playlists::table +// } +// } + +#[derive(Deserialize, Serialize)] +pub struct PlaylistTrack { + pub id: String, + pub title: String, + pub duration_ms: i32, + + pub artists: Vec, + pub added_at: NaiveDateTime, + pub spotify_id: Option, + pub tidal_id: Option, +} + +impl From for PlaylistTrack { + fn from(track: Tracks) -> Self { + let artists = ArtistTracks::get_artists(&track).unwrap(); + + let added_at = playlists_tracks::table + .filter(playlists_tracks::track_id.eq(&track.id)) + .select(playlists_tracks::added_at) + .first::(&mut db::connection().unwrap()) + .unwrap(); + + PlaylistTrack { + id: track.id, + title: track.title, + duration_ms: track.duration_ms, + artists, + added_at, + spotify_id: track.spotify_id, + tidal_id: track.tidal_id, } } } diff --git a/src/models/playlists_tracks.rs b/src/models/playlists_tracks.rs new file mode 100644 index 0000000..bc98b23 --- /dev/null +++ b/src/models/playlists_tracks.rs @@ -0,0 +1,38 @@ +use diesel::prelude::*; +use diesel::result::Error; + +use crate::helpers::db; + +use super::playlists::{PlaylistTrack, Playlists}; +use super::tracks::Tracks; + +use crate::schema::{playlists_tracks, tracks}; + +#[derive(Identifiable, Selectable, Queryable, Associations, Debug)] +#[diesel(belongs_to(Playlists, foreign_key = playlist_id))] +#[diesel(belongs_to(Tracks, foreign_key = track_id))] +#[diesel(table_name = playlists_tracks)] +#[diesel(primary_key(playlist_id, track_id))] +pub struct PlaylistTracks { + pub playlist_id: String, + pub track_id: String, + pub added_at: String, +} + +impl PlaylistTracks { + pub fn get_tracks(playlist: &Playlists) -> Result, Error> { + let conn = &mut db::connection()?; + let track_ids = PlaylistTracks::belonging_to(playlist).select(playlists_tracks::track_id); + + let tracks: Vec = tracks::table + .filter(tracks::id.eq_any(track_ids)) + .load::(conn)?; + + let tracks = tracks + .into_iter() + .map(PlaylistTrack::from) + .collect::>(); + + Ok(tracks) + } +} diff --git a/src/models/tracks.rs b/src/models/tracks.rs index aa4e0bd..21507c3 100644 --- a/src/models/tracks.rs +++ b/src/models/tracks.rs @@ -1,18 +1,15 @@ use crate::helpers::db; use crate::models::{artists::Artists, spotify}; -use crate::schema::tracks; +use crate::schema::{playlists_tracks, tracks}; use chrono::NaiveDateTime; +use diesel::prelude::*; use diesel::result::Error; -use diesel::{ - AsChangeset, ExpressionMethods, Insertable, QueryDsl, Queryable, RunQueryDsl, Selectable, -}; use serde::{Deserialize, Serialize}; -use crate::schema::playlists_tracks; - -#[derive(AsChangeset, Clone, Insertable, Queryable, Selectable, Deserialize, Serialize)] -#[diesel(table_name = crate::schema::tracks)] -#[diesel(check_for_backend(diesel::pg::Pg))] +#[derive( + AsChangeset, Debug, Deserialize, Serialize, Queryable, Identifiable, Selectable, Clone, +)] +#[diesel(table_name = tracks)] pub struct Tracks { pub id: String, pub title: String, @@ -25,6 +22,17 @@ pub struct Tracks { pub tidal_id: Option, } +#[derive(AsChangeset, Clone, Debug, Deserialize, Insertable)] +#[diesel(table_name = tracks)] +pub struct NewTrack { + pub id: String, + pub title: String, + pub duration_ms: i32, + + pub spotify_id: Option, + pub tidal_id: Option, +} + impl Tracks { pub fn find(id: String) -> Result { let conn = &mut db::connection()?; @@ -32,29 +40,15 @@ impl Tracks { Ok(playlist) } - pub fn create(track: Tracks) -> Result { + pub fn create(track: NewTrack) -> Result { let conn = &mut db::connection()?; let playlist = diesel::insert_into(tracks::table) - .values(Tracks::from(track)) + .values(track) .get_result(conn)?; Ok(playlist) } - pub fn find_by_playlist(playlist_id: &str) -> Result, Error> { - let conn = &mut db::connection()?; - let tracks: Vec<(String, String)> = playlists_tracks::table - .filter(playlists_tracks::playlist_id.eq(playlist_id)) - .get_results::<(String, String)>(conn)?; - - let tracks = tracks - .into_iter() - .map(|(_, track_id)| Tracks::find(track_id).unwrap()) - .collect::>(); - - Ok(tracks) - } - - pub fn create_or_update(track: Tracks) -> Result { + pub fn create_or_update(track: NewTrack) -> Result { let conn = &mut db::connection()?; let playlist = diesel::insert_into(tracks::table) .values(track.clone()) @@ -70,20 +64,6 @@ impl Tracks { Ok(artists) } - fn from(track: Tracks) -> Self { - Tracks { - id: track.id, - title: track.title, - duration_ms: track.duration_ms, - - created_at: track.created_at, - updated_at: track.updated_at, - - spotify_id: track.spotify_id, - tidal_id: track.tidal_id, - } - } - pub fn with_artists(track: Tracks) -> Result { let artists = track.get_artists()?; diff --git a/src/models/user.rs b/src/models/users.rs similarity index 60% rename from src/models/user.rs rename to src/models/users.rs index 99d242c..33c1427 100644 --- a/src/models/user.rs +++ b/src/models/users.rs @@ -11,18 +11,6 @@ use serde::{Deserialize, Serialize}; #[derive(AsChangeset, Insertable, Queryable, Selectable, Deserialize, Serialize)] #[diesel(table_name = crate::schema::users)] #[diesel(check_for_backend(diesel::pg::Pg))] -pub struct User { - pub id: String, - pub name: String, - pub email: String, - - pub password: String, - - pub created_at: Option, - pub updated_at: Option, -} - -#[derive(Debug, Deserialize, Queryable, Serialize)] pub struct Users { pub id: String, pub name: String, @@ -41,14 +29,6 @@ impl Users { Ok(user) } - pub fn create(user: User) -> Result { - let conn = &mut db::connection()?; - let user = diesel::insert_into(users::table) - .values(User::from(user)) - .get_result(conn)?; - Ok(user) - } - pub fn find_by_email(email: &str) -> Result { let conn = &mut db::connection()?; let user = users::table.filter(users::email.eq(email)).first(conn)?; @@ -59,16 +39,3 @@ impl Users { bcrypt::verify(password, hash) } } - -impl User { - fn from(user: User) -> User { - User { - id: user.id, - name: user.name, - email: user.email, - password: user.password, - created_at: user.created_at, - updated_at: user.updated_at, - } - } -} diff --git a/src/routes/auth.rs b/src/routes/auth.rs index f376a69..00d0739 100644 --- a/src/routes/auth.rs +++ b/src/routes/auth.rs @@ -1,6 +1,6 @@ use crate::helpers::jwt::get_encoding_key; use crate::middlewares::error::ErrorResponse; -use crate::models::user::Users; +use crate::models::users::Users; use actix_web::http::StatusCode; use actix_web::{post, web, HttpResponse, Scope}; use chrono::{Days, Utc}; @@ -17,7 +17,7 @@ pub struct JWTClaims { pub exp: i64, } -#[post("/auth/login")] +#[post("/login")] async fn login(body: web::Json) -> Result { #[derive(Deserialize, Serialize)] struct Response { diff --git a/src/routes/me.rs b/src/routes/me.rs index 4a63bf3..660c772 100644 --- a/src/routes/me.rs +++ b/src/routes/me.rs @@ -1,6 +1,7 @@ use crate::middlewares::error::ErrorResponse; use crate::middlewares::user::get_user; -use crate::models::playlists::{NewPlaylist, Playlists}; +use crate::models::playlists::{NewPlaylist, PlaylistCreator, Playlists}; +use crate::routes::playlists::GetPlaylistResponse; use actix_web::{get, post, web, HttpRequest, HttpResponse, Scope}; use serde::Serialize; @@ -33,14 +34,32 @@ async fn me(req: HttpRequest) -> Result { async fn me_playlists(req: HttpRequest) -> Result { let user = get_user(req)?; - #[derive(Serialize)] - struct Response { - playlists: Vec, - } + let playlists = Playlists::find_for_user(&user.id, false)?; + let playlists = playlists + .into_iter() + .map(|playlist| { + let creator = playlist.get_creator().unwrap(); - Ok(HttpResponse::Ok().json(Response { - playlists: Playlists::find_for_user(&user.id, false)?, - })) + let tracks = playlist.get_tracks().unwrap(); + let (duration, tracks_count) = playlist.get_data(&tracks); + + GetPlaylistResponse { + id: playlist.id.to_string(), + name: playlist.name.to_string(), + playlist_type: playlist.playlist_type.to_string(), + parent_id: playlist.parent_id, + creator: PlaylistCreator { + id: creator.id, + name: creator.name, + }, + tracks, + tracks_count, + duration, + } + }) + .collect::>(); + + Ok(HttpResponse::Ok().json(playlists)) } #[post("/playlists")] @@ -50,7 +69,7 @@ async fn create_playlist( ) -> Result { let user = get_user(req)?; - playlist.creator_id = Option::from(user.id); + playlist.creator_id = user.id; Playlists::create(playlist.into_inner())?; diff --git a/src/routes/playlists.rs b/src/routes/playlists.rs index d49aed9..4106c97 100644 --- a/src/routes/playlists.rs +++ b/src/routes/playlists.rs @@ -1,7 +1,8 @@ use crate::middlewares::error::ErrorResponse; -use crate::models::playlists::{PlaylistCreator, Playlists}; -use crate::models::tracks::TracksWithArtists; -use actix_web::{get, web, HttpResponse, Scope}; +use crate::middlewares::user::get_user_option; +use crate::models::playlists::{PlaylistCreator, PlaylistTrack, Playlists}; +use actix_web::{get, web, HttpRequest, HttpResponse, Scope}; +use reqwest::StatusCode; use serde::{Deserialize, Serialize}; pub fn routes() -> Scope { @@ -9,33 +10,42 @@ pub fn routes() -> Scope { } #[derive(Deserialize, Serialize)] -struct GetPlaylistResponse { +pub struct GetPlaylistResponse { pub id: String, pub name: String, + pub playlist_type: String, + pub parent_id: Option, pub creator: PlaylistCreator, - pub tracks: Vec, + pub tracks: Vec, pub tracks_count: usize, pub duration: usize, } #[get("/{playlist_id}")] -async fn get_playlist(path: web::Path) -> Result { +async fn get_playlist( + req: HttpRequest, + path: web::Path, +) -> Result { let playlist_id = path.into_inner(); let playlist = Playlists::find(playlist_id.as_str())?; + if !playlist.can_see(get_user_option(req)) { + return Err(ErrorResponse::new( + "Playlist not found", + StatusCode::NOT_FOUND, + )); + } + let creator = playlist.get_creator()?; let tracks = playlist.get_tracks()?; - let tracks_count = tracks.len(); - let duration = tracks - .iter() - .map(|track| track.duration_ms) - .reduce(|a, b| a + b) - .unwrap() as usize; + let (duration, tracks_count) = playlist.get_data(&tracks); Ok(HttpResponse::Ok().json(GetPlaylistResponse { id: playlist.id.to_string(), name: playlist.name.to_string(), + playlist_type: playlist.playlist_type.to_string(), + parent_id: playlist.parent_id, creator: PlaylistCreator { id: creator.id, name: creator.name, diff --git a/src/routes/users.rs b/src/routes/users.rs index 85fa1d7..ee1db79 100644 --- a/src/routes/users.rs +++ b/src/routes/users.rs @@ -1,6 +1,9 @@ use crate::middlewares::error::ErrorResponse; -use crate::models::playlists::Playlists; -use crate::models::user::Users; +use crate::models::{ + playlists::{PlaylistCreator, Playlists}, + users::Users, +}; +use crate::routes::playlists::GetPlaylistResponse; use actix_web::http::StatusCode; use actix_web::{get, web, HttpResponse, Result, Scope}; use diesel::result::Error as DBError; @@ -49,6 +52,29 @@ async fn get_user_playlists(path: web::Path) -> Result>(); Ok(HttpResponse::Ok().json(playlists)) } diff --git a/src/schema.rs b/src/schema.rs index 17fb37c..ebc6b6b 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -33,6 +33,10 @@ diesel::table! { name -> Varchar, public -> Bool, #[max_length = 24] + playlist_type -> Varchar, + #[max_length = 24] + parent_id -> Nullable, + #[max_length = 24] creator_id -> Varchar, created_at -> Nullable, updated_at -> Nullable, @@ -45,6 +49,7 @@ diesel::table! { playlist_id -> Varchar, #[max_length = 24] track_id -> Varchar, + added_at -> Timestamp, } }