Grouping and filtering music files
This commit is contained in:
@@ -1,11 +1,14 @@
|
|||||||
#![allow(clippy::missing_errors_doc)]
|
#![allow(clippy::missing_errors_doc)]
|
||||||
#![allow(clippy::unnecessary_struct_initialization)]
|
#![allow(clippy::unnecessary_struct_initialization)]
|
||||||
#![allow(clippy::unused_async)]
|
#![allow(clippy::unused_async)]
|
||||||
|
use axum::{debug_handler, extract::Query};
|
||||||
use loco_rs::prelude::*;
|
use loco_rs::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use axum::debug_handler;
|
|
||||||
|
|
||||||
use crate::models::_entities::music_files::{ActiveModel, Entity, Model};
|
use crate::models::{
|
||||||
|
_entities::music_files::{ActiveModel, Entity, Model},
|
||||||
|
music_files::MusicFilter,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct Params {
|
pub struct Params {
|
||||||
@@ -26,14 +29,25 @@ impl Params {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum GroupByColumn {
|
||||||
|
Title,
|
||||||
|
Artist,
|
||||||
|
Album,
|
||||||
|
}
|
||||||
|
|
||||||
async fn load_item(ctx: &AppContext, id: i32) -> Result<Model> {
|
async fn load_item(ctx: &AppContext, id: i32) -> Result<Model> {
|
||||||
let item = Entity::find_by_id(id).one(&ctx.db).await?;
|
let item = Entity::find_by_id(id).one(&ctx.db).await?;
|
||||||
item.ok_or_else(|| Error::NotFound)
|
item.ok_or_else(|| Error::NotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[debug_handler]
|
#[debug_handler]
|
||||||
pub async fn list(State(ctx): State<AppContext>) -> Result<Response> {
|
pub async fn list(
|
||||||
format::json(Entity::find().all(&ctx.db).await?)
|
State(ctx): State<AppContext>,
|
||||||
|
Query(filter): Query<MusicFilter>,
|
||||||
|
) -> Result<Response> {
|
||||||
|
format::json(Model::filter_by(&ctx.db, filter).await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[debug_handler]
|
#[debug_handler]
|
||||||
@@ -70,6 +84,34 @@ pub async fn get_one(Path(id): Path<i32>, State(ctx): State<AppContext>) -> Resu
|
|||||||
format::json(load_item(&ctx, id).await?)
|
format::json(load_item(&ctx, id).await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn group_by(
|
||||||
|
Path(column): Path<GroupByColumn>,
|
||||||
|
State(ctx): State<AppContext>,
|
||||||
|
Query(filter): Query<MusicFilter>,
|
||||||
|
) -> Result<Response> {
|
||||||
|
use GroupByColumn::*;
|
||||||
|
|
||||||
|
let rows = Model::filter_by(&ctx.db, filter).await?;
|
||||||
|
|
||||||
|
let mut grouped: std::collections::HashMap<String, Vec<Model>> =
|
||||||
|
std::collections::HashMap::new();
|
||||||
|
|
||||||
|
for row in rows {
|
||||||
|
let key = match column {
|
||||||
|
Title => row.title.clone().unwrap_or_else(|| "<unknown>".to_string()),
|
||||||
|
Artist => row
|
||||||
|
.artist
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| "<unknown>".to_string()),
|
||||||
|
Album => row.album.clone().unwrap_or_else(|| "<unknown>".to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
grouped.entry(key).or_default().push(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
format::json(grouped)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn routes() -> Routes {
|
pub fn routes() -> Routes {
|
||||||
Routes::new()
|
Routes::new()
|
||||||
.prefix("api/music_files/")
|
.prefix("api/music_files/")
|
||||||
@@ -79,4 +121,5 @@ pub fn routes() -> Routes {
|
|||||||
.add("{id}", delete(remove))
|
.add("{id}", delete(remove))
|
||||||
.add("{id}", put(update))
|
.add("{id}", put(update))
|
||||||
.add("{id}", patch(update))
|
.add("{id}", patch(update))
|
||||||
|
.add("/group_by/{column}", get(group_by))
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
use loco_rs::prelude::*;
|
use loco_rs::prelude::*;
|
||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::{entity::prelude::*, Condition};
|
||||||
|
use serde::{Deserialize, Deserializer};
|
||||||
|
|
||||||
use crate::services::suggestion::MetadataFields;
|
use crate::{models::_entities::music_files, services::suggestion::MetadataFields};
|
||||||
|
|
||||||
use lofty::{
|
use lofty::{
|
||||||
config::WriteOptions,
|
config::WriteOptions,
|
||||||
@@ -15,6 +16,34 @@ use tracing::error;
|
|||||||
pub use super::_entities::music_files::{ActiveModel, Entity, Model};
|
pub use super::_entities::music_files::{ActiveModel, Entity, Model};
|
||||||
pub type MusicFiles = Entity;
|
pub type MusicFiles = Entity;
|
||||||
|
|
||||||
|
pub enum MatchValue {
|
||||||
|
Exact(String),
|
||||||
|
Partial(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for MatchValue {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<MatchValue, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let s = String::deserialize(deserializer)?;
|
||||||
|
if let Some(stripped) = s.strip_prefix("exact:") {
|
||||||
|
Ok(MatchValue::Exact(stripped.to_string()))
|
||||||
|
} else if let Some(stripped) = s.strip_prefix("partial:") {
|
||||||
|
Ok(MatchValue::Partial(stripped.to_string()))
|
||||||
|
} else {
|
||||||
|
Ok(MatchValue::Partial(s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct MusicFilter {
|
||||||
|
pub title: Option<MatchValue>,
|
||||||
|
pub artist: Option<MatchValue>,
|
||||||
|
pub album: Option<MatchValue>,
|
||||||
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl ActiveModelBehavior for ActiveModel {
|
impl ActiveModelBehavior for ActiveModel {
|
||||||
async fn before_save<C>(self, _db: &C, insert: bool) -> std::result::Result<Self, DbErr>
|
async fn before_save<C>(self, _db: &C, insert: bool) -> std::result::Result<Self, DbErr>
|
||||||
@@ -32,7 +61,35 @@ impl ActiveModelBehavior for ActiveModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// implement your read-oriented logic here
|
// implement your read-oriented logic here
|
||||||
impl Model {}
|
impl Model {
|
||||||
|
pub async fn filter_by(db: &DatabaseConnection, filter: MusicFilter) -> ModelResult<Vec<Self>> {
|
||||||
|
let mut query = Entity::find();
|
||||||
|
let mut condition = Condition::all();
|
||||||
|
|
||||||
|
if let Some(title) = filter.title {
|
||||||
|
condition = condition.add(match title {
|
||||||
|
MatchValue::Exact(val) => music_files::Column::Title.eq(val),
|
||||||
|
MatchValue::Partial(val) => music_files::Column::Title.contains(val),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if let Some(artist) = filter.artist {
|
||||||
|
condition = condition.add(match artist {
|
||||||
|
MatchValue::Exact(val) => music_files::Column::Artist.eq(val),
|
||||||
|
MatchValue::Partial(val) => music_files::Column::Artist.contains(val),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if let Some(album) = filter.album {
|
||||||
|
condition = condition.add(match album {
|
||||||
|
MatchValue::Exact(val) => music_files::Column::Album.eq(val),
|
||||||
|
MatchValue::Partial(val) => music_files::Column::Album.contains(val),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
query = query.filter(condition);
|
||||||
|
|
||||||
|
query.all(db).await.map_err(|_| ModelError::EntityNotFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// implement your write-oriented logic here
|
// implement your write-oriented logic here
|
||||||
impl ActiveModel {}
|
impl ActiveModel {}
|
||||||
|
Reference in New Issue
Block a user