finish postgres

This commit is contained in:
JMARyA 2024-10-04 14:38:35 +02:00
parent 7b7e1a4014
commit 08e24f63f4
Signed by: jmarya
GPG key ID: 901B2ADDF27C2263
16 changed files with 454 additions and 306 deletions

View file

@ -1,3 +1,4 @@
use crate::route::ToAPI;
use mongodb::bson::doc;
use serde::{Deserialize, Serialize};
use serde_json::json;
@ -15,7 +16,7 @@ pub struct Album {
}
impl Album {
pub async fn create(title: &str, artist: Option<&str>) -> Self {
pub async fn create(title: &str, artist: Option<uuid::Uuid>) -> Self {
sqlx::query_as("INSERT INTO album (title, artist) VALUES ($1, $2) RETURNING *")
.bind(title)
.bind(artist)
@ -24,6 +25,52 @@ impl Album {
.unwrap()
}
pub async fn find_all() -> Vec<Self> {
sqlx::query_as("SELECT * FROM album")
.fetch_all(get_pg!())
.await
.unwrap()
}
pub async fn find(title: &str, artist: Option<uuid::Uuid>) -> Option<Self> {
sqlx::query_as("SELECT * FROM album WHERE title = $1 AND artist = $2")
.bind(title)
.bind(artist)
.fetch_optional(get_pg!())
.await
.unwrap()
}
pub async fn find_regex_col(col: &str, query: &str) -> Vec<Self> {
sqlx::query_as(&format!("SELECT * FROM album WHERE {col} ~* $1"))
.bind(query)
.fetch_all(get_pg!())
.await
.unwrap()
}
pub async fn find_of_artist(artist: &uuid::Uuid) -> Vec<Self> {
sqlx::query_as("SELECT * FROM album WHERE artist = $1")
.bind(artist)
.fetch_all(get_pg!())
.await
.unwrap()
}
pub async fn remove(&self) {
sqlx::query("DELETE FROM album WHERE id = $1")
.bind(self.id)
.fetch(get_pg!());
}
pub async fn get(id: &uuid::Uuid) -> Option<Self> {
sqlx::query_as("SELECT * FROM album WHERE id = $1")
.bind(id)
.fetch_optional(get_pg!())
.await
.unwrap()
}
pub async fn get_tracks_of_album(album: &uuid::Uuid) -> Vec<Track> {
sqlx::query_as("SELECT * FROM track WHERE album = $1")
.bind(album)
@ -55,7 +102,7 @@ impl Album {
}
}
impl Album {
impl ToAPI for Album {
async fn api(&self) -> serde_json::Value {
json!({
"id": &self.id,

View file

@ -1,3 +1,4 @@
use crate::route::ToAPI;
use serde::{Deserialize, Serialize};
use serde_json::json;
use sqlx::FromRow;
@ -21,14 +22,48 @@ impl Artist {
.unwrap()
}
pub async fn get(id: &uuid::Uuid) -> Option<Self> {
sqlx::query_as("SELECT * FROM artist WHERE id = $1")
.bind(id)
.fetch_optional(get_pg!())
.await
.unwrap()
}
pub async fn find(name: &str) -> Option<Self> {
sqlx::query_as("SELECT * FROM artist WHERE name = $1")
.bind(name)
.fetch_optional(get_pg!())
.await
.unwrap()
}
pub async fn find_all() -> Vec<Self> {
sqlx::query_as("SELECT * FROM artist")
.fetch_all(get_pg!())
.await
.unwrap()
}
pub async fn find_regex_col(col: &str, query: &str) -> Vec<Self> {
sqlx::query_as(&format!("SELECT * FROM artist WHERE {col} ~* $1"))
.bind(query)
.fetch_all(get_pg!())
.await
.unwrap()
}
pub async fn remove(&self) {
sqlx::query("DELETE FROM artist WHERE id = $1")
.bind(self.id)
.fetch(get_pg!());
}
/// 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.
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)})
.await?
.path;
let track_path = Track::find_first_of_artist(id).await?.path;
let track_path = std::path::Path::new(&track_path);
let artist_path = track_path.parent()?.parent()?;
@ -45,7 +80,7 @@ impl Artist {
}
}
impl Artist {
impl ToAPI for Artist {
async fn api(&self) -> serde_json::Value {
json!({
"id": &self.id,

View file

@ -1,14 +1,16 @@
use std::path::{Path, PathBuf};
use std::{
path::{Path, PathBuf},
str::FromStr,
};
use album::Album;
use artist::Artist;
use mongod::{reference_of, Model, Referencable, Reference};
use mongodb::bson::doc;
use serde_json::json;
use track::Track;
use walkdir::WalkDir;
use crate::cache::RouteCache;
use crate::{cache::RouteCache, get_pg};
pub mod album;
pub mod artist;
@ -39,28 +41,29 @@ impl Libary {
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()
pub async fn find_or_create_artist(&self, artist: &str) -> uuid::Uuid {
if let Some(artist) = Artist::find(artist).await {
artist.id
} else {
Artist::create(artist).await.reference()
Artist::create(artist).await.id
}
}
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()
pub async fn find_or_create_album(
&self,
album: &str,
artist_id: Option<uuid::Uuid>,
) -> uuid::Uuid {
if let Some(album) = Album::find(album, artist_id).await {
album.id
} else {
Album::create(album, artist_id).await.reference()
Album::create(album, artist_id).await.id
}
}
pub async fn add_path_to_library(&self, path: &str) {
// search for path already present
if Track::find_one_partial(doc! { "path": path }, json!({}))
.await
.is_some()
{
if Track::of_path(path).await.is_some() {
return;
}
@ -70,7 +73,6 @@ impl Libary {
let metadata = metadata::get_metadata(path);
let mut entry = json!({
"_id": uuid::Uuid::new_v4().to_string(),
"path": path,
});
@ -80,7 +82,7 @@ impl Libary {
entry
.as_object_mut()
.unwrap()
.insert("artist_id".into(), artist_id.into());
.insert("artist".into(), artist_id.to_string().into());
} else {
log::warn!("{path} has no artist");
}
@ -92,14 +94,15 @@ impl Libary {
entry
.as_object()
.unwrap()
.get("artist_id")
.map(|x| x.as_str().unwrap()),
.get("artist")
.map(|x| uuid::Uuid::from_str(x.as_str().unwrap()).unwrap()),
)
.await;
entry
.as_object_mut()
.unwrap()
.insert("album_id".into(), album_id.into());
.insert("album".into(), album_id.to_string().into());
} else {
log::warn!("{path} has no album and will be treated as single");
}
@ -136,41 +139,37 @@ impl Libary {
}
pub async fn get_artists(&self) -> Vec<Artist> {
Artist::find_all().await.unwrap()
sqlx::query_as("SELECT * FROM artist")
.fetch_all(get_pg!())
.await
.unwrap()
}
pub async fn get_albums_by_artist(&self, artist: &str) -> Vec<Album> {
Album::find(
doc! { "artist_id": reference_of!(Artist, artist).unwrap()},
None,
None,
)
.await
.unwrap()
pub async fn get_albums_by_artist(&self, artist: &uuid::Uuid) -> Vec<Album> {
sqlx::query_as("SELECT * FROM album WHERE artist = $1")
.bind(artist)
.fetch_all(get_pg!())
.await
.unwrap()
}
pub async fn get_singles_by_artist(&self, artist: &str) -> Vec<Track> {
Track::find(
doc! {
"album_id": None::<String>,
"artist_id": reference_of!(Artist, artist).unwrap()
},
None,
None,
)
.await
.unwrap()
pub async fn get_singles_by_artist(&self, artist: &uuid::Uuid) -> Vec<Track> {
sqlx::query_as("SELECT * FROM track WHERE album IS NULL AND artist = $1")
.bind(artist)
.fetch_all(get_pg!())
.await
.unwrap()
}
pub async fn get_album_by_id(&self, album: &str) -> Option<Album> {
pub async fn get_album_by_id(&self, album: &uuid::Uuid) -> Option<Album> {
Album::get(album).await
}
pub async fn get_track_by_id(&self, track_id: &str) -> Option<Track> {
pub async fn get_track_by_id(&self, track_id: &uuid::Uuid) -> Option<Track> {
Track::get(track_id).await
}
pub async fn reload_metadata(&self, track_id: &str) -> Result<(), ()> {
pub async fn reload_metadata(&self, track_id: &uuid::Uuid) -> Result<(), ()> {
let mut track = Track::get(track_id).await.ok_or_else(|| ())?;
let path = &track.path;
log::info!("Rescanning metadata for {path}");
@ -185,7 +184,7 @@ impl Libary {
update
.as_object_mut()
.unwrap()
.insert("artist_id".into(), artist_id.into());
.insert("artist".into(), artist_id.to_string().into());
} else {
log::warn!("{path} has no artist");
}
@ -197,14 +196,14 @@ impl Libary {
update
.as_object()
.unwrap()
.get("artist_id")
.map(|x| x.as_str().unwrap()),
.get("artist")
.map(|x| uuid::Uuid::from_str(x.as_str().unwrap()).unwrap()),
)
.await;
update
.as_object_mut()
.unwrap()
.insert("album_id".into(), album_id.into());
.insert("album".into(), album_id.to_string().into());
} else {
log::warn!("{path} has no album and will be treated as single");
}
@ -244,66 +243,26 @@ impl Libary {
pub async fn clean_lost_files(&self) {
// tracks
for track in Track::find_partial(doc! {}, json!({"path": 1}), None, None)
.await
.unwrap()
{
if !std::path::Path::new(&track.path.as_ref().unwrap()).exists() {
log::info!("Cleaning lost {}", track.path.as_ref().unwrap());
Track::remove(&track._id).await.unwrap();
for track in Track::find_all().await {
if !std::path::Path::new(&track.path).exists() {
log::info!("Cleaning lost {}", track.path);
track.remove().await;
}
}
// albums
for album in Album::find_partial(doc! {}, json!({"title": 1}), None, None)
.await
.unwrap()
{
if Track::find_partial(
doc! { "album_id": album.reference() },
json!({}),
None,
None,
)
.await
.unwrap()
.is_empty()
{
log::info!(
"Cleaning album {} with no tracks",
album.title.as_ref().unwrap()
);
Album::remove(album.id()).await.unwrap();
for album in Album::find_all().await {
if Track::find_of_album(&album.id).await.is_empty() {
log::info!("Cleaning album {} with no tracks", album.title);
album.remove().await;
}
}
// artists
for artist in Artist::find_partial(doc! {}, json!({"name": 1}), None, None)
.await
.unwrap()
{
if Track::find_partial(
doc! { "artist_id": artist.reference()},
json!({}),
None,
None,
)
.await
.unwrap()
.is_empty()
&& Album::find_partial(
doc! { "artist_id": artist.reference()},
json!({}),
None,
None,
)
.await
.unwrap()
.is_empty()
for artist in Artist::find_all().await {
if Track::find_first_of_artist(&artist.id).await.is_none()
&& Album::find_of_artist(&artist.id).await.is_empty()
{
log::info!(
"Cleaning artist {} with no tracks or albums",
artist.name.as_ref().unwrap()
);
Artist::remove(artist.id()).await.unwrap();
log::info!("Cleaning artist {} with no tracks or albums", artist.name);
artist.remove().await;
}
}
}

View file

@ -1,19 +1,25 @@
use mongod::{
assert_reference_of,
derive::{Model, Referencable},
reference_of, Model, Referencable, Reference, Validate,
};
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use crate::library::{track::Track, user::User};
use mongod::ToAPI;
#[derive(Debug, Clone, Serialize, Deserialize, Model, Referencable)]
use crate::{
get_pg,
library::{track::Track, user::User},
};
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct Playlist {
pub _id: String,
pub owner: Reference,
pub id: String,
pub owner: String,
pub title: String,
pub visibility: Visibility,
pub tracks: Vec<Reference>,
pub tracks: Vec<uuid::Uuid>,
}
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::Type)]
#[sqlx(type_name = "visibility", rename_all = "lowercase")]
pub enum Visibility {
Private,
Public,
}
impl Playlist {
@ -21,50 +27,49 @@ impl Playlist {
owner: User,
title: &str,
visibility: Visibility,
tracks: &[String],
tracks: &[uuid::Uuid],
) -> Option<Self> {
let mut tracks_ref = vec![];
sqlx::query_as("INSERT INTO playlist (owner, title, visibility, tracks) VALUES ($1, $2, $3, $4) RETURNING *")
.bind(owner.username)
.bind(title)
.bind(visibility)
.bind(tracks)
.fetch_one(get_pg!()).await.ok()
}
for track in tracks {
tracks_ref.push(reference_of!(Track, track)?);
}
pub async fn find_regex_col(col: &str, query: &str) -> Vec<Self> {
sqlx::query_as(&format!("SELECT * FROM playlist WHERE {col} ~* $1"))
.bind(query)
.fetch_all(get_pg!())
.await
.unwrap()
}
Some(Self {
_id: uuid::Uuid::new_v4().to_string(),
owner: owner.reference(),
title: title.to_string(),
visibility,
tracks: tracks_ref,
})
pub async fn get(id: &uuid::Uuid) -> Option<Self> {
sqlx::query_as("SELECT * FROM playlist WHERE id = $1")
.bind(id)
.fetch_optional(get_pg!())
.await
.unwrap()
}
pub async fn of(u: &User) -> Vec<Self> {
sqlx::query_as("SELECT * FROM playlist WHERE owner = $1")
.bind(&u.username)
.fetch_all(get_pg!())
.await
.unwrap()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum Visibility {
Private,
Public,
}
impl Validate for Playlist {
async fn validate(&self) -> Result<(), String> {
assert_reference_of!(self.owner, User);
for track in &self.tracks {
assert_reference_of!(track, Track);
}
Ok(())
}
}
impl ToAPI for Playlist {
async fn api(&self) -> serde_json::Value {
impl Playlist {
pub async fn api(&self) -> serde_json::Value {
serde_json::json!({
"id": self._id,
"owner": self.owner.id(),
"id": self.id,
"owner": self.owner,
"visibility": serde_json::to_value(&self.visibility).unwrap(),
"title": self.title,
"tracks": self.tracks.iter().map(mongod::Reference::id).collect::<Vec<_>>()
"tracks": self.tracks
})
}
}

View file

@ -1,10 +1,7 @@
use serde::Serialize;
use std::cmp::Ordering;
use mongod::Model;
use mongodb::bson::doc;
use serde::Serialize;
use mongod::ToAPI;
use crate::route::ToAPI;
use super::{album::Album, artist::Artist, playlist::Playlist, track::Track};
@ -74,13 +71,7 @@ pub async fn search_for(query: String) -> Option<Vec<SearchResult>> {
let mut results: Vec<SearchResult> = Vec::new();
// Add artist results
for artist in Artist::find(
doc! { "name": { "$regex": &query, "$options": "i" } },
None,
None,
)
.await?
{
for artist in Artist::find_regex_col("name", &query).await {
results.push(SearchResult {
kind: "artist".to_string(),
data: artist.api().await,
@ -89,13 +80,7 @@ pub async fn search_for(query: String) -> Option<Vec<SearchResult>> {
}
// Add album results
for album in Album::find(
doc! { "title": { "$regex": &query, "$options": "i" } },
None,
None,
)
.await?
{
for album in Album::find_regex_col("title", &query).await {
results.push(SearchResult {
kind: "album".to_string(),
data: album.api().await,
@ -104,28 +89,16 @@ pub async fn search_for(query: String) -> Option<Vec<SearchResult>> {
}
// Add track results
for track in Track::find(
doc! { "title": { "$regex": &query, "$options": "i" } },
None,
None,
)
.await?
{
for track in Track::find_regex_col("title", &query).await {
results.push(SearchResult {
kind: "track".to_string(),
data: track.api().await,
score: calculate_score(&track.title, &query, Some(track.date_added)),
score: calculate_score(&track.title, &query, Some(track.date_added.timestamp())),
});
}
// Add playlist results
for playlist in Playlist::find(
doc! { "title": { "$regex": &query, "$options": "i" } },
None,
None,
)
.await?
{
for playlist in Playlist::find_regex_col("title", &query).await {
results.push(SearchResult {
kind: "playlist".to_string(),
data: playlist.api().await,

View file

@ -1,11 +1,12 @@
use serde::{Deserialize, Serialize};
use serde_json::json;
use sqlx::prelude::FromRow;
use std::collections::HashSet;
use std::{collections::HashSet, str::FromStr};
use crate::{
get_pg,
library::{album::Album, artist::Artist},
route::ToAPI,
};
use super::{event::Event, metadata::AudioMetadata, user::User};
@ -30,6 +31,75 @@ impl Track {
.fetch(get_pg!());
}
pub async fn of_path(path: &str) -> Option<Self> {
sqlx::query_as("SELECT * FROM track WHERE path = $1")
.bind(path)
.fetch_optional(get_pg!())
.await
.unwrap()
}
pub async fn find_recently_added() -> Vec<Self> {
sqlx::query_as("SELECT * FROM track ORDER BY date_added DESC LIMIT 90")
.fetch_all(get_pg!())
.await
.unwrap()
}
pub async fn get(id: &uuid::Uuid) -> Option<Self> {
sqlx::query_as("SELECT * FROM track WHERE id = $1")
.bind(id)
.fetch_optional(get_pg!())
.await
.unwrap()
}
pub async fn update(&self, update_set: &serde_json::Value) -> Option<()> {
let map = update_set.as_object()?;
let artist = map
.get("artist")
.map(|x| uuid::Uuid::from_str(x.as_str().unwrap()).unwrap());
let album = map
.get("album")
.map(|x| uuid::Uuid::from_str(x.as_str().unwrap()).unwrap());
let title = map.get("title")?.as_str()?;
let meta = map.get("meta");
sqlx::query(
"UPDATE track SET artist = $1, album = $2, title = $3, meta = $4 WHERE id = $5;
",
)
.bind(artist)
.bind(album)
.bind(title)
.bind(meta)
.bind(self.id)
.fetch(get_pg!());
Some(())
}
pub async fn find_all() -> Vec<Self> {
sqlx::query_as("SELECT * FROM track")
.fetch_all(get_pg!())
.await
.unwrap()
}
pub async fn remove(&self) {
sqlx::query("DELETE FROM track WHERE id = $1")
.bind(self.id)
.fetch(get_pg!());
}
pub async fn find_of_album(album: &uuid::Uuid) -> Vec<Self> {
sqlx::query_as("SELECT * FROM track WHERE album = $1")
.bind(album)
.fetch_all(get_pg!())
.await
.unwrap()
}
pub async fn get_latest_of_user(u: &User) -> Vec<Self> {
let latest_events = Event::get_latest_events_of(u).await;
let mut ids = HashSet::new();
@ -59,13 +129,13 @@ impl Track {
/// Transcode audio
pub fn transcode(&self, codec: &str, bitrate: u32, ext: &str) -> Option<String> {
let transcoded = format!("./data/transcode/{codec}/{bitrate}/{}.{ext}", self._id);
let transcoded = format!("./data/transcode/{codec}/{bitrate}/{}.{ext}", self.id);
if std::path::Path::new(&transcoded).exists() {
return Some(transcoded);
}
log::info!("Transcoding {} to {} {}", self._id, codec, bitrate);
log::info!("Transcoding {} to {} {}", self.id, codec, bitrate);
std::fs::create_dir_all(format!("./data/transcode/{codec}/{bitrate}")).unwrap();
@ -87,38 +157,57 @@ impl Track {
Some(transcoded)
}
pub async fn find_regex_col(col: &str, query: &str) -> Vec<Self> {
sqlx::query_as(&format!("SELECT * FROM track WHERE {col} ~* $1"))
.bind(query)
.fetch_all(get_pg!())
.await
.unwrap()
}
/// Find tracks with no album or artist
pub async fn get_orphans() -> Vec<Track> {
// todo : fix
Self::find(
doc! {
"artist_id": None::<String>,
"album_id": None::<String>
},
None,
None,
)
.await
.unwrap()
sqlx::query_as("SELECT * FROM track WHERE artist IS NULL AND album IS NULL")
.fetch_all(get_pg!())
.await
.unwrap()
}
pub async fn find_first_of_artist(artist: &uuid::Uuid) -> Option<Self> {
sqlx::query_as("SELECT * FROM track WHERE artist = $1 LIMIT 1")
.bind(artist)
.fetch_optional(get_pg!())
.await
.unwrap()
}
pub async fn find_first_of_album(album: &uuid::Uuid) -> Option<Self> {
sqlx::query_as("SELECT * FROM track WHERE album = $1 LIMIT 1")
.bind(album)
.fetch_optional(get_pg!())
.await
.unwrap()
}
}
impl Track {
impl ToAPI for Track {
async fn api(&self) -> serde_json::Value {
// 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::get(album_ref).await.unwrap();
(album.get_cover().await.is_some(), album.title, album._id)
(album.get_cover().await.is_some(), album.title, album.id)
} else {
(false, String::new(), String::new())
(false, String::new(), uuid::Uuid::nil())
};
let artist_title = if let Some(artist_ref) = &self.artist {
artist_ref
.get_partial::<Artist>(json!({"name": 1}))
let res: (String,) = sqlx::query_as("SELECT name FROM artist WHERE id = $1")
.bind(artist_ref)
.fetch_one(get_pg!())
.await
.name
.unwrap();
Some(res.0)
} else {
None
};
@ -126,7 +215,7 @@ impl Track {
json!({
"id": self.id,
"title": self.title,
"track_number": self.meta.as_ref().map(|x| AudioMetadata(*x).track_number()),
"track_number": self.meta.as_ref().map(|x| AudioMetadata(x.clone()).track_number()),
"meta": serde_json::to_value(&self.meta).unwrap(),
"album_id": self.album,
"album": album_title,

View file

@ -1,3 +1,4 @@
use crate::route::ToAPI;
use data_encoding::HEXUPPER;
use rand::RngCore;
use serde::{Deserialize, Serialize};
@ -81,6 +82,13 @@ impl User {
Err(())
}
pub async fn find_all() -> Vec<Self> {
sqlx::query_as("SELECT * FROM user")
.fetch_all(get_pg!())
.await
.unwrap()
}
pub async fn session(&self) -> Session {
sqlx::query_as(
"INSERT INTO user_session (token, user) VALUES ($1, $2) RETURNING id, token, user",
@ -93,7 +101,7 @@ impl User {
}
pub const fn is_admin(&self) -> bool {
matches!(self.role, UserRole::Admin)
matches!(self.user_role, UserRole::Admin)
}
pub fn verify_pw(&self, password: &str) -> bool {
@ -101,7 +109,7 @@ impl User {
}
}
impl User {
impl ToAPI for User {
async fn api(&self) -> serde_json::Value {
json!({
"username": self.username,