From 08e24f63f43a6955437c0fb7069ab0bd6e5b9630 Mon Sep 17 00:00:00 2001 From: JMARyA Date: Fri, 4 Oct 2024 14:38:35 +0200 Subject: [PATCH] finish postgres --- migrations/0000_init.sql | 9 +++ src/library/album.rs | 51 ++++++++++++- src/library/artist.rs | 45 +++++++++-- src/library/mod.rs | 159 +++++++++++++++------------------------ src/library/playlist.rs | 97 +++++++++++++----------- src/library/search.rs | 41 ++-------- src/library/track.rs | 133 ++++++++++++++++++++++++++------ src/library/user.rs | 12 ++- src/route/admin.rs | 17 ++--- src/route/album.rs | 28 +++---- src/route/artist.rs | 10 +-- src/route/event.rs | 10 +-- src/route/mod.rs | 27 +++++++ src/route/playlist.rs | 85 ++++++++++----------- src/route/track.rs | 23 ++++-- src/route/user.rs | 13 ++-- 16 files changed, 454 insertions(+), 306 deletions(-) diff --git a/migrations/0000_init.sql b/migrations/0000_init.sql index 4b5f4a7..8af1755 100644 --- a/migrations/0000_init.sql +++ b/migrations/0000_init.sql @@ -47,3 +47,12 @@ CREATE TABLE IF NOT EXISTS events ( ); SELECT create_hypertable('events', by_range('time')); + +CREATE TABLE IF NOT EXISTS playlist ( + id UUID NOT NULL PRIMARY KEY DEFAULT gen_random_uuid(), + owner VARCHAR(255) NOT NULL, + title text NOT NULL, + visibility text NOT NULL DEFAULT 'private' CHECK (visibility IN ('public', 'private')), + tracks UUID[] NOT NULL DEFAULT [], + FOREIGN KEY(owner) REFERENCES user(username) +); diff --git a/src/library/album.rs b/src/library/album.rs index 48d5e82..969b3c6 100644 --- a/src/library/album.rs +++ b/src/library/album.rs @@ -1,3 +1,4 @@ +use crate::route::ToAPI; use mongodb::bson::doc; use serde::{Deserialize, Serialize}; use serde_json::json; @@ -15,7 +16,7 @@ pub struct Album { } impl Album { - pub async fn create(title: &str, artist: Option<&str>) -> Self { + pub async fn create(title: &str, artist: Option) -> Self { sqlx::query_as("INSERT INTO album (title, artist) VALUES ($1, $2) RETURNING *") .bind(title) .bind(artist) @@ -24,6 +25,52 @@ impl Album { .unwrap() } + pub async fn find_all() -> Vec { + sqlx::query_as("SELECT * FROM album") + .fetch_all(get_pg!()) + .await + .unwrap() + } + + pub async fn find(title: &str, artist: Option) -> Option { + sqlx::query_as("SELECT * FROM album WHERE title = $1 AND artist = $2") + .bind(title) + .bind(artist) + .fetch_optional(get_pg!()) + .await + .unwrap() + } + + pub async fn find_regex_col(col: &str, query: &str) -> Vec { + sqlx::query_as(&format!("SELECT * FROM album WHERE {col} ~* $1")) + .bind(query) + .fetch_all(get_pg!()) + .await + .unwrap() + } + + pub async fn find_of_artist(artist: &uuid::Uuid) -> Vec { + sqlx::query_as("SELECT * FROM album WHERE artist = $1") + .bind(artist) + .fetch_all(get_pg!()) + .await + .unwrap() + } + + pub async fn remove(&self) { + sqlx::query("DELETE FROM album WHERE id = $1") + .bind(self.id) + .fetch(get_pg!()); + } + + pub async fn get(id: &uuid::Uuid) -> Option { + sqlx::query_as("SELECT * FROM album WHERE id = $1") + .bind(id) + .fetch_optional(get_pg!()) + .await + .unwrap() + } + pub async fn get_tracks_of_album(album: &uuid::Uuid) -> Vec { sqlx::query_as("SELECT * FROM track WHERE album = $1") .bind(album) @@ -55,7 +102,7 @@ impl Album { } } -impl Album { +impl ToAPI for Album { async fn api(&self) -> serde_json::Value { json!({ "id": &self.id, diff --git a/src/library/artist.rs b/src/library/artist.rs index b6e47bb..1559563 100644 --- a/src/library/artist.rs +++ b/src/library/artist.rs @@ -1,3 +1,4 @@ +use crate::route::ToAPI; use serde::{Deserialize, Serialize}; use serde_json::json; use sqlx::FromRow; @@ -21,14 +22,48 @@ impl Artist { .unwrap() } + pub async fn get(id: &uuid::Uuid) -> Option { + sqlx::query_as("SELECT * FROM artist WHERE id = $1") + .bind(id) + .fetch_optional(get_pg!()) + .await + .unwrap() + } + + pub async fn find(name: &str) -> Option { + sqlx::query_as("SELECT * FROM artist WHERE name = $1") + .bind(name) + .fetch_optional(get_pg!()) + .await + .unwrap() + } + + pub async fn find_all() -> Vec { + sqlx::query_as("SELECT * FROM artist") + .fetch_all(get_pg!()) + .await + .unwrap() + } + + pub async fn find_regex_col(col: &str, query: &str) -> Vec { + sqlx::query_as(&format!("SELECT * FROM artist WHERE {col} ~* $1")) + .bind(query) + .fetch_all(get_pg!()) + .await + .unwrap() + } + + pub async fn remove(&self) { + sqlx::query("DELETE FROM artist WHERE id = $1") + .bind(self.id) + .fetch(get_pg!()); + } + /// Gets the image of an artist or `None` if it can't be found. /// /// This function gets a track from the artist. It then expects the folder structure to be `Artist/Album/Track.ext` and searches for an image file named `artist` in the artist folder. pub async fn get_image_of(id: &uuid::Uuid) -> Option { - // todo : fix - let track_path = Track::find_one(doc! { "artist_id": reference_of!(Artist, id)}) - .await? - .path; + let track_path = Track::find_first_of_artist(id).await?.path; let track_path = std::path::Path::new(&track_path); let artist_path = track_path.parent()?.parent()?; @@ -45,7 +80,7 @@ impl Artist { } } -impl Artist { +impl ToAPI for Artist { async fn api(&self) -> serde_json::Value { json!({ "id": &self.id, diff --git a/src/library/mod.rs b/src/library/mod.rs index e39351c..5ac34a6 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -1,14 +1,16 @@ -use std::path::{Path, PathBuf}; +use std::{ + path::{Path, PathBuf}, + str::FromStr, +}; use album::Album; use artist::Artist; -use mongod::{reference_of, Model, Referencable, Reference}; use mongodb::bson::doc; use serde_json::json; use track::Track; use walkdir::WalkDir; -use crate::cache::RouteCache; +use crate::{cache::RouteCache, get_pg}; pub mod album; pub mod artist; @@ -39,28 +41,29 @@ impl Libary { Self { root_dir } } - pub async fn find_or_create_artist(&self, artist: &str) -> Reference { - if let Some(artist) = Artist::find_one(doc! { "name": artist }).await { - artist.reference() + pub async fn find_or_create_artist(&self, artist: &str) -> uuid::Uuid { + if let Some(artist) = Artist::find(artist).await { + artist.id } else { - Artist::create(artist).await.reference() + Artist::create(artist).await.id } } - pub async fn find_or_create_album(&self, album: &str, artist_id: Option<&str>) -> Reference { - if let Some(album) = Album::find_one(doc! { "title": album, "artist_id": artist_id}).await { - album.reference() + pub async fn find_or_create_album( + &self, + album: &str, + artist_id: Option, + ) -> uuid::Uuid { + if let Some(album) = Album::find(album, artist_id).await { + album.id } else { - Album::create(album, artist_id).await.reference() + Album::create(album, artist_id).await.id } } pub async fn add_path_to_library(&self, path: &str) { // search for path already present - if Track::find_one_partial(doc! { "path": path }, json!({})) - .await - .is_some() - { + if Track::of_path(path).await.is_some() { return; } @@ -70,7 +73,6 @@ impl Libary { let metadata = metadata::get_metadata(path); let mut entry = json!({ - "_id": uuid::Uuid::new_v4().to_string(), "path": path, }); @@ -80,7 +82,7 @@ impl Libary { entry .as_object_mut() .unwrap() - .insert("artist_id".into(), artist_id.into()); + .insert("artist".into(), artist_id.to_string().into()); } else { log::warn!("{path} has no artist"); } @@ -92,14 +94,15 @@ impl Libary { entry .as_object() .unwrap() - .get("artist_id") - .map(|x| x.as_str().unwrap()), + .get("artist") + .map(|x| uuid::Uuid::from_str(x.as_str().unwrap()).unwrap()), ) .await; + entry .as_object_mut() .unwrap() - .insert("album_id".into(), album_id.into()); + .insert("album".into(), album_id.to_string().into()); } else { log::warn!("{path} has no album and will be treated as single"); } @@ -136,41 +139,37 @@ impl Libary { } pub async fn get_artists(&self) -> Vec { - Artist::find_all().await.unwrap() + sqlx::query_as("SELECT * FROM artist") + .fetch_all(get_pg!()) + .await + .unwrap() } - pub async fn get_albums_by_artist(&self, artist: &str) -> Vec { - Album::find( - doc! { "artist_id": reference_of!(Artist, artist).unwrap()}, - None, - None, - ) - .await - .unwrap() + pub async fn get_albums_by_artist(&self, artist: &uuid::Uuid) -> Vec { + sqlx::query_as("SELECT * FROM album WHERE artist = $1") + .bind(artist) + .fetch_all(get_pg!()) + .await + .unwrap() } - pub async fn get_singles_by_artist(&self, artist: &str) -> Vec { - Track::find( - doc! { - "album_id": None::, - "artist_id": reference_of!(Artist, artist).unwrap() - }, - None, - None, - ) - .await - .unwrap() + pub async fn get_singles_by_artist(&self, artist: &uuid::Uuid) -> Vec { + sqlx::query_as("SELECT * FROM track WHERE album IS NULL AND artist = $1") + .bind(artist) + .fetch_all(get_pg!()) + .await + .unwrap() } - pub async fn get_album_by_id(&self, album: &str) -> Option { + pub async fn get_album_by_id(&self, album: &uuid::Uuid) -> Option { Album::get(album).await } - pub async fn get_track_by_id(&self, track_id: &str) -> Option { + pub async fn get_track_by_id(&self, track_id: &uuid::Uuid) -> Option { Track::get(track_id).await } - pub async fn reload_metadata(&self, track_id: &str) -> Result<(), ()> { + pub async fn reload_metadata(&self, track_id: &uuid::Uuid) -> Result<(), ()> { let mut track = Track::get(track_id).await.ok_or_else(|| ())?; let path = &track.path; log::info!("Rescanning metadata for {path}"); @@ -185,7 +184,7 @@ impl Libary { update .as_object_mut() .unwrap() - .insert("artist_id".into(), artist_id.into()); + .insert("artist".into(), artist_id.to_string().into()); } else { log::warn!("{path} has no artist"); } @@ -197,14 +196,14 @@ impl Libary { update .as_object() .unwrap() - .get("artist_id") - .map(|x| x.as_str().unwrap()), + .get("artist") + .map(|x| uuid::Uuid::from_str(x.as_str().unwrap()).unwrap()), ) .await; update .as_object_mut() .unwrap() - .insert("album_id".into(), album_id.into()); + .insert("album".into(), album_id.to_string().into()); } else { log::warn!("{path} has no album and will be treated as single"); } @@ -244,66 +243,26 @@ impl Libary { pub async fn clean_lost_files(&self) { // tracks - for track in Track::find_partial(doc! {}, json!({"path": 1}), None, None) - .await - .unwrap() - { - if !std::path::Path::new(&track.path.as_ref().unwrap()).exists() { - log::info!("Cleaning lost {}", track.path.as_ref().unwrap()); - Track::remove(&track._id).await.unwrap(); + for track in Track::find_all().await { + if !std::path::Path::new(&track.path).exists() { + log::info!("Cleaning lost {}", track.path); + track.remove().await; } } // albums - for album in Album::find_partial(doc! {}, json!({"title": 1}), None, None) - .await - .unwrap() - { - if Track::find_partial( - doc! { "album_id": album.reference() }, - json!({}), - None, - None, - ) - .await - .unwrap() - .is_empty() - { - log::info!( - "Cleaning album {} with no tracks", - album.title.as_ref().unwrap() - ); - Album::remove(album.id()).await.unwrap(); + for album in Album::find_all().await { + if Track::find_of_album(&album.id).await.is_empty() { + log::info!("Cleaning album {} with no tracks", album.title); + album.remove().await; } } // artists - for artist in Artist::find_partial(doc! {}, json!({"name": 1}), None, None) - .await - .unwrap() - { - if Track::find_partial( - doc! { "artist_id": artist.reference()}, - json!({}), - None, - None, - ) - .await - .unwrap() - .is_empty() - && Album::find_partial( - doc! { "artist_id": artist.reference()}, - json!({}), - None, - None, - ) - .await - .unwrap() - .is_empty() + for artist in Artist::find_all().await { + if Track::find_first_of_artist(&artist.id).await.is_none() + && Album::find_of_artist(&artist.id).await.is_empty() { - log::info!( - "Cleaning artist {} with no tracks or albums", - artist.name.as_ref().unwrap() - ); - Artist::remove(artist.id()).await.unwrap(); + log::info!("Cleaning artist {} with no tracks or albums", artist.name); + artist.remove().await; } } } diff --git a/src/library/playlist.rs b/src/library/playlist.rs index a1186b7..2a358ed 100644 --- a/src/library/playlist.rs +++ b/src/library/playlist.rs @@ -1,19 +1,25 @@ -use mongod::{ - assert_reference_of, - derive::{Model, Referencable}, - reference_of, Model, Referencable, Reference, Validate, -}; use serde::{Deserialize, Serialize}; +use sqlx::FromRow; -use crate::library::{track::Track, user::User}; -use mongod::ToAPI; -#[derive(Debug, Clone, Serialize, Deserialize, Model, Referencable)] +use crate::{ + get_pg, + library::{track::Track, user::User}, +}; + +#[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct Playlist { - pub _id: String, - pub owner: Reference, + pub id: String, + pub owner: String, pub title: String, pub visibility: Visibility, - pub tracks: Vec, + pub tracks: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, sqlx::Type)] +#[sqlx(type_name = "visibility", rename_all = "lowercase")] +pub enum Visibility { + Private, + Public, } impl Playlist { @@ -21,50 +27,49 @@ impl Playlist { owner: User, title: &str, visibility: Visibility, - tracks: &[String], + tracks: &[uuid::Uuid], ) -> Option { - let mut tracks_ref = vec![]; + sqlx::query_as("INSERT INTO playlist (owner, title, visibility, tracks) VALUES ($1, $2, $3, $4) RETURNING *") + .bind(owner.username) + .bind(title) + .bind(visibility) + .bind(tracks) + .fetch_one(get_pg!()).await.ok() + } - for track in tracks { - tracks_ref.push(reference_of!(Track, track)?); - } + pub async fn find_regex_col(col: &str, query: &str) -> Vec { + sqlx::query_as(&format!("SELECT * FROM playlist WHERE {col} ~* $1")) + .bind(query) + .fetch_all(get_pg!()) + .await + .unwrap() + } - Some(Self { - _id: uuid::Uuid::new_v4().to_string(), - owner: owner.reference(), - title: title.to_string(), - visibility, - tracks: tracks_ref, - }) + pub async fn get(id: &uuid::Uuid) -> Option { + sqlx::query_as("SELECT * FROM playlist WHERE id = $1") + .bind(id) + .fetch_optional(get_pg!()) + .await + .unwrap() + } + + pub async fn of(u: &User) -> Vec { + sqlx::query_as("SELECT * FROM playlist WHERE owner = $1") + .bind(&u.username) + .fetch_all(get_pg!()) + .await + .unwrap() } } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub enum Visibility { - Private, - Public, -} - -impl Validate for Playlist { - async fn validate(&self) -> Result<(), String> { - assert_reference_of!(self.owner, User); - - for track in &self.tracks { - assert_reference_of!(track, Track); - } - - Ok(()) - } -} - -impl ToAPI for Playlist { - async fn api(&self) -> serde_json::Value { +impl Playlist { + pub async fn api(&self) -> serde_json::Value { serde_json::json!({ - "id": self._id, - "owner": self.owner.id(), + "id": self.id, + "owner": self.owner, "visibility": serde_json::to_value(&self.visibility).unwrap(), "title": self.title, - "tracks": self.tracks.iter().map(mongod::Reference::id).collect::>() + "tracks": self.tracks }) } } diff --git a/src/library/search.rs b/src/library/search.rs index 680abfc..75cb67f 100644 --- a/src/library/search.rs +++ b/src/library/search.rs @@ -1,10 +1,7 @@ +use serde::Serialize; use std::cmp::Ordering; -use mongod::Model; -use mongodb::bson::doc; -use serde::Serialize; - -use mongod::ToAPI; +use crate::route::ToAPI; use super::{album::Album, artist::Artist, playlist::Playlist, track::Track}; @@ -74,13 +71,7 @@ pub async fn search_for(query: String) -> Option> { let mut results: Vec = Vec::new(); // Add artist results - for artist in Artist::find( - doc! { "name": { "$regex": &query, "$options": "i" } }, - None, - None, - ) - .await? - { + for artist in Artist::find_regex_col("name", &query).await { results.push(SearchResult { kind: "artist".to_string(), data: artist.api().await, @@ -89,13 +80,7 @@ pub async fn search_for(query: String) -> Option> { } // Add album results - for album in Album::find( - doc! { "title": { "$regex": &query, "$options": "i" } }, - None, - None, - ) - .await? - { + for album in Album::find_regex_col("title", &query).await { results.push(SearchResult { kind: "album".to_string(), data: album.api().await, @@ -104,28 +89,16 @@ pub async fn search_for(query: String) -> Option> { } // Add track results - for track in Track::find( - doc! { "title": { "$regex": &query, "$options": "i" } }, - None, - None, - ) - .await? - { + for track in Track::find_regex_col("title", &query).await { results.push(SearchResult { kind: "track".to_string(), data: track.api().await, - score: calculate_score(&track.title, &query, Some(track.date_added)), + score: calculate_score(&track.title, &query, Some(track.date_added.timestamp())), }); } // Add playlist results - for playlist in Playlist::find( - doc! { "title": { "$regex": &query, "$options": "i" } }, - None, - None, - ) - .await? - { + for playlist in Playlist::find_regex_col("title", &query).await { results.push(SearchResult { kind: "playlist".to_string(), data: playlist.api().await, diff --git a/src/library/track.rs b/src/library/track.rs index 1946c88..d5a8bbd 100644 --- a/src/library/track.rs +++ b/src/library/track.rs @@ -1,11 +1,12 @@ use serde::{Deserialize, Serialize}; use serde_json::json; use sqlx::prelude::FromRow; -use std::collections::HashSet; +use std::{collections::HashSet, str::FromStr}; use crate::{ get_pg, library::{album::Album, artist::Artist}, + route::ToAPI, }; use super::{event::Event, metadata::AudioMetadata, user::User}; @@ -30,6 +31,75 @@ impl Track { .fetch(get_pg!()); } + pub async fn of_path(path: &str) -> Option { + sqlx::query_as("SELECT * FROM track WHERE path = $1") + .bind(path) + .fetch_optional(get_pg!()) + .await + .unwrap() + } + + pub async fn find_recently_added() -> Vec { + sqlx::query_as("SELECT * FROM track ORDER BY date_added DESC LIMIT 90") + .fetch_all(get_pg!()) + .await + .unwrap() + } + + pub async fn get(id: &uuid::Uuid) -> Option { + sqlx::query_as("SELECT * FROM track WHERE id = $1") + .bind(id) + .fetch_optional(get_pg!()) + .await + .unwrap() + } + + pub async fn update(&self, update_set: &serde_json::Value) -> Option<()> { + let map = update_set.as_object()?; + let artist = map + .get("artist") + .map(|x| uuid::Uuid::from_str(x.as_str().unwrap()).unwrap()); + let album = map + .get("album") + .map(|x| uuid::Uuid::from_str(x.as_str().unwrap()).unwrap()); + let title = map.get("title")?.as_str()?; + let meta = map.get("meta"); + + sqlx::query( + "UPDATE track SET artist = $1, album = $2, title = $3, meta = $4 WHERE id = $5; +", + ) + .bind(artist) + .bind(album) + .bind(title) + .bind(meta) + .bind(self.id) + .fetch(get_pg!()); + + Some(()) + } + + pub async fn find_all() -> Vec { + sqlx::query_as("SELECT * FROM track") + .fetch_all(get_pg!()) + .await + .unwrap() + } + + pub async fn remove(&self) { + sqlx::query("DELETE FROM track WHERE id = $1") + .bind(self.id) + .fetch(get_pg!()); + } + + pub async fn find_of_album(album: &uuid::Uuid) -> Vec { + sqlx::query_as("SELECT * FROM track WHERE album = $1") + .bind(album) + .fetch_all(get_pg!()) + .await + .unwrap() + } + pub async fn get_latest_of_user(u: &User) -> Vec { let latest_events = Event::get_latest_events_of(u).await; let mut ids = HashSet::new(); @@ -59,13 +129,13 @@ impl Track { /// Transcode audio pub fn transcode(&self, codec: &str, bitrate: u32, ext: &str) -> Option { - let transcoded = format!("./data/transcode/{codec}/{bitrate}/{}.{ext}", self._id); + let transcoded = format!("./data/transcode/{codec}/{bitrate}/{}.{ext}", self.id); if std::path::Path::new(&transcoded).exists() { return Some(transcoded); } - log::info!("Transcoding {} to {} {}", self._id, codec, bitrate); + log::info!("Transcoding {} to {} {}", self.id, codec, bitrate); std::fs::create_dir_all(format!("./data/transcode/{codec}/{bitrate}")).unwrap(); @@ -87,38 +157,57 @@ impl Track { Some(transcoded) } + pub async fn find_regex_col(col: &str, query: &str) -> Vec { + sqlx::query_as(&format!("SELECT * FROM track WHERE {col} ~* $1")) + .bind(query) + .fetch_all(get_pg!()) + .await + .unwrap() + } + /// Find tracks with no album or artist pub async fn get_orphans() -> Vec { - // todo : fix - Self::find( - doc! { - "artist_id": None::, - "album_id": None:: - }, - None, - None, - ) - .await - .unwrap() + sqlx::query_as("SELECT * FROM track WHERE artist IS NULL AND album IS NULL") + .fetch_all(get_pg!()) + .await + .unwrap() + } + + pub async fn find_first_of_artist(artist: &uuid::Uuid) -> Option { + sqlx::query_as("SELECT * FROM track WHERE artist = $1 LIMIT 1") + .bind(artist) + .fetch_optional(get_pg!()) + .await + .unwrap() + } + + pub async fn find_first_of_album(album: &uuid::Uuid) -> Option { + sqlx::query_as("SELECT * FROM track WHERE album = $1 LIMIT 1") + .bind(album) + .fetch_optional(get_pg!()) + .await + .unwrap() } } -impl Track { +impl ToAPI for Track { async fn api(&self) -> serde_json::Value { // todo : fix let (cover, album_title, album_id) = if let Some(album_ref) = &self.album { - let album = album_ref.get::().await; + let album = Album::get(album_ref).await.unwrap(); - (album.get_cover().await.is_some(), album.title, album._id) + (album.get_cover().await.is_some(), album.title, album.id) } else { - (false, String::new(), String::new()) + (false, String::new(), uuid::Uuid::nil()) }; let artist_title = if let Some(artist_ref) = &self.artist { - artist_ref - .get_partial::(json!({"name": 1})) + let res: (String,) = sqlx::query_as("SELECT name FROM artist WHERE id = $1") + .bind(artist_ref) + .fetch_one(get_pg!()) .await - .name + .unwrap(); + Some(res.0) } else { None }; @@ -126,7 +215,7 @@ impl Track { json!({ "id": self.id, "title": self.title, - "track_number": self.meta.as_ref().map(|x| AudioMetadata(*x).track_number()), + "track_number": self.meta.as_ref().map(|x| AudioMetadata(x.clone()).track_number()), "meta": serde_json::to_value(&self.meta).unwrap(), "album_id": self.album, "album": album_title, diff --git a/src/library/user.rs b/src/library/user.rs index c350a8c..9e9a9da 100644 --- a/src/library/user.rs +++ b/src/library/user.rs @@ -1,3 +1,4 @@ +use crate::route::ToAPI; use data_encoding::HEXUPPER; use rand::RngCore; use serde::{Deserialize, Serialize}; @@ -81,6 +82,13 @@ impl User { Err(()) } + pub async fn find_all() -> Vec { + sqlx::query_as("SELECT * FROM user") + .fetch_all(get_pg!()) + .await + .unwrap() + } + pub async fn session(&self) -> Session { sqlx::query_as( "INSERT INTO user_session (token, user) VALUES ($1, $2) RETURNING id, token, user", @@ -93,7 +101,7 @@ impl User { } pub const fn is_admin(&self) -> bool { - matches!(self.role, UserRole::Admin) + matches!(self.user_role, UserRole::Admin) } pub fn verify_pw(&self, password: &str) -> bool { @@ -101,7 +109,7 @@ impl User { } } -impl User { +impl ToAPI for User { async fn api(&self) -> serde_json::Value { json!({ "username": self.username, diff --git a/src/route/admin.rs b/src/route/admin.rs index 957c3af..2c1f92f 100644 --- a/src/route/admin.rs +++ b/src/route/admin.rs @@ -1,8 +1,7 @@ use super::api_error; use super::FallibleApiResponse; -use mongod::vec_to_api; -use mongod::Model; -use mongodb::bson::doc; +use crate::get_pg; +use crate::route::vec_to_api; use rocket::{get, State}; use serde_json::json; @@ -21,13 +20,11 @@ pub async fn clean_library(lib: &State, u: User) -> FallibleApiResponse #[get("/library/singles")] pub async fn get_singles_route(u: User) -> FallibleApiResponse { check_admin!(u); - let singles = Track::find( - doc! { "album_id": None::, "artist_id": {"$ne": None:: }}, - None, - None, - ) - .await - .unwrap(); + let singles: Vec = + sqlx::query_as("SELECT * FROM track WHERE album IS NULL AND artist IS NOT NULL") + .fetch_all(get_pg!()) + .await + .unwrap(); Ok(json!(vec_to_api(&singles).await)) } diff --git a/src/route/album.rs b/src/route/album.rs index c4269ae..633e42d 100644 --- a/src/route/album.rs +++ b/src/route/album.rs @@ -1,11 +1,8 @@ use std::cmp::Ordering; use super::api_error; +use super::to_uuid; use super::FallibleApiResponse; -use mongod::vec_to_api; -use mongod::Model; -use mongod::Referencable; -use mongodb::bson::doc; use rocket::fs::NamedFile; use rocket::{get, State}; use serde_json::json; @@ -14,13 +11,14 @@ use crate::cache::RouteCache; use crate::library::album::Album; use crate::library::track::Track; use crate::library::Libary; +use crate::route::vec_to_api; +use crate::route::ToAPI; use crate::use_api_cache; -use mongod::ToAPI; #[get("/artist//albums")] pub async fn albums_route(artist_id: &str, lib: &State) -> FallibleApiResponse { - let albums = lib.get_albums_by_artist(artist_id).await; - let singles = lib.get_singles_by_artist(artist_id).await; + let albums = lib.get_albums_by_artist(&to_uuid(artist_id)?).await; + let singles = lib.get_singles_by_artist(&to_uuid(artist_id)?).await; Ok(json!({ "artist": artist_id, @@ -49,7 +47,7 @@ pub async fn album_cover_route( NamedFile::open( cache .get_option("album_cover_route", album_id, || async { - let album = lib.get_album_by_id(album_id).await?; + let album = lib.get_album_by_id(&to_uuid(album_id).unwrap()).await?; album.get_cover().await }) .await?, @@ -60,18 +58,16 @@ pub async fn album_cover_route( #[get("/albums/latest")] pub async fn latest_albums_route(cache: &State) -> FallibleApiResponse { + // todo : fix + use_api_cache!("albums", "latest", cache); - let albums = Album::find_all().await.unwrap(); + let albums = Album::find_all().await; let mut albums_tracks = vec![]; for album in &albums { - albums_tracks.push( - Track::find_one(doc! { "album_id": album.reference()}) - .await - .unwrap(), - ); + albums_tracks.push(Track::find_first_of_album(&album.id).await.unwrap()); } let mut joined: Vec<(_, _)> = albums.into_iter().zip(albums_tracks).collect(); @@ -100,11 +96,11 @@ pub async fn album_route( use_api_cache!("album_route", album_id, cache); let album = lib - .get_album_by_id(album_id) + .get_album_by_id(&to_uuid(album_id)?) .await .ok_or_else(|| api_error("No album with that ID found"))?; - let tracks = Album::get_tracks_of_album(&format!("album::{}", album._id)).await; + let tracks = Album::get_tracks_of_album(&album.id).await; let mut tracks = vec_to_api(&tracks).await; diff --git a/src/route/artist.rs b/src/route/artist.rs index 055347a..c43940e 100644 --- a/src/route/artist.rs +++ b/src/route/artist.rs @@ -1,9 +1,9 @@ use super::api_error; +use super::to_uuid; +use super::vec_to_api; use super::FallibleApiResponse; +use super::ToAPI; use fs::NamedFile; -use mongod::vec_to_api; -use mongod::Model; -use mongod::ToAPI; use mongodb::bson::doc; use rocket::{fs, get, State}; @@ -22,7 +22,7 @@ pub async fn artists_route(lib: &State) -> FallibleApiResponse { pub async fn artist_image_route(id: &str, cache: &State) -> Option { let image = cache .get_option("artist_image_route", id, || async { - Artist::get_image_of(id).await + Artist::get_image_of(&to_uuid(id).ok()?).await }) .await; @@ -31,7 +31,7 @@ pub async fn artist_image_route(id: &str, cache: &State) -> Option")] pub async fn artist_route(id: &str) -> FallibleApiResponse { - Ok(Artist::get(id) + Ok(Artist::get(&to_uuid(id)?) .await .ok_or_else(|| api_error("No artist with that ID found"))? .api() diff --git a/src/route/event.rs b/src/route/event.rs index 1031a12..5b48396 100644 --- a/src/route/event.rs +++ b/src/route/event.rs @@ -1,8 +1,6 @@ use super::api_error; +use super::to_uuid; use super::FallibleApiResponse; -use mongod::reference_of; -use mongod::Model; -use mongodb::bson::doc; use rocket::post; use rocket::serde::json::Json; use serde::Deserialize; @@ -12,7 +10,6 @@ use crate::library::event::Event; use crate::library::event::EventKind; use crate::library::track::Track; use crate::library::user::User; -use mongod::Referencable; #[derive(Debug, Clone, Deserialize)] pub struct EventJson { @@ -26,7 +23,10 @@ pub async fn event_report_route(report: Json, u: User) -> FallibleApi Event::create( report.kind.clone(), &u, - reference_of!(Track, track).ok_or_else(|| api_error("Invalid track"))?, + Track::get(&to_uuid(&track)?) + .await + .ok_or_else(|| api_error("Invalid track"))? + .id, ) .await; diff --git a/src/route/mod.rs b/src/route/mod.rs index 2f20fa8..6a62dee 100644 --- a/src/route/mod.rs +++ b/src/route/mod.rs @@ -1,3 +1,5 @@ +use std::str::FromStr; + use rocket::{ get, response::{status::BadRequest, Redirect}, @@ -16,9 +18,34 @@ pub mod user; // todo : rework api +/// A trait to generate a Model API representation in JSON format. +pub trait ToAPI: Sized { + /// Generate public API JSON + fn api(&self) -> impl std::future::Future; +} + +/// Converts a slice of items implementing the `ToAPI` trait into a `Vec` of JSON values. +pub async fn vec_to_api(items: &[impl ToAPI]) -> Vec { + let mut ret = Vec::with_capacity(items.len()); + + for e in items { + ret.push(e.api().await); + } + + ret +} + +pub fn to_uuid(id: &str) -> Result { + uuid::Uuid::from_str(id).map_err(|_| no_uuid_error()) +} + type ApiError = BadRequest; type FallibleApiResponse = Result; +pub fn no_uuid_error() -> ApiError { + api_error("No valid UUID") +} + pub fn api_error(msg: &str) -> ApiError { BadRequest(json!({ "error": msg diff --git a/src/route/playlist.rs b/src/route/playlist.rs index 576455a..12a51bd 100644 --- a/src/route/playlist.rs +++ b/src/route/playlist.rs @@ -1,9 +1,6 @@ +use crate::get_pg; use crate::library::track::Track; use crate::library::user::User; -use mongod::reference_of; -use mongod::vec_to_api; -use mongod::Model; -use mongod::Referencable; use mongodb::bson::doc; use rocket::get; use rocket::post; @@ -15,7 +12,8 @@ use crate::library::playlist::Visibility; use crate::route::FallibleApiResponse; use super::api_error; -use mongod::ToAPI; +use super::to_uuid; +use super::vec_to_api; #[get("/playlists")] pub async fn playlists_route(u: User) -> FallibleApiResponse { @@ -24,13 +22,11 @@ pub async fn playlists_route(u: User) -> FallibleApiResponse { json!({"id": "recentlyAdded", "name": "Recently Added"}), ]; - let own_playlists = Playlist::find(doc! { "owner": u.reference()}, None, None) - .await - .unwrap(); + let own_playlists = Playlist::of(&u).await; for playlist in own_playlists { playlists.push(json!({ - "id": playlist._id, + "id": playlist.id, "name": playlist.title })); } @@ -39,9 +35,7 @@ pub async fn playlists_route(u: User) -> FallibleApiResponse { } pub async fn recently_added_playlist() -> FallibleApiResponse { - let tracks = Track::find(doc! {}, Some(90), Some(doc! { "date_added": -1 })) - .await - .unwrap(); + let tracks = Track::find_recently_added().await; Ok(json!(vec_to_api(&tracks).await)) } @@ -49,8 +43,8 @@ pub async fn recently_added_playlist() -> FallibleApiResponse { pub async fn playlist_route(id: &str, u: User) -> FallibleApiResponse { if id == "recents" { return Ok(Playlist { - _id: "recents".to_string(), - owner: u.reference(), + id: "recents".to_string(), + owner: u.username, title: "Recently Played".to_string(), visibility: Visibility::Public, tracks: vec![], @@ -61,8 +55,8 @@ pub async fn playlist_route(id: &str, u: User) -> FallibleApiResponse { if id == "recentlyAdded" { return Ok(Playlist { - _id: "recentlyAdded".to_string(), - owner: u.reference(), + id: "recentlyAdded".to_string(), + owner: u.username, title: "Recently Added".to_string(), visibility: Visibility::Public, tracks: vec![], @@ -71,13 +65,11 @@ pub async fn playlist_route(id: &str, u: User) -> FallibleApiResponse { .await); } - let playlist = Playlist::get(id) + let playlist = Playlist::get(&to_uuid(id)?) .await .ok_or_else(|| api_error("No playlist with that ID found"))?; - if matches!(playlist.visibility, Visibility::Private) - && u.username != playlist.owner.get::().await.username - { + if matches!(playlist.visibility, Visibility::Private) && u.username != playlist.owner { return Err(api_error("Forbidden")); } @@ -95,22 +87,26 @@ pub async fn playlist_tracks_route(id: &str, u: User) -> FallibleApiResponse { return recently_added_playlist().await; } - let playlist = Playlist::get(id) + let playlist = Playlist::get(&to_uuid(id)?) .await .ok_or_else(|| api_error("No playlist with that ID found"))?; - if matches!(playlist.visibility, Visibility::Private) - && u.username != playlist.owner.get::().await.username - { + if matches!(playlist.visibility, Visibility::Private) && u.username != playlist.owner { return Err(api_error("Forbidden")); } - let mut tracks: Vec = vec![]; + let mut tracks: Vec = vec![]; for track in playlist.tracks { - tracks.push(track.get().await); + tracks.push(track); } + let tracks: Vec = sqlx::query_as("SELECT * FROM track WHERE id IN ANY($1)") + .bind(tracks) + .fetch_all(get_pg!()) + .await + .unwrap(); + Ok(json!(vec_to_api(&tracks).await)) } @@ -118,7 +114,7 @@ pub async fn playlist_tracks_route(id: &str, u: User) -> FallibleApiResponse { pub struct PlaylistData { pub title: Option, pub visibility: Option, - pub tracks: Vec, + pub tracks: Vec, } #[post("/playlist", data = "")] @@ -137,9 +133,8 @@ pub async fn playlist_add_route(playlist: Json, u: User) -> Fallib ) .await .ok_or_else(|| api_error("Failed to create playlist"))?; - playlist.insert().await.unwrap(); - Ok(json!({"created": playlist._id})) + Ok(json!({"created": playlist.id})) } #[post("/playlist/", data = "")] @@ -148,34 +143,36 @@ pub async fn playlist_edit_route( edit: Json, u: User, ) -> FallibleApiResponse { - let playlist = Playlist::get(id) + let playlist = Playlist::get(&to_uuid(id)?) .await .ok_or_else(|| api_error("No playlist with that ID found"))?; - if playlist.owner.id() != u._id { + if playlist.owner != u.username { return Err(api_error("Forbidden")); } let mut tracks_ref = vec![]; for track in &edit.tracks { - tracks_ref - .push(reference_of!(Track, track).ok_or_else(|| api_error("Invalid tracks found"))?); + tracks_ref.push( + Track::get(track) + .await + .ok_or_else(|| api_error("Invalid tracks found"))? + .id, + ); } - let playlist_id = playlist._id.clone(); + let playlist_id = playlist.id.clone(); - let mut changed = playlist.change(); + let new_title = edit.title.as_ref().unwrap_or(&playlist.title); + let new_vis = edit.visibility.as_ref().unwrap_or(&playlist.visibility); - if let Some(title) = &edit.title { - changed = changed.title(title); - } - - if let Some(visibility) = &edit.visibility { - changed = changed.visibility(visibility.clone()); - } - - changed.tracks(tracks_ref).update().await.unwrap(); + sqlx::query("UPDATE playlist SET title = $1, visibility = $2, tracks = $3 WHERE id = $4") + .bind(new_title) + .bind(new_vis) + .bind(tracks_ref) + .bind(&to_uuid(&playlist_id)?) + .fetch(get_pg!()); Ok(json!({"edited": playlist_id})) } diff --git a/src/route/track.rs b/src/route/track.rs index 8dcc11a..f98e098 100644 --- a/src/route/track.rs +++ b/src/route/track.rs @@ -1,9 +1,12 @@ +use std::str::FromStr; + use super::api_error; +use super::no_uuid_error; +use super::to_uuid; use super::FallibleApiResponse; +use super::ToAPI; use crate::library::user::User; use fs::NamedFile; -use mongod::ToAPI; -use mongodb::bson::doc; use rocket::{fs, get, State}; use serde_json::json; @@ -13,7 +16,7 @@ use crate::library::Libary; #[get("/track/")] pub async fn track_route(track_id: &str, lib: &State) -> FallibleApiResponse { Ok(lib - .get_track_by_id(track_id) + .get_track_by_id(&to_uuid(track_id)?) .await .ok_or_else(|| api_error("No track with that ID found"))? .api() @@ -27,7 +30,7 @@ pub async fn track_reload_meta_route( u: User, ) -> FallibleApiResponse { check_admin!(u); - lib.reload_metadata(track_id) + lib.reload_metadata(&to_uuid(track_id)?) .await .map_err(|_| api_error("Error reloading metadata"))?; Ok(json!({"ok": 1})) @@ -35,7 +38,9 @@ pub async fn track_reload_meta_route( #[get("/track//audio")] pub async fn track_audio_route(track_id: &str, lib: &State) -> Option { - let track = lib.get_track_by_id(track_id).await?; + let track = lib + .get_track_by_id(&uuid::Uuid::from_str(track_id).ok()?) + .await?; NamedFile::open(std::path::Path::new(&track.path)) .await .ok() @@ -43,12 +48,16 @@ pub async fn track_audio_route(track_id: &str, lib: &State) -> Option/audio/opus128")] pub async fn track_audio_opus128_route(track_id: &str, lib: &State) -> Option { - let track = lib.get_track_by_id(track_id).await?; + let track = lib + .get_track_by_id(&uuid::Uuid::from_str(track_id).ok()?) + .await?; NamedFile::open(track.get_opus(128)?).await.ok() } #[get("/track//audio/aac128")] pub async fn track_audio_aac128_route(track_id: &str, lib: &State) -> Option { - let track = lib.get_track_by_id(track_id).await?; + let track = lib + .get_track_by_id(&uuid::Uuid::from_str(track_id).ok()?) + .await?; NamedFile::open(track.get_aac(128)?).await.ok() } diff --git a/src/route/user.rs b/src/route/user.rs index 40b5f40..fbc91fb 100644 --- a/src/route/user.rs +++ b/src/route/user.rs @@ -1,9 +1,7 @@ +use crate::get_pg; use crate::library::user::Session; use crate::library::user::User; -use mongod::vec_to_api; -use mongod::Model; -use mongod::ToAPI; -use mongodb::bson::doc; +use crate::route::vec_to_api; use rocket::get; use rocket::http::Status; use rocket::outcome::Outcome; @@ -33,8 +31,7 @@ impl<'r> FromRequest<'r> for User { async fn from_request(request: &'r Request<'_>) -> rocket::request::Outcome { match request.headers().get_one("token") { Some(key) => { - if let Some(session) = Session::find_one(doc! { "token": key}).await { - let user = session.user.get().await; + if let Some(user) = sqlx::query_as("SELECT * FROM user WHERE id = (SELECT user FROM user_session WHERE token = $1)").bind(key).fetch_optional(get_pg!()).await.unwrap() { Outcome::Success(user) } else { Outcome::Error((Status::Unauthorized, ())) @@ -84,7 +81,7 @@ pub async fn passwd_route(passwd: Json, u: User) -> FallibleApiRespo pub async fn users_route(u: User) -> FallibleApiResponse { check_admin!(u); - let users: Vec<_> = vec_to_api(&User::find_all().await.unwrap()).await; + let users: Vec<_> = vec_to_api(&User::find_all().await).await; Ok(json!({"users": users})) } @@ -101,5 +98,5 @@ pub async fn user_create_route(user: Json, u: User) -> FallibleApiRes .await .unwrap(); - Ok(json!({"created": new_user._id})) + Ok(json!({"created": new_user.username})) }