This commit is contained in:
JMARyA 2024-10-04 12:00:33 +02:00
parent 02b9e34258
commit 7b7e1a4014
Signed by: jmarya
GPG key ID: 901B2ADDF27C2263
10 changed files with 942 additions and 270 deletions

761
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -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"] }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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