init
This commit is contained in:
parent
0d3df6bb64
commit
dcf546fa9c
18 changed files with 3463 additions and 1 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
/target
|
||||||
|
/db
|
2714
Cargo.lock
generated
Normal file
2714
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
17
Cargo.toml
Normal file
17
Cargo.toml
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
[package]
|
||||||
|
name = "synthwave"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
mongodb = "2.8.2"
|
||||||
|
rocket = { version = "0.5.1", features = ["json"] }
|
||||||
|
rocket_cors = "0.6.0"
|
||||||
|
serde = { version = "1.0.203", features = ["derive"] }
|
||||||
|
serde_json = "1.0.117"
|
||||||
|
uuid = { version = "1.9.1", features = ["v4"] }
|
||||||
|
walkdir = "2.5.0"
|
||||||
|
mongod = { git = "https://git.hydrar.de/jmarya/mongod" }
|
||||||
|
bcrypt = "0.15.1"
|
||||||
|
data-encoding = "2.6.0"
|
||||||
|
rand = "0.8.5"
|
|
@ -1,2 +1,3 @@
|
||||||
# synth.universe
|
# synthwave
|
||||||
|
synthwave music server
|
||||||
|
|
||||||
|
|
22
docker-compose.yml
Normal file
22
docker-compose.yml
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
version: '3'
|
||||||
|
services:
|
||||||
|
# synthwave:
|
||||||
|
# build: .
|
||||||
|
# ports:
|
||||||
|
# - "8080:8080"
|
||||||
|
# depends_on:
|
||||||
|
# - mongodb
|
||||||
|
# environment:
|
||||||
|
# - "DB_URI=mongodb://user:pass@mongodb:27017"
|
||||||
|
# - "DB=synthwrld"
|
||||||
|
|
||||||
|
mongodb:
|
||||||
|
image: mongo:latest
|
||||||
|
ports:
|
||||||
|
- "27017:27017"
|
||||||
|
environment:
|
||||||
|
MONGO_INITDB_ROOT_USERNAME: user
|
||||||
|
MONGO_INITDB_ROOT_PASSWORD: pass
|
||||||
|
volumes:
|
||||||
|
- ./db:/data/db
|
||||||
|
|
35
src/extract_metadata.py
Normal file
35
src/extract_metadata.py
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import sys
|
||||||
|
from mutagen import File
|
||||||
|
import json
|
||||||
|
|
||||||
|
def print_metadata(file_path):
|
||||||
|
try:
|
||||||
|
audio = File(file_path)
|
||||||
|
meta = {}
|
||||||
|
|
||||||
|
if audio is None:
|
||||||
|
print(json.dumps({"error": "unsupported file"}))
|
||||||
|
return
|
||||||
|
|
||||||
|
for key, value in audio.items():
|
||||||
|
if type(value) is list and len(value) == 1:
|
||||||
|
value = value[0]
|
||||||
|
meta[key] = value
|
||||||
|
|
||||||
|
if audio.info:
|
||||||
|
meta["info"] = {}
|
||||||
|
for key, value in vars(audio.info).items():
|
||||||
|
meta["info"][key] = value
|
||||||
|
|
||||||
|
print(json.dumps(meta))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({"error": str(e)}))
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) != 2:
|
||||||
|
print("Usage: python script.py <path_to_audio_file>")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
file_path = sys.argv[1]
|
||||||
|
print_metadata(file_path)
|
41
src/library/album.rs
Normal file
41
src/library/album.rs
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
use mongod::{
|
||||||
|
assert_reference_of,
|
||||||
|
derive::{Model, Referencable},
|
||||||
|
Model, Referencable, Reference, Validate,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::library::artist::Artist;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Model, Referencable)]
|
||||||
|
pub struct Album {
|
||||||
|
pub _id: String,
|
||||||
|
pub title: String,
|
||||||
|
pub artist_id: Option<Reference>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Album {
|
||||||
|
pub async fn create(title: &str, artist: Option<&str>) -> Self {
|
||||||
|
let a = Self {
|
||||||
|
_id: uuid::Uuid::new_v4().to_string(),
|
||||||
|
title: title.to_string(),
|
||||||
|
artist_id: if let Some(artist) = artist {
|
||||||
|
Some(Reference::new_raw(artist).await.unwrap())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
};
|
||||||
|
a.insert().await.unwrap();
|
||||||
|
a
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
28
src/library/artist.rs
Normal file
28
src/library/artist.rs
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
use mongod::{
|
||||||
|
derive::{Model, Referencable},
|
||||||
|
Model, Validate,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Model, Referencable)]
|
||||||
|
pub struct Artist {
|
||||||
|
pub _id: String,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Artist {
|
||||||
|
pub async fn create(name: &str) -> Self {
|
||||||
|
let a = Artist {
|
||||||
|
_id: uuid::Uuid::new_v4().to_string(),
|
||||||
|
name: name.to_string(),
|
||||||
|
};
|
||||||
|
a.insert().await.unwrap();
|
||||||
|
a
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Validate for Artist {
|
||||||
|
async fn validate(&self) -> Result<(), String> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
57
src/library/metadata.rs
Normal file
57
src/library/metadata.rs
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct AudioMetadata(pub serde_json::Value);
|
||||||
|
|
||||||
|
impl Into<mongodb::bson::Bson> for AudioMetadata {
|
||||||
|
fn into(self) -> mongodb::bson::Bson {
|
||||||
|
mongodb::bson::to_bson(&self.0).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AudioMetadata {
|
||||||
|
fn get_key(&self, key: &str) -> Option<&str> {
|
||||||
|
Some(self.0.as_object()?.get(key)?.as_str()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn title(&self) -> Option<&str> {
|
||||||
|
self.get_key("title")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn artist(&self) -> Option<&str> {
|
||||||
|
self.get_key("artist")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn album(&self) -> Option<&str> {
|
||||||
|
self.get_key("album")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn track_number(&self) -> Option<usize> {
|
||||||
|
self.get_key("tracknumber").map(|x| x.parse().ok())?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_metadata(file_path: &str) -> Option<AudioMetadata> {
|
||||||
|
Some(AudioMetadata(get_metadata_json(file_path)?))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_metadata_json(file_path: &str) -> Option<serde_json::Value> {
|
||||||
|
let output = std::process::Command::new("python3")
|
||||||
|
.arg("src/extract_metadata.py")
|
||||||
|
.arg(file_path)
|
||||||
|
.output()
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
serde_json::from_str(&stdout).ok()?
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for AudioMetadata {
|
||||||
|
type Target = serde_json::Value;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
152
src/library/mod.rs
Normal file
152
src/library/mod.rs
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use album::Album;
|
||||||
|
use artist::Artist;
|
||||||
|
use mongod::{Model, Referencable, Reference};
|
||||||
|
use mongodb::bson::doc;
|
||||||
|
use serde_json::json;
|
||||||
|
use track::Track;
|
||||||
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
|
pub mod album;
|
||||||
|
pub mod artist;
|
||||||
|
pub mod metadata;
|
||||||
|
pub mod track;
|
||||||
|
pub mod user;
|
||||||
|
|
||||||
|
/// Checks if a file has a music file extension
|
||||||
|
fn is_music_file(path: &Path) -> bool {
|
||||||
|
if let Some(extension) = path.extension() {
|
||||||
|
match extension.to_str().unwrap_or("").to_lowercase().as_str() {
|
||||||
|
"mp3" | "flac" | "wav" | "aac" | "ogg" | "m4a" | "opus" => return true,
|
||||||
|
_ => return false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Libary {
|
||||||
|
root_dir: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Libary {
|
||||||
|
pub async fn new(root_dir: PathBuf) -> Self {
|
||||||
|
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()
|
||||||
|
} else {
|
||||||
|
Artist::create(artist).await.reference()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
} else {
|
||||||
|
Album::create(album, artist_id).await.reference()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn add_path_to_library(&self, path: &str) {
|
||||||
|
// search for path already present
|
||||||
|
if let Some(_) = Track::find_one_partial(doc! { "path": path }, &json!({})).await {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// add track to library
|
||||||
|
let metadata = metadata::get_metadata(path);
|
||||||
|
|
||||||
|
let mut entry = json!({
|
||||||
|
"_id": uuid::Uuid::new_v4().to_string(),
|
||||||
|
"path": path,
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(meta) = &metadata {
|
||||||
|
if let Some(artist) = meta.artist() {
|
||||||
|
let artist_id = self.find_or_create_artist(artist).await;
|
||||||
|
entry
|
||||||
|
.as_object_mut()
|
||||||
|
.unwrap()
|
||||||
|
.insert("artist_id".into(), artist_id.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(album) = meta.album() {
|
||||||
|
let album_id = self
|
||||||
|
.find_or_create_album(
|
||||||
|
album,
|
||||||
|
entry
|
||||||
|
.as_object()
|
||||||
|
.unwrap()
|
||||||
|
.get("artist_id")
|
||||||
|
.map(|x| x.as_str().unwrap()),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
entry
|
||||||
|
.as_object_mut()
|
||||||
|
.unwrap()
|
||||||
|
.insert("album_id".into(), album_id.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(title) = meta.title() {
|
||||||
|
entry
|
||||||
|
.as_object_mut()
|
||||||
|
.unwrap()
|
||||||
|
.insert("title".into(), title.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(metadata) = metadata {
|
||||||
|
entry
|
||||||
|
.as_object_mut()
|
||||||
|
.unwrap()
|
||||||
|
.insert("meta".into(), metadata.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Track::create(&entry.as_object().unwrap()).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_artists(&self) -> Vec<Artist> {
|
||||||
|
Artist::find(doc! {}).await.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_artist_by_id(&self, id: &str) -> Option<Artist> {
|
||||||
|
Artist::get(id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_albums_by_artist(&self, artist: &str) -> Vec<Album> {
|
||||||
|
let artist = format!("artist::{artist}");
|
||||||
|
Album::find(doc! { "artist_id": artist}).await.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_album_by_id(&self, album: &str) -> Option<Album> {
|
||||||
|
Album::get(album).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_tracks_of_album(&self, album: &str) -> Vec<Track> {
|
||||||
|
Track::find(doc! { "album_id": album}).await.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_track_by_id(&self, track_id: &str) -> Option<Track> {
|
||||||
|
Track::get(track_id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn rescan(&self) {
|
||||||
|
for entry in WalkDir::new(self.root_dir.clone())
|
||||||
|
.follow_links(true)
|
||||||
|
.into_iter()
|
||||||
|
.take(30) // todo : remove
|
||||||
|
.filter_map(|e| e.ok())
|
||||||
|
{
|
||||||
|
let path = entry.path();
|
||||||
|
if path.is_file() && is_music_file(path) {
|
||||||
|
let path = path.to_string_lossy().to_string();
|
||||||
|
|
||||||
|
println!("Found {path}");
|
||||||
|
self.add_path_to_library(&path).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
51
src/library/track.rs
Normal file
51
src/library/track.rs
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
use mongod::{
|
||||||
|
assert_reference_of,
|
||||||
|
derive::{Model, Referencable},
|
||||||
|
Model, Referencable, Reference, Validate,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::library::artist::Artist;
|
||||||
|
|
||||||
|
use super::metadata::AudioMetadata;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Model, Referencable)]
|
||||||
|
pub struct Track {
|
||||||
|
pub _id: String,
|
||||||
|
pub path: String,
|
||||||
|
pub title: String,
|
||||||
|
pub album_id: Option<Reference>,
|
||||||
|
pub artist_id: Option<Reference>,
|
||||||
|
pub meta: Option<AudioMetadata>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Track {
|
||||||
|
pub async fn create(data: &serde_json::Map<String, serde_json::Value>) {
|
||||||
|
let mut t = Self {
|
||||||
|
_id: uuid::Uuid::new_v4().to_string(),
|
||||||
|
path: data.get("path").unwrap().as_str().unwrap().to_string(),
|
||||||
|
title: data.get("title").unwrap().as_str().unwrap().to_string(),
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
100
src/library/user.rs
Normal file
100
src/library/user.rs
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
use data_encoding::HEXUPPER;
|
||||||
|
use mongod::{
|
||||||
|
assert_reference_of, derive::{Model, Referencable}, Model, Referencable, Reference, Validate
|
||||||
|
};
|
||||||
|
use mongodb::bson::doc;
|
||||||
|
use rand::RngCore;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
fn gen_token(token_length: usize) -> String {
|
||||||
|
let mut token_bytes = vec![0u8; token_length];
|
||||||
|
|
||||||
|
rand::thread_rng().fill_bytes(&mut token_bytes);
|
||||||
|
|
||||||
|
HEXUPPER.encode(&token_bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Model, Referencable)]
|
||||||
|
pub struct User {
|
||||||
|
pub _id: String,
|
||||||
|
pub username: String,
|
||||||
|
pub password: String,
|
||||||
|
pub role: UserRole
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub enum UserRole {
|
||||||
|
Regular,
|
||||||
|
Admin
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Validate for User {
|
||||||
|
async fn validate(&self) -> Result<(), String> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl User {
|
||||||
|
pub async fn create(username: &str, password: &str, role: UserRole) -> Option<Self> {
|
||||||
|
if User::find_one_partial(doc! { "username": username }, &json!({})).await.is_some() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let u = User{
|
||||||
|
_id: uuid::Uuid::new_v4().to_string(),
|
||||||
|
username: username.to_string(),
|
||||||
|
password: bcrypt::hash(password, bcrypt::DEFAULT_COST).unwrap(),
|
||||||
|
role
|
||||||
|
};
|
||||||
|
|
||||||
|
u.insert().await.unwrap();
|
||||||
|
|
||||||
|
Some(u)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn login(username: &str, password: &str) -> Option<Session> {
|
||||||
|
let u = User::find_one(doc! { "username": username }).await?;
|
||||||
|
|
||||||
|
if !u.verify_pw(password) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(u.session().await)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn session(&self) -> Session {
|
||||||
|
let s = Session{
|
||||||
|
_id: uuid::Uuid::new_v4().to_string(),
|
||||||
|
token: gen_token(60),
|
||||||
|
user: self.reference()
|
||||||
|
};
|
||||||
|
|
||||||
|
s.insert().await.unwrap();
|
||||||
|
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_admin(&self) -> bool {
|
||||||
|
matches!(self.role, UserRole::Admin)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify_pw(&self, password: &str) -> bool {
|
||||||
|
bcrypt::verify(password, &self.password).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Model, Referencable)]
|
||||||
|
pub struct Session {
|
||||||
|
pub _id: String,
|
||||||
|
pub token: String,
|
||||||
|
pub user: Reference
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Validate for Session {
|
||||||
|
async fn validate(&self) -> Result<(), String> {
|
||||||
|
assert_reference_of!(self.user, User);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
43
src/main.rs
Normal file
43
src/main.rs
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
use library::Libary;
|
||||||
|
|
||||||
|
mod library;
|
||||||
|
mod route;
|
||||||
|
|
||||||
|
use rocket::routes;
|
||||||
|
use rocket::{http::Method, launch};
|
||||||
|
|
||||||
|
#[launch]
|
||||||
|
async fn rocket() -> _ {
|
||||||
|
let cors = rocket_cors::CorsOptions {
|
||||||
|
allowed_origins: rocket_cors::AllowedOrigins::all(),
|
||||||
|
allowed_methods: vec![Method::Get, Method::Post, Method::Options]
|
||||||
|
.into_iter()
|
||||||
|
.map(From::from)
|
||||||
|
.collect(),
|
||||||
|
allowed_headers: rocket_cors::AllowedHeaders::all(),
|
||||||
|
allow_credentials: true,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.to_cors()
|
||||||
|
.expect("error creating CORS options");
|
||||||
|
|
||||||
|
let lib = Libary::new("/Users/angelo/Downloads/Music".into()).await;
|
||||||
|
|
||||||
|
lib.rescan().await;
|
||||||
|
|
||||||
|
rocket::build()
|
||||||
|
.mount(
|
||||||
|
"/",
|
||||||
|
routes![
|
||||||
|
route::artist::artists_route,
|
||||||
|
route::artist::artist_route,
|
||||||
|
route::album::albums_route,
|
||||||
|
route::album::album_route,
|
||||||
|
route::track::track_route,
|
||||||
|
route::track::track_audio_route,
|
||||||
|
route::album::album_cover_route
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.manage(lib)
|
||||||
|
.attach(cors)
|
||||||
|
}
|
83
src/route/album.rs
Normal file
83
src/route/album.rs
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
|
use mongodb::bson::doc;
|
||||||
|
use rocket::*;
|
||||||
|
use serde_json::json;
|
||||||
|
use rocket::fs::NamedFile;
|
||||||
|
use mongod::Referencable;
|
||||||
|
use super::FallibleApiResponse;
|
||||||
|
use super::api_error;
|
||||||
|
|
||||||
|
use crate::library::Libary;
|
||||||
|
|
||||||
|
#[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;
|
||||||
|
|
||||||
|
Ok(json!({
|
||||||
|
"artist": artist_id,
|
||||||
|
"albums": &albums
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sort_by_tracknumber(a: &serde_json::Value, b: &serde_json::Value) -> Ordering {
|
||||||
|
a.get("tracknumber")
|
||||||
|
.unwrap()
|
||||||
|
.as_i64()
|
||||||
|
.unwrap()
|
||||||
|
.cmp(&b.get("tracknumber").unwrap().as_i64().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/album/<album_id>/cover")]
|
||||||
|
pub async fn album_cover_route(album_id: &str, lib: &State<Libary>) -> Option<NamedFile> {
|
||||||
|
let album = lib.get_album_by_id(album_id).await?;
|
||||||
|
|
||||||
|
let track_path = lib
|
||||||
|
.get_tracks_of_album(&format!("album::{}", album._id))
|
||||||
|
.await
|
||||||
|
.first()?
|
||||||
|
.path
|
||||||
|
.clone();
|
||||||
|
let track_path = std::path::Path::new(&track_path);
|
||||||
|
|
||||||
|
for ext in ["png", "jpg", "jpeg", "avif"] {
|
||||||
|
let cover_file = track_path.parent()?.join(format!("cover.{ext}"));
|
||||||
|
|
||||||
|
if cover_file.exists() {
|
||||||
|
return NamedFile::open(cover_file).await.ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/album/<album_id>")]
|
||||||
|
pub async fn album_route(album_id: &str, lib: &State<Libary>) -> FallibleApiResponse {
|
||||||
|
let album = lib
|
||||||
|
.get_album_by_id(album_id)
|
||||||
|
.await
|
||||||
|
.ok_or_else(|| api_error("No album with that ID found"))?;
|
||||||
|
|
||||||
|
let mut tracks = lib
|
||||||
|
.get_tracks_of_album(&format!("album::{}", album._id))
|
||||||
|
.await
|
||||||
|
.into_iter()
|
||||||
|
.map(|x| {
|
||||||
|
json!({
|
||||||
|
"id": x.id(),
|
||||||
|
"title": x.title,
|
||||||
|
"tracknumber": x.meta.map(|x| x.track_number())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
tracks.sort_by(sort_by_tracknumber);
|
||||||
|
|
||||||
|
let mut album = serde_json::to_value(album).unwrap();
|
||||||
|
album
|
||||||
|
.as_object_mut()
|
||||||
|
.unwrap()
|
||||||
|
.insert("tracks".into(), tracks.into());
|
||||||
|
|
||||||
|
Ok(album)
|
||||||
|
}
|
23
src/route/artist.rs
Normal file
23
src/route/artist.rs
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
use mongodb::bson::doc;
|
||||||
|
use rocket::*;
|
||||||
|
use super::FallibleApiResponse;
|
||||||
|
use super::api_error;
|
||||||
|
|
||||||
|
use crate::library::Libary;
|
||||||
|
|
||||||
|
/// Get all artists
|
||||||
|
#[get("/artists")]
|
||||||
|
pub async fn artists_route(lib: &State<Libary>) -> FallibleApiResponse {
|
||||||
|
let artists = lib.get_artists().await;
|
||||||
|
Ok(serde_json::to_value(&artists).unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/artist/<id>")]
|
||||||
|
pub async fn artist_route(id: &str, lib: &State<Libary>) -> FallibleApiResponse {
|
||||||
|
Ok(serde_json::to_value(
|
||||||
|
&lib.get_artist_by_id(id)
|
||||||
|
.await
|
||||||
|
.ok_or_else(|| api_error("No artist with that ID found"))?,
|
||||||
|
)
|
||||||
|
.unwrap())
|
||||||
|
}
|
17
src/route/mod.rs
Normal file
17
src/route/mod.rs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
use rocket::response::status::BadRequest;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
pub mod artist;
|
||||||
|
pub mod album;
|
||||||
|
pub mod track;
|
||||||
|
pub mod user;
|
||||||
|
|
||||||
|
|
||||||
|
type ApiError = BadRequest<serde_json::Value>;
|
||||||
|
type FallibleApiResponse = Result<serde_json::Value, ApiError>;
|
||||||
|
|
||||||
|
pub fn api_error(msg: &str) -> ApiError {
|
||||||
|
BadRequest(json!({
|
||||||
|
"error": msg
|
||||||
|
}))
|
||||||
|
}
|
25
src/route/track.rs
Normal file
25
src/route/track.rs
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
use rocket::*;
|
||||||
|
use fs::NamedFile;
|
||||||
|
use mongodb::bson::doc;
|
||||||
|
use super::FallibleApiResponse;
|
||||||
|
use super::api_error;
|
||||||
|
|
||||||
|
use crate::library::Libary;
|
||||||
|
|
||||||
|
#[get("/track/<track_id>")]
|
||||||
|
pub async fn track_route(track_id: &str, lib: &State<Libary>) -> FallibleApiResponse {
|
||||||
|
Ok(serde_json::to_value(
|
||||||
|
&lib.get_track_by_id(track_id)
|
||||||
|
.await
|
||||||
|
.ok_or_else(|| api_error("No track with that ID found"))?,
|
||||||
|
)
|
||||||
|
.unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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?;
|
||||||
|
NamedFile::open(std::path::Path::new(&track.path))
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
}
|
51
src/route/user.rs
Normal file
51
src/route/user.rs
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
|
||||||
|
use rocket::http::Status;
|
||||||
|
use mongod::Model;
|
||||||
|
use mongodb::bson::doc;
|
||||||
|
use rocket::outcome::Outcome;
|
||||||
|
use rocket::post;
|
||||||
|
use rocket::request::FromRequest;
|
||||||
|
use rocket::Request;
|
||||||
|
use serde_json::json;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use rocket::serde::json::Json;
|
||||||
|
use crate::library::user::Session;
|
||||||
|
use crate::library::user::User;
|
||||||
|
|
||||||
|
use super::FallibleApiResponse;
|
||||||
|
use super::api_error;
|
||||||
|
|
||||||
|
#[rocket::async_trait]
|
||||||
|
impl<'r> FromRequest<'r> for User {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
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;
|
||||||
|
Outcome::Success(user)
|
||||||
|
} else {
|
||||||
|
Outcome::Error((Status::Unauthorized, ()))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => Outcome::Error((Status::Unauthorized, ())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct LoginData {
|
||||||
|
pub username: String,
|
||||||
|
pub password: String
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/login", data = "<login>")]
|
||||||
|
pub async fn login_route(login: Json<LoginData>) -> FallibleApiResponse {
|
||||||
|
let ses = User::login(&login.username, &login.password).await.ok_or_else(|| api_error("Login failed"))?;
|
||||||
|
|
||||||
|
Ok(json!({
|
||||||
|
"token": ses.token
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue