Grouping and filtering music files
Some checks failed
CI / Check Style (push) Failing after 26s
CI / Run Clippy (push) Failing after 5m2s
CI / Run Tests (push) Failing after 11m13s

This commit is contained in:
2025-07-28 22:35:46 +02:00
parent 77bdae5fff
commit f8523c2e51
2 changed files with 114 additions and 14 deletions

View File

@@ -1,11 +1,14 @@
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::unnecessary_struct_initialization)]
#![allow(clippy::unused_async)]
use axum::{debug_handler, extract::Query};
use loco_rs::prelude::*;
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)]
pub struct Params {
@@ -14,7 +17,7 @@ pub struct Params {
pub artist: Option<String>,
pub album: Option<String>,
pub metadata: Option<serde_json::Value>,
}
}
impl Params {
fn update(&self, item: &mut ActiveModel) {
@@ -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> {
let item = Entity::find_by_id(id).one(&ctx.db).await?;
item.ok_or_else(|| Error::NotFound)
}
#[debug_handler]
pub async fn list(State(ctx): State<AppContext>) -> Result<Response> {
format::json(Entity::find().all(&ctx.db).await?)
pub async fn list(
State(ctx): State<AppContext>,
Query(filter): Query<MusicFilter>,
) -> Result<Response> {
format::json(Model::filter_by(&ctx.db, filter).await?)
}
#[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?)
}
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 {
Routes::new()
.prefix("api/music_files/")
@@ -79,4 +121,5 @@ pub fn routes() -> Routes {
.add("{id}", delete(remove))
.add("{id}", put(update))
.add("{id}", patch(update))
.add("/group_by/{column}", get(group_by))
}

View File

@@ -1,7 +1,8 @@
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::{
config::WriteOptions,
@@ -15,6 +16,34 @@ use tracing::error;
pub use super::_entities::music_files::{ActiveModel, Entity, Model};
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]
impl ActiveModelBehavior for ActiveModel {
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
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
impl ActiveModel {}