init
This commit is contained in:
parent
0d3df6bb64
commit
dcf546fa9c
18 changed files with 3463 additions and 1 deletions
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(())
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue