work
This commit is contained in:
parent
02b9e34258
commit
7b7e1a4014
10 changed files with 942 additions and 270 deletions
761
Cargo.lock
generated
761
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -9,12 +9,12 @@ rocket = { version = "0.5.1", features = ["json"] }
|
||||||
rocket_cors = "0.6.0"
|
rocket_cors = "0.6.0"
|
||||||
serde = { version = "1.0.203", features = ["derive"] }
|
serde = { version = "1.0.203", features = ["derive"] }
|
||||||
serde_json = "1.0.117"
|
serde_json = "1.0.117"
|
||||||
uuid = { version = "1.9.1", features = ["v4"] }
|
|
||||||
walkdir = "2.5.0"
|
walkdir = "2.5.0"
|
||||||
mongod = { git = "https://git.hydrar.de/jmarya/mongod", features = ["cache"] }
|
|
||||||
bcrypt = "0.15.1"
|
bcrypt = "0.15.1"
|
||||||
data-encoding = "2.6.0"
|
data-encoding = "2.6.0"
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
env_logger = "0.11.5"
|
env_logger = "0.11.5"
|
||||||
log = "0.4.22"
|
log = "0.4.22"
|
||||||
chrono = "0.4.38"
|
chrono = { version = "0.4.38", features = ["serde"] }
|
||||||
|
uuid = { version = "1.8.0", features = ["v4", "serde"] }
|
||||||
|
sqlx = { version = "0.8", features = ["postgres", "runtime-tokio-native-tls", "derive", "uuid", "chrono", "json"] }
|
|
@ -5,20 +5,19 @@ services:
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
depends_on:
|
depends_on:
|
||||||
- mongodb
|
- postgres
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/data # Runtime data (optional)
|
- ./data:/data # Runtime data (optional)
|
||||||
- ./media:/media # Audio files
|
- ./media:/media # Audio files
|
||||||
environment:
|
environment:
|
||||||
- "DB_URI=mongodb://user:pass@mongodb:27017"
|
- "DATABASE_URL=postgres://user:pass@postgres/synthwave"
|
||||||
- "DB=synthwrld"
|
|
||||||
|
|
||||||
mongodb:
|
postgres:
|
||||||
image: mongo:latest
|
image: timescale/timescaledb:latest-pg16
|
||||||
ports:
|
restart: always
|
||||||
- "27017:27017"
|
|
||||||
environment:
|
|
||||||
MONGO_INITDB_ROOT_USERNAME: user
|
|
||||||
MONGO_INITDB_ROOT_PASSWORD: pass
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./db:/data/db
|
- ./db:/var/lib/postgresql/data/
|
||||||
|
environment:
|
||||||
|
- POSTGRES_USER=user
|
||||||
|
- POSTGRES_PASSWORD=pass
|
||||||
|
- POSTGRES_DB=synthwave
|
||||||
|
|
49
migrations/0000_init.sql
Normal file
49
migrations/0000_init.sql
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS user (
|
||||||
|
username varchar(255) NOT NULL PRIMARY KEY,
|
||||||
|
password text NOT NULL,
|
||||||
|
user_role text NOT NULL DEFAULT 'Regular' CHECK (user_role IN ('regular', 'admin'))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS user_session (
|
||||||
|
id UUID NOT NULL PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
token text NOT NULL,
|
||||||
|
user varchar(255) NOT NULL,
|
||||||
|
FOREIGN KEY(user) REFERENCES user(username)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS artist (
|
||||||
|
id UUID NOT NULL PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
name text NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS album (
|
||||||
|
id UUID NOT NULL PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
title text NOT NULL,
|
||||||
|
artist UUID
|
||||||
|
FOREIGN KEY(artist) REFERENCES artist(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS track (
|
||||||
|
id UUID NOT NULL PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
path text NOT NULL,
|
||||||
|
title text,
|
||||||
|
date_added timestampz NOT NULL DEFAULT current_timestamp,
|
||||||
|
album UUID,
|
||||||
|
artist UUID,
|
||||||
|
meta jsonb,
|
||||||
|
FOREIGN KEY(album) REFERENCES album(id),
|
||||||
|
FOREIGN KEY(artist) REFERENCES artist(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS events (
|
||||||
|
id UUID NOT NULL PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
time timestampz NOT NULL DEFAULT current_timestamp,
|
||||||
|
kind text CHECK (kind IN ('play', 'played', 'stop')),
|
||||||
|
user VARCHAR(255) NOT NULL,
|
||||||
|
track UUID NOT NULL,
|
||||||
|
FOREIGN KEY(user) REFERENCES user(username),
|
||||||
|
FOREIGN KEY(track) REFERENCES track(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
SELECT create_hypertable('events', by_range('time'));
|
|
@ -1,41 +1,33 @@
|
||||||
use mongod::{
|
|
||||||
assert_reference_of,
|
|
||||||
derive::{Model, Referencable},
|
|
||||||
Model, Referencable, Reference, Validate,
|
|
||||||
};
|
|
||||||
use mongodb::bson::doc;
|
use mongodb::bson::doc;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
use sqlx::FromRow;
|
||||||
|
|
||||||
use crate::library::artist::Artist;
|
use crate::{get_pg, library::artist::Artist};
|
||||||
use mongod::ToAPI;
|
|
||||||
|
|
||||||
use super::track::Track;
|
use super::track::Track;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Model, Referencable)]
|
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
||||||
pub struct Album {
|
pub struct Album {
|
||||||
pub _id: String,
|
pub id: uuid::Uuid,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub artist_id: Option<Reference>,
|
pub artist: Option<uuid::Uuid>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Album {
|
impl Album {
|
||||||
pub async fn create(title: &str, artist: Option<&str>) -> Self {
|
pub async fn create(title: &str, artist: Option<&str>) -> Self {
|
||||||
let a = Self {
|
sqlx::query_as("INSERT INTO album (title, artist) VALUES ($1, $2) RETURNING *")
|
||||||
_id: uuid::Uuid::new_v4().to_string(),
|
.bind(title)
|
||||||
title: title.to_string(),
|
.bind(artist)
|
||||||
artist_id: if let Some(artist) = artist {
|
.fetch_one(get_pg!())
|
||||||
Some(Reference::new_raw(artist))
|
.await
|
||||||
} else {
|
.unwrap()
|
||||||
None
|
|
||||||
},
|
|
||||||
};
|
|
||||||
a.insert().await.unwrap();
|
|
||||||
a
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_tracks_of_album(album: &str) -> Vec<Track> {
|
pub async fn get_tracks_of_album(album: &uuid::Uuid) -> Vec<Track> {
|
||||||
Track::find(doc! { "album_id": album}, None, None)
|
sqlx::query_as("SELECT * FROM track WHERE album = $1")
|
||||||
|
.bind(album)
|
||||||
|
.fetch_all(get_pg!())
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
@ -44,7 +36,7 @@ impl Album {
|
||||||
///
|
///
|
||||||
/// This method first retrieves the list of tracks in the album, then looks for a file named "cover.{ext}" where {ext} is one of png, jpg, jpeg, avif. The first existing cover file found will be returned.
|
/// This method first retrieves the list of tracks in the album, then looks for a file named "cover.{ext}" where {ext} is one of png, jpg, jpeg, avif. The first existing cover file found will be returned.
|
||||||
pub async fn get_cover(&self) -> Option<String> {
|
pub async fn get_cover(&self) -> Option<String> {
|
||||||
let track_path = Self::get_tracks_of_album(&format!("album::{}", self._id))
|
let track_path = Self::get_tracks_of_album(&self.id)
|
||||||
.await
|
.await
|
||||||
.first()?
|
.first()?
|
||||||
.path
|
.path
|
||||||
|
@ -63,27 +55,17 @@ impl Album {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToAPI for Album {
|
impl Album {
|
||||||
async fn api(&self) -> serde_json::Value {
|
async fn api(&self) -> serde_json::Value {
|
||||||
json!({
|
json!({
|
||||||
"id": &self._id,
|
"id": &self.id,
|
||||||
"title": &self.title,
|
"title": &self.title,
|
||||||
"artist": self.artist_id.as_ref().map(mongod::Reference::id),
|
"artist": self.artist,
|
||||||
"cover_url": if self.get_cover().await.is_some() {
|
"cover_url": if self.get_cover().await.is_some() {
|
||||||
Some(format!("/album/{}/cover", self._id))
|
Some(format!("/album/{}/cover", self.id))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Validate for Album {
|
|
||||||
async fn validate(&self) -> Result<(), String> {
|
|
||||||
if let Some(artist_id) = &self.artist_id {
|
|
||||||
assert_reference_of!(artist_id, Artist);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,35 +1,31 @@
|
||||||
use mongod::{
|
|
||||||
derive::{Model, Referencable},
|
|
||||||
reference_of, Model, Referencable, Validate,
|
|
||||||
};
|
|
||||||
use mongodb::bson::doc;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
use sqlx::FromRow;
|
||||||
|
|
||||||
use mongod::ToAPI;
|
use crate::get_pg;
|
||||||
|
|
||||||
use super::track::Track;
|
use super::track::Track;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Model, Referencable)]
|
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
||||||
pub struct Artist {
|
pub struct Artist {
|
||||||
pub _id: String,
|
pub id: uuid::Uuid,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Artist {
|
impl Artist {
|
||||||
pub async fn create(name: &str) -> Self {
|
pub async fn create(name: &str) -> Self {
|
||||||
let a = Artist {
|
sqlx::query_as("INSERT INTO artist (name) VALUES ($1) RETURNING id, name")
|
||||||
_id: uuid::Uuid::new_v4().to_string(),
|
.bind(name)
|
||||||
name: name.to_string(),
|
.fetch_one(get_pg!())
|
||||||
};
|
.await
|
||||||
a.insert().await.unwrap();
|
.unwrap()
|
||||||
a
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the image of an artist or `None` if it can't be found.
|
/// 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.
|
/// 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: &str) -> Option<String> {
|
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)})
|
let track_path = Track::find_one(doc! { "artist_id": reference_of!(Artist, id)})
|
||||||
.await?
|
.await?
|
||||||
.path;
|
.path;
|
||||||
|
@ -49,22 +45,16 @@ impl Artist {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToAPI for Artist {
|
impl Artist {
|
||||||
async fn api(&self) -> serde_json::Value {
|
async fn api(&self) -> serde_json::Value {
|
||||||
json!({
|
json!({
|
||||||
"id": &self._id,
|
"id": &self.id,
|
||||||
"name": &self.name,
|
"name": &self.name,
|
||||||
"image": if Artist::get_image_of(self.id()).await.is_some() {
|
"image": if Artist::get_image_of(&self.id).await.is_some() {
|
||||||
Some(format!("/artist/{}/image", self.id()))
|
Some(format!("/artist/{}/image", self.id))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Validate for Artist {
|
|
||||||
async fn validate(&self) -> Result<(), String> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,25 +1,19 @@
|
||||||
use mongod::{
|
|
||||||
assert_reference_of,
|
|
||||||
derive::{Model, Referencable},
|
|
||||||
Model, Referencable, Reference, Validate,
|
|
||||||
};
|
|
||||||
use mongodb::bson::doc;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sqlx::prelude::FromRow;
|
||||||
|
|
||||||
use crate::library::user::User;
|
use crate::{get_pg, library::user::User};
|
||||||
|
|
||||||
use super::track::Track;
|
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Model, Referencable)]
|
|
||||||
pub struct Event {
|
pub struct Event {
|
||||||
pub _id: String,
|
pub id: uuid::Uuid,
|
||||||
|
pub timestamp: chrono::DateTime<chrono::Utc>,
|
||||||
pub kind: EventKind,
|
pub kind: EventKind,
|
||||||
pub user: Reference,
|
pub user: String,
|
||||||
pub track: Reference,
|
pub track: uuid::Uuid,
|
||||||
pub timestamp: i64,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::Type)]
|
||||||
|
#[sqlx(type_name = "event_kind", rename_all = "lowercase")]
|
||||||
pub enum EventKind {
|
pub enum EventKind {
|
||||||
Play,
|
Play,
|
||||||
Played,
|
Played,
|
||||||
|
@ -27,36 +21,21 @@ pub enum EventKind {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Event {
|
impl Event {
|
||||||
pub async fn create(kind: EventKind, user: &User, track: Reference) -> Self {
|
pub async fn create(kind: EventKind, user: &User, track: uuid::Uuid) -> Self {
|
||||||
let event = Self {
|
sqlx::query_as("INSERT INTO events (kind, user, track) VALUES ($1, $2, $3) RETURNING *")
|
||||||
_id: uuid::Uuid::new_v4().to_string(),
|
.bind(kind)
|
||||||
kind: kind,
|
.bind(&user.username)
|
||||||
user: user.reference(),
|
.bind(track)
|
||||||
track,
|
.fetch_one(get_pg!())
|
||||||
timestamp: chrono::Utc::now().timestamp(),
|
.await
|
||||||
};
|
.unwrap()
|
||||||
|
|
||||||
event.insert().await.unwrap();
|
|
||||||
|
|
||||||
event
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_latest_events_of(u: &User) -> Vec<Self> {
|
pub async fn get_latest_events_of(u: &User) -> Vec<Self> {
|
||||||
Self::find(
|
sqlx::query_as("SELECT * FROM events WHERE user = $1 ORDER BY time DESC LIMIT 300")
|
||||||
doc! { "user": u.reference() },
|
.bind(&u.username)
|
||||||
Some(300),
|
.fetch_all(get_pg!())
|
||||||
Some(doc! { "timestamp": -1 }),
|
.await
|
||||||
)
|
.unwrap()
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Validate for Event {
|
|
||||||
async fn validate(&self) -> Result<(), String> {
|
|
||||||
assert_reference_of!(self.user, User);
|
|
||||||
assert_reference_of!(self.track, Track);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,61 +1,50 @@
|
||||||
use std::collections::HashSet;
|
|
||||||
|
|
||||||
use mongod::{
|
|
||||||
assert_reference_of,
|
|
||||||
derive::{Model, Referencable},
|
|
||||||
Model, Referencable, Reference, Validate,
|
|
||||||
};
|
|
||||||
use mongodb::bson::doc;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
use sqlx::prelude::FromRow;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use crate::library::{album::Album, artist::Artist};
|
use crate::{
|
||||||
use mongod::ToAPI;
|
get_pg,
|
||||||
|
library::{album::Album, artist::Artist},
|
||||||
|
};
|
||||||
|
|
||||||
use super::{event::Event, metadata::AudioMetadata, user::User};
|
use super::{event::Event, metadata::AudioMetadata, user::User};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Model, Referencable, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
||||||
pub struct Track {
|
pub struct Track {
|
||||||
pub _id: String,
|
pub id: uuid::Uuid,
|
||||||
pub path: String,
|
pub path: String,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub date_added: i64,
|
pub date_added: chrono::DateTime<chrono::Utc>,
|
||||||
pub album_id: Option<Reference>,
|
pub album: Option<uuid::Uuid>,
|
||||||
pub artist_id: Option<Reference>,
|
pub artist: Option<uuid::Uuid>,
|
||||||
pub meta: Option<AudioMetadata>,
|
pub meta: Option<serde_json::Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Track {
|
impl Track {
|
||||||
pub async fn create(data: &serde_json::Map<String, serde_json::Value>) {
|
pub async fn create(data: &serde_json::Map<String, serde_json::Value>) {
|
||||||
let mut t = Self {
|
sqlx::query("INSERT INTO track (path, title, meta) VALUES ($1, $2, $4)")
|
||||||
_id: uuid::Uuid::new_v4().to_string(),
|
.bind(data.get("path").unwrap().as_str().unwrap().to_string())
|
||||||
path: data.get("path").unwrap().as_str().unwrap().to_string(),
|
.bind(data.get("title").unwrap().as_str().unwrap().to_string())
|
||||||
title: data.get("title").unwrap().as_str().unwrap().to_string(),
|
.bind(data.get("meta"))
|
||||||
date_added: chrono::Utc::now().timestamp(),
|
.fetch(get_pg!());
|
||||||
album_id: None,
|
|
||||||
artist_id: None,
|
|
||||||
meta: data.get("meta").map(|x| AudioMetadata(x.clone())),
|
|
||||||
};
|
|
||||||
t.insert().await.unwrap();
|
|
||||||
t.update(&serde_json::to_value(data).unwrap())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_latest_of_user(u: &User) -> Vec<Self> {
|
pub async fn get_latest_of_user(u: &User) -> Vec<Self> {
|
||||||
let latest_events = Event::get_latest_events_of(u).await;
|
let latest_events = Event::get_latest_events_of(u).await;
|
||||||
let mut ids = HashSet::new();
|
let mut ids = HashSet::new();
|
||||||
let mut tracks = vec![];
|
|
||||||
|
|
||||||
for event in latest_events {
|
for event in latest_events {
|
||||||
let track: Track = event.track.get().await;
|
if !ids.contains(&event.track) {
|
||||||
if !ids.contains(&track._id) {
|
ids.insert(event.track.clone());
|
||||||
ids.insert(track._id.clone());
|
|
||||||
tracks.push(track);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tracks
|
sqlx::query_as("SELECT * FROM track WHERE id = ANY($1)")
|
||||||
|
.bind(ids.into_iter().collect::<Vec<_>>())
|
||||||
|
.fetch_all(get_pg!())
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Transcode audio to OPUS with `bitrate`
|
/// Transcode audio to OPUS with `bitrate`
|
||||||
|
@ -100,6 +89,7 @@ impl Track {
|
||||||
|
|
||||||
/// Find tracks with no album or artist
|
/// Find tracks with no album or artist
|
||||||
pub async fn get_orphans() -> Vec<Track> {
|
pub async fn get_orphans() -> Vec<Track> {
|
||||||
|
// todo : fix
|
||||||
Self::find(
|
Self::find(
|
||||||
doc! {
|
doc! {
|
||||||
"artist_id": None::<String>,
|
"artist_id": None::<String>,
|
||||||
|
@ -113,9 +103,10 @@ impl Track {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToAPI for Track {
|
impl Track {
|
||||||
async fn api(&self) -> serde_json::Value {
|
async fn api(&self) -> serde_json::Value {
|
||||||
let (cover, album_title, album_id) = if let Some(album_ref) = &self.album_id {
|
// 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_ref.get::<Album>().await;
|
||||||
|
|
||||||
(album.get_cover().await.is_some(), album.title, album._id)
|
(album.get_cover().await.is_some(), album.title, album._id)
|
||||||
|
@ -123,7 +114,7 @@ impl ToAPI for Track {
|
||||||
(false, String::new(), String::new())
|
(false, String::new(), String::new())
|
||||||
};
|
};
|
||||||
|
|
||||||
let artist_title = if let Some(artist_ref) = &self.artist_id {
|
let artist_title = if let Some(artist_ref) = &self.artist {
|
||||||
artist_ref
|
artist_ref
|
||||||
.get_partial::<Artist>(json!({"name": 1}))
|
.get_partial::<Artist>(json!({"name": 1}))
|
||||||
.await
|
.await
|
||||||
|
@ -133,33 +124,19 @@ impl ToAPI for Track {
|
||||||
};
|
};
|
||||||
|
|
||||||
json!({
|
json!({
|
||||||
"id": self._id,
|
"id": self.id,
|
||||||
"title": self.title,
|
"title": self.title,
|
||||||
"track_number": self.meta.as_ref().map(super::metadata::AudioMetadata::track_number),
|
"track_number": self.meta.as_ref().map(|x| AudioMetadata(*x).track_number()),
|
||||||
"meta": serde_json::to_value(&self.meta).unwrap(),
|
"meta": serde_json::to_value(&self.meta).unwrap(),
|
||||||
"album_id": self.album_id.as_ref().map(|x| x.id().to_string()),
|
"album_id": self.album,
|
||||||
"album": album_title,
|
"album": album_title,
|
||||||
"cover": if cover {
|
"cover": if cover {
|
||||||
Some(format!("/album/{album_id}/cover"))
|
Some(format!("/album/{album_id}/cover"))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
"artist_id": self.artist_id.as_ref().map(|x| x.id().to_string()),
|
"artist_id": self.artist,
|
||||||
"artist": artist_title
|
"artist": artist_title
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Validate for Track {
|
|
||||||
async fn validate(&self) -> Result<(), String> {
|
|
||||||
if let Some(artist_id) = &self.artist_id {
|
|
||||||
assert_reference_of!(artist_id, Artist);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(album_id) = &self.artist_id {
|
|
||||||
assert_reference_of!(album_id, Artist);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,15 +1,10 @@
|
||||||
use data_encoding::HEXUPPER;
|
use data_encoding::HEXUPPER;
|
||||||
use mongod::{
|
|
||||||
assert_reference_of,
|
|
||||||
derive::{Model, Referencable},
|
|
||||||
Model, Referencable, Reference, Validate,
|
|
||||||
};
|
|
||||||
use mongodb::bson::doc;
|
|
||||||
use rand::RngCore;
|
use rand::RngCore;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
use sqlx::FromRow;
|
||||||
|
|
||||||
use mongod::ToAPI;
|
use crate::get_pg;
|
||||||
|
|
||||||
fn gen_token(token_length: usize) -> String {
|
fn gen_token(token_length: usize) -> String {
|
||||||
let mut token_bytes = vec![0u8; token_length];
|
let mut token_bytes = vec![0u8; token_length];
|
||||||
|
@ -19,65 +14,66 @@ fn gen_token(token_length: usize) -> String {
|
||||||
HEXUPPER.encode(&token_bytes)
|
HEXUPPER.encode(&token_bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Model, Referencable)]
|
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
pub _id: String,
|
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub password: String,
|
pub password: String,
|
||||||
pub role: UserRole,
|
pub user_role: UserRole,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::Type)]
|
||||||
|
#[sqlx(type_name = "user_role", rename_all = "lowercase")]
|
||||||
pub enum UserRole {
|
pub enum UserRole {
|
||||||
Regular,
|
Regular,
|
||||||
Admin,
|
Admin,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Validate for User {
|
|
||||||
async fn validate(&self) -> Result<(), String> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl User {
|
impl User {
|
||||||
pub async fn create(username: &str, password: &str, role: UserRole) -> Option<Self> {
|
pub async fn find(username: &str) -> Option<Self> {
|
||||||
if Self::find_one_partial(doc! { "username": username }, json!({}))
|
sqlx::query_as("SELECT * FROM user WHERE username = $1")
|
||||||
|
.bind(username)
|
||||||
|
.fetch_optional(get_pg!())
|
||||||
.await
|
.await
|
||||||
.is_some()
|
.unwrap()
|
||||||
{
|
}
|
||||||
|
|
||||||
|
pub async fn create(username: &str, password: &str, role: UserRole) -> Option<Self> {
|
||||||
|
if Self::find(username).await.is_some() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let u = Self {
|
let u = Self {
|
||||||
_id: uuid::Uuid::new_v4().to_string(),
|
|
||||||
username: username.to_string(),
|
username: username.to_string(),
|
||||||
password: bcrypt::hash(password, bcrypt::DEFAULT_COST).unwrap(),
|
password: bcrypt::hash(password, bcrypt::DEFAULT_COST).unwrap(),
|
||||||
role,
|
user_role: role,
|
||||||
};
|
};
|
||||||
|
|
||||||
u.insert().await.ok()?;
|
sqlx::query("INSERT INTO user (username, password, user_role) VALUES ($1, $2, $3)")
|
||||||
|
.bind(&u.username)
|
||||||
|
.bind(&u.password)
|
||||||
|
.bind(&u.user_role)
|
||||||
|
.fetch(get_pg!());
|
||||||
|
|
||||||
Some(u)
|
Some(u)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn login(username: &str, password: &str) -> Option<(Session, UserRole)> {
|
pub async fn login(username: &str, password: &str) -> Option<(Session, UserRole)> {
|
||||||
let u = Self::find_one(doc! { "username": username }).await?;
|
let u = Self::find(username).await?;
|
||||||
|
|
||||||
if !u.verify_pw(password) {
|
if !u.verify_pw(password) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
Some((u.session().await, u.role))
|
Some((u.session().await, u.user_role))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Change the password of a `User`
|
/// Change the password of a `User`
|
||||||
pub async fn passwd(self, old: &str, new: &str) -> Result<(), ()> {
|
pub async fn passwd(self, old: &str, new: &str) -> Result<(), ()> {
|
||||||
if self.verify_pw(old) {
|
if self.verify_pw(old) {
|
||||||
self.change()
|
sqlx::query("UPDATE user SET password = $1 WHERE username = $2;")
|
||||||
.password(bcrypt::hash(new, bcrypt::DEFAULT_COST).unwrap())
|
.bind(bcrypt::hash(new, bcrypt::DEFAULT_COST).unwrap())
|
||||||
.update()
|
.bind(&self.username)
|
||||||
.await
|
.fetch(get_pg!());
|
||||||
.map_err(|_| ())?;
|
|
||||||
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
@ -86,15 +82,14 @@ impl User {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn session(&self) -> Session {
|
pub async fn session(&self) -> Session {
|
||||||
let s = Session {
|
sqlx::query_as(
|
||||||
_id: uuid::Uuid::new_v4().to_string(),
|
"INSERT INTO user_session (token, user) VALUES ($1, $2) RETURNING id, token, user",
|
||||||
token: gen_token(60),
|
)
|
||||||
user: self.reference(),
|
.bind(gen_token(64))
|
||||||
};
|
.bind(&self.username)
|
||||||
|
.fetch_one(get_pg!())
|
||||||
s.insert().await.unwrap();
|
.await
|
||||||
|
.unwrap()
|
||||||
s
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn is_admin(&self) -> bool {
|
pub const fn is_admin(&self) -> bool {
|
||||||
|
@ -106,26 +101,18 @@ impl User {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToAPI for User {
|
impl User {
|
||||||
async fn api(&self) -> serde_json::Value {
|
async fn api(&self) -> serde_json::Value {
|
||||||
json!({
|
json!({
|
||||||
"username": self.username,
|
"username": self.username,
|
||||||
"role": self.role
|
"role": self.user_role
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Model, Referencable)]
|
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
||||||
pub struct Session {
|
pub struct Session {
|
||||||
pub _id: String,
|
pub id: uuid::Uuid,
|
||||||
pub token: String,
|
pub token: String,
|
||||||
pub user: Reference,
|
pub user: String,
|
||||||
}
|
|
||||||
|
|
||||||
impl Validate for Session {
|
|
||||||
async fn validate(&self) -> Result<(), String> {
|
|
||||||
assert_reference_of!(self.user, User);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
30
src/main.rs
30
src/main.rs
|
@ -5,11 +5,30 @@ mod library;
|
||||||
mod route;
|
mod route;
|
||||||
|
|
||||||
use library::user::{User, UserRole};
|
use library::user::{User, UserRole};
|
||||||
use mongod::Model;
|
|
||||||
use mongodb::bson::doc;
|
use mongodb::bson::doc;
|
||||||
use rocket::routes;
|
use rocket::routes;
|
||||||
|
use rocket::tokio::sync::OnceCell;
|
||||||
use rocket::{http::Method, launch};
|
use rocket::{http::Method, launch};
|
||||||
|
|
||||||
|
pub static PG: OnceCell<sqlx::PgPool> = OnceCell::const_new();
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! get_pg {
|
||||||
|
() => {
|
||||||
|
if let Some(client) = $crate::PG.get() {
|
||||||
|
client
|
||||||
|
} else {
|
||||||
|
let client = sqlx::postgres::PgPoolOptions::new()
|
||||||
|
.max_connections(5)
|
||||||
|
.connect(&std::env::var("DATABASE_URL").unwrap())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
$crate::PG.set(client).unwrap();
|
||||||
|
$crate::PG.get().unwrap()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
#[launch]
|
#[launch]
|
||||||
async fn rocket() -> _ {
|
async fn rocket() -> _ {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
|
@ -27,16 +46,17 @@ async fn rocket() -> _ {
|
||||||
.to_cors()
|
.to_cors()
|
||||||
.expect("error creating CORS options");
|
.expect("error creating CORS options");
|
||||||
|
|
||||||
|
let pg = get_pg!();
|
||||||
|
|
||||||
|
sqlx::migrate!("./migrations").run(pg).await.unwrap();
|
||||||
|
|
||||||
let lib = Libary::new("./media".into());
|
let lib = Libary::new("./media".into());
|
||||||
let cache = cache::RouteCache::new();
|
let cache = cache::RouteCache::new();
|
||||||
|
|
||||||
lib.rescan(&cache).await;
|
lib.rescan(&cache).await;
|
||||||
|
|
||||||
// create initial admin user
|
// create initial admin user
|
||||||
if User::find(doc! { "username": "admin" }, None, None)
|
if User::find("admin").await.is_none() {
|
||||||
.await
|
|
||||||
.is_none()
|
|
||||||
{
|
|
||||||
User::create("admin", "admin", UserRole::Admin).await;
|
User::create("admin", "admin", UserRole::Admin).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue