This commit is contained in:
Miguel da Mota 2024-01-07 15:43:56 +01:00
parent 292ff60720
commit fa8ed2b599
19 changed files with 349 additions and 216 deletions

View file

@ -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())
})

View file

@ -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<Users, ErrorResponse> {
)),
}
}
pub fn get_user_option(req: HttpRequest) -> Option<Users> {
let user = get_user(req);
match user {
Ok(user) => Some(user),
Err(_) => None,
}
}

View file

@ -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<String>,
}
#[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<NaiveDateTime>,
pub updated_at: Option<NaiveDateTime>,
pub name: String,
pub spotify_id: Option<String>,
pub tidal_id: Option<String>,
@ -38,14 +32,14 @@ pub struct Artists {
impl Artists {
pub fn find(id: String) -> Result<Self, Error> {
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<Self, Error> {
pub fn create(artist: NewArtist) -> Result<Self, Error> {
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,
}
}
}

View file

@ -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<Vec<Tracks>, Error> {
let conn = &mut db::connection()?;
let track_ids = ArtistTracks::belonging_to(artist).select(artists_tracks::track_id);
let tracks: Vec<Tracks> = tracks::table
.filter(tracks::id.eq_any(track_ids))
.load::<Tracks>(conn)?;
Ok(tracks)
}
pub fn get_artists(track: &Tracks) -> Result<Vec<TrackArtist>, 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::<TrackArtist>(conn)?;
Ok(artists)
}
}
#[derive(Debug, Deserialize, Serialize, Queryable)]
#[diesel(table_name = artists)]
pub struct TrackArtist {
pub id: String,
pub name: String,
}

View file

@ -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;

View file

@ -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<String>,
pub creator_id: String,
pub created_at: Option<NaiveDateTime>,
pub updated_at: Option<NaiveDateTime>,
}
#[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<NaiveDateTime>,
pub updated_at: Option<NaiveDateTime>,
}
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<Vec<TracksWithArtists>, Error> {
let tracks = Tracks::find_by_playlist(&self.id)?;
let tracks: Vec<TracksWithArtists> = tracks
.into_iter()
.map(Tracks::with_artists)
.collect::<Result<Vec<TracksWithArtists>, _>>()?;
pub fn get_tracks(&self) -> Result<Vec<PlaylistTrack>, Error> {
let tracks: Vec<PlaylistTrack> = 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<String>,
}
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<Users>) -> 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<Playlists>,
// }
// impl Folder {
// pub fn get_playlists(&self) -> Result<Vec<Playlists>, Error> {
// let playlists = playlists::table
// }
// }
#[derive(Deserialize, Serialize)]
pub struct PlaylistTrack {
pub id: String,
pub title: String,
pub duration_ms: i32,
pub artists: Vec<TrackArtist>,
pub added_at: NaiveDateTime,
pub spotify_id: Option<String>,
pub tidal_id: Option<String>,
}
impl From<Tracks> 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::<NaiveDateTime>(&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,
}
}
}

View file

@ -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<Vec<PlaylistTrack>, Error> {
let conn = &mut db::connection()?;
let track_ids = PlaylistTracks::belonging_to(playlist).select(playlists_tracks::track_id);
let tracks: Vec<Tracks> = tracks::table
.filter(tracks::id.eq_any(track_ids))
.load::<Tracks>(conn)?;
let tracks = tracks
.into_iter()
.map(PlaylistTrack::from)
.collect::<Vec<PlaylistTrack>>();
Ok(tracks)
}
}

View file

@ -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<String>,
}
#[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<String>,
pub tidal_id: Option<String>,
}
impl Tracks {
pub fn find(id: String) -> Result<Self, Error> {
let conn = &mut db::connection()?;
@ -32,29 +40,15 @@ impl Tracks {
Ok(playlist)
}
pub fn create(track: Tracks) -> Result<Self, Error> {
pub fn create(track: NewTrack) -> Result<Self, Error> {
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<Vec<Tracks>, 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::<Vec<Tracks>>();
Ok(tracks)
}
pub fn create_or_update(track: Tracks) -> Result<Self, Error> {
pub fn create_or_update(track: NewTrack) -> Result<Self, Error> {
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<TracksWithArtists, Error> {
let artists = track.get_artists()?;

View file

@ -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<NaiveDateTime>,
pub updated_at: Option<NaiveDateTime>,
}
#[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<Self, Error> {
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<Self, Error> {
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,
}
}
}

View file

@ -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<LoginBody>) -> Result<HttpResponse, ErrorResponse> {
#[derive(Deserialize, Serialize)]
struct Response {

View file

@ -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<HttpResponse, ErrorResponse> {
async fn me_playlists(req: HttpRequest) -> Result<HttpResponse, ErrorResponse> {
let user = get_user(req)?;
#[derive(Serialize)]
struct Response {
playlists: Vec<Playlists>,
}
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::<Vec<GetPlaylistResponse>>();
Ok(HttpResponse::Ok().json(playlists))
}
#[post("/playlists")]
@ -50,7 +69,7 @@ async fn create_playlist(
) -> Result<HttpResponse, ErrorResponse> {
let user = get_user(req)?;
playlist.creator_id = Option::from(user.id);
playlist.creator_id = user.id;
Playlists::create(playlist.into_inner())?;

View file

@ -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<String>,
pub creator: PlaylistCreator,
pub tracks: Vec<TracksWithArtists>,
pub tracks: Vec<PlaylistTrack>,
pub tracks_count: usize,
pub duration: usize,
}
#[get("/{playlist_id}")]
async fn get_playlist(path: web::Path<String>) -> Result<HttpResponse, ErrorResponse> {
async fn get_playlist(
req: HttpRequest,
path: web::Path<String>,
) -> Result<HttpResponse, ErrorResponse> {
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,

View file

@ -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<String>) -> Result<HttpResponse, Err
let user = get_a_user(&path.into_inner())?;
let playlists = Playlists::find_for_user(&user.id, true)?;
let playlists = playlists
.into_iter()
.map(|playlist| {
let creator = playlist.get_creator().unwrap();
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::<Vec<GetPlaylistResponse>>();
Ok(HttpResponse::Ok().json(playlists))
}

View file

@ -33,6 +33,10 @@ diesel::table! {
name -> Varchar,
public -> Bool,
#[max_length = 24]
playlist_type -> Varchar,
#[max_length = 24]
parent_id -> Nullable<Varchar>,
#[max_length = 24]
creator_id -> Varchar,
created_at -> Nullable<Timestamp>,
updated_at -> Nullable<Timestamp>,
@ -45,6 +49,7 @@ diesel::table! {
playlist_id -> Varchar,
#[max_length = 24]
track_id -> Varchar,
added_at -> Timestamp,
}
}