From f8523c2e51c88f88c93916b9776d1ddcfa8d3c98 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Mon, 28 Jul 2025 22:35:46 +0200 Subject: [PATCH] Grouping and filtering music files --- src/controllers/music_file.rs | 65 +++++++++++++++++++++++++++++------ src/models/music_files.rs | 63 +++++++++++++++++++++++++++++++-- 2 files changed, 114 insertions(+), 14 deletions(-) diff --git a/src/controllers/music_file.rs b/src/controllers/music_file.rs index 7f37f2c..61c6e81 100644 --- a/src/controllers/music_file.rs +++ b/src/controllers/music_file.rs @@ -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,16 +17,24 @@ pub struct Params { pub artist: Option, pub album: Option, pub metadata: Option, - } +} impl Params { fn update(&self, item: &mut ActiveModel) { - item.path = Set(self.path.clone()); - item.title = Set(self.title.clone()); - item.artist = Set(self.artist.clone()); - item.album = Set(self.album.clone()); - item.metadata = Set(self.metadata.clone()); - } + item.path = Set(self.path.clone()); + item.title = Set(self.title.clone()); + item.artist = Set(self.artist.clone()); + item.album = Set(self.album.clone()); + item.metadata = Set(self.metadata.clone()); + } +} + +#[derive(Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum GroupByColumn { + Title, + Artist, + Album, } async fn load_item(ctx: &AppContext, id: i32) -> Result { @@ -32,8 +43,11 @@ async fn load_item(ctx: &AppContext, id: i32) -> Result { } #[debug_handler] -pub async fn list(State(ctx): State) -> Result { - format::json(Entity::find().all(&ctx.db).await?) +pub async fn list( + State(ctx): State, + Query(filter): Query, +) -> Result { + format::json(Model::filter_by(&ctx.db, filter).await?) } #[debug_handler] @@ -70,6 +84,34 @@ pub async fn get_one(Path(id): Path, State(ctx): State) -> Resu format::json(load_item(&ctx, id).await?) } +pub async fn group_by( + Path(column): Path, + State(ctx): State, + Query(filter): Query, +) -> Result { + use GroupByColumn::*; + + let rows = Model::filter_by(&ctx.db, filter).await?; + + let mut grouped: std::collections::HashMap> = + std::collections::HashMap::new(); + + for row in rows { + let key = match column { + Title => row.title.clone().unwrap_or_else(|| "".to_string()), + Artist => row + .artist + .clone() + .unwrap_or_else(|| "".to_string()), + Album => row.album.clone().unwrap_or_else(|| "".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)) } diff --git a/src/models/music_files.rs b/src/models/music_files.rs index 9cc3fff..2c98228 100644 --- a/src/models/music_files.rs +++ b/src/models/music_files.rs @@ -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(deserializer: D) -> Result + 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, + pub artist: Option, + pub album: Option, +} + #[async_trait::async_trait] impl ActiveModelBehavior for ActiveModel { async fn before_save(self, _db: &C, insert: bool) -> std::result::Result @@ -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> { + 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 {}