This commit is contained in:
JMARyA 2024-07-24 11:07:24 +02:00
parent 0d3df6bb64
commit dcf546fa9c
Signed by: jmarya
GPG key ID: 901B2ADDF27C2263
18 changed files with 3463 additions and 1 deletions

41
src/library/album.rs Normal file
View 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
View 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
View 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
View 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
View 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
View 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(())
}
}