finish postgres

This commit is contained in:
JMARyA 2024-10-04 14:38:35 +02:00
parent 7b7e1a4014
commit 08e24f63f4
Signed by: jmarya
GPG key ID: 901B2ADDF27C2263
16 changed files with 454 additions and 306 deletions

View file

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

View file

@ -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<uuid::Uuid>) -> 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<Self> {
sqlx::query_as("SELECT * FROM album")
.fetch_all(get_pg!())
.await
.unwrap()
}
pub async fn find(title: &str, artist: Option<uuid::Uuid>) -> Option<Self> {
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<Self> {
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<Self> {
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<Self> {
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<Track> {
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,

View file

@ -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<Self> {
sqlx::query_as("SELECT * FROM artist WHERE id = $1")
.bind(id)
.fetch_optional(get_pg!())
.await
.unwrap()
}
pub async fn find(name: &str) -> Option<Self> {
sqlx::query_as("SELECT * FROM artist WHERE name = $1")
.bind(name)
.fetch_optional(get_pg!())
.await
.unwrap()
}
pub async fn find_all() -> Vec<Self> {
sqlx::query_as("SELECT * FROM artist")
.fetch_all(get_pg!())
.await
.unwrap()
}
pub async fn find_regex_col(col: &str, query: &str) -> Vec<Self> {
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<String> {
// 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,

View file

@ -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>,
) -> 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> {
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> {
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<Album> {
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> {
Track::find(
doc! {
"album_id": None::<String>,
"artist_id": reference_of!(Artist, artist).unwrap()
},
None,
None,
)
.await
.unwrap()
pub async fn get_singles_by_artist(&self, artist: &uuid::Uuid) -> Vec<Track> {
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<Album> {
pub async fn get_album_by_id(&self, album: &uuid::Uuid) -> Option<Album> {
Album::get(album).await
}
pub async fn get_track_by_id(&self, track_id: &str) -> Option<Track> {
pub async fn get_track_by_id(&self, track_id: &uuid::Uuid) -> Option<Track> {
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;
}
}
}

View file

@ -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<Reference>,
pub tracks: Vec<uuid::Uuid>,
}
#[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<Self> {
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<Self> {
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<Self> {
sqlx::query_as("SELECT * FROM playlist WHERE id = $1")
.bind(id)
.fetch_optional(get_pg!())
.await
.unwrap()
}
pub async fn of(u: &User) -> Vec<Self> {
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::<Vec<_>>()
"tracks": self.tracks
})
}
}

View file

@ -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<Vec<SearchResult>> {
let mut results: Vec<SearchResult> = 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<Vec<SearchResult>> {
}
// 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<Vec<SearchResult>> {
}
// 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,

View file

@ -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<Self> {
sqlx::query_as("SELECT * FROM track WHERE path = $1")
.bind(path)
.fetch_optional(get_pg!())
.await
.unwrap()
}
pub async fn find_recently_added() -> Vec<Self> {
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<Self> {
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<Self> {
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<Self> {
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<Self> {
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<String> {
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<Self> {
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<Track> {
// todo : fix
Self::find(
doc! {
"artist_id": None::<String>,
"album_id": None::<String>
},
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<Self> {
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<Self> {
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::<Album>().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::<Artist>(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,

View file

@ -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<Self> {
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,

View file

@ -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<Libary>, 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::<String>, "artist_id": {"$ne": None::<String> }},
None,
None,
)
.await
.unwrap();
let singles: Vec<Track> =
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))
}

View file

@ -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/<artist_id>/albums")]
pub async fn albums_route(artist_id: &str, lib: &State<Libary>) -> 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<RouteCache>) -> 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;

View file

@ -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<Libary>) -> FallibleApiResponse {
pub async fn artist_image_route(id: &str, cache: &State<RouteCache>) -> Option<NamedFile> {
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<RouteCache>) -> Option<N
#[get("/artist/<id>")]
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()

View file

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

View file

@ -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<Output = serde_json::Value>;
}
/// 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<serde_json::Value> {
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, ApiError> {
uuid::Uuid::from_str(id).map_err(|_| no_uuid_error())
}
type ApiError = BadRequest<serde_json::Value>;
type FallibleApiResponse = Result<serde_json::Value, ApiError>;
pub fn no_uuid_error() -> ApiError {
api_error("No valid UUID")
}
pub fn api_error(msg: &str) -> ApiError {
BadRequest(json!({
"error": msg

View file

@ -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::<User>().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::<User>().await.username
{
if matches!(playlist.visibility, Visibility::Private) && u.username != playlist.owner {
return Err(api_error("Forbidden"));
}
let mut tracks: Vec<Track> = vec![];
let mut tracks: Vec<uuid::Uuid> = vec![];
for track in playlist.tracks {
tracks.push(track.get().await);
tracks.push(track);
}
let tracks: Vec<Track> = 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<String>,
pub visibility: Option<Visibility>,
pub tracks: Vec<String>,
pub tracks: Vec<uuid::Uuid>,
}
#[post("/playlist", data = "<playlist>")]
@ -137,9 +133,8 @@ pub async fn playlist_add_route(playlist: Json<PlaylistData>, 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/<id>", data = "<edit>")]
@ -148,34 +143,36 @@ pub async fn playlist_edit_route(
edit: Json<PlaylistData>,
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}))
}

View file

@ -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/<track_id>")]
pub async fn track_route(track_id: &str, lib: &State<Libary>) -> 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/<track_id>/audio")]
pub async fn track_audio_route(track_id: &str, lib: &State<Libary>) -> Option<NamedFile> {
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<Libary>) -> Option<Na
#[get("/track/<track_id>/audio/opus128")]
pub async fn track_audio_opus128_route(track_id: &str, lib: &State<Libary>) -> Option<NamedFile> {
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/<track_id>/audio/aac128")]
pub async fn track_audio_aac128_route(track_id: &str, lib: &State<Libary>) -> Option<NamedFile> {
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()
}

View file

@ -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<Self, Self::Error> {
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<PasswdData>, 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<LoginData>, u: User) -> FallibleApiRes
.await
.unwrap();
Ok(json!({"created": new_user._id}))
Ok(json!({"created": new_user.username}))
}