From 181f736f2548b70b009b939eac883bb58f378981 Mon Sep 17 00:00:00 2001 From: Connor Johnstone Date: Wed, 1 Apr 2026 19:36:24 -0400 Subject: [PATCH] fleshed out subsonic more --- src/queries/albums.rs | 126 ++++++++++++++++++++++++++++++++++++++++++ src/queries/tracks.rs | 48 ++++++++++++++++ 2 files changed, 174 insertions(+) diff --git a/src/queries/albums.rs b/src/queries/albums.rs index 11b740e..24e5f5a 100644 --- a/src/queries/albums.rs +++ b/src/queries/albums.rs @@ -1,3 +1,4 @@ +use sea_orm::sea_query::Expr; use sea_orm::*; use crate::entities::album::{self, ActiveModel, Entity as Albums, Model as Album}; @@ -84,6 +85,131 @@ pub async fn get_by_artist(db: &DatabaseConnection, artist_id: i32) -> DbResult< .await?) } +pub async fn get_random(db: &DatabaseConnection, count: u64) -> DbResult> { + Ok(Albums::find() + .order_by(Expr::cust("RANDOM()"), Order::Asc) + .limit(count) + .all(db) + .await?) +} + +pub async fn list_newest(db: &DatabaseConnection, limit: u64, offset: u64) -> DbResult> { + Ok(Albums::find() + .order_by_desc(Expr::cust("COALESCE(year, 0)")) + .order_by_asc(album::Column::Name) + .limit(limit) + .offset(offset) + .all(db) + .await?) +} + +pub async fn list_by_year_range( + db: &DatabaseConnection, + from: i32, + to: i32, + limit: u64, + offset: u64, +) -> DbResult> { + let (lo, hi) = if from <= to { (from, to) } else { (to, from) }; + Ok(Albums::find() + .filter(album::Column::Year.gte(lo)) + .filter(album::Column::Year.lte(hi)) + .order_by_asc(album::Column::Year) + .limit(limit) + .offset(offset) + .all(db) + .await?) +} + +pub async fn list_by_genre( + db: &DatabaseConnection, + genre: &str, + limit: u64, + offset: u64, +) -> DbResult> { + use crate::entities::track; + + // Find album IDs that have tracks matching this genre + let pattern = format!("%{genre}%"); + let album_ids: Vec = track::Entity::find() + .filter(Expr::cust_with_values( + "LOWER(genre) LIKE LOWER(?)", + [pattern], + )) + .filter(track::Column::AlbumId.is_not_null()) + .all(db) + .await? + .into_iter() + .filter_map(|t| t.album_id) + .collect::>() + .into_iter() + .collect(); + + if album_ids.is_empty() { + return Ok(vec![]); + } + + Ok(Albums::find() + .filter(album::Column::Id.is_in(album_ids)) + .order_by_asc(album::Column::Name) + .limit(limit) + .offset(offset) + .all(db) + .await?) +} + +pub async fn list_alphabetical_by_artist( + db: &DatabaseConnection, + limit: u64, + offset: u64, +) -> DbResult> { + Ok(Albums::find() + .order_by_asc(Expr::cust("LOWER(album_artist)")) + .order_by_asc(Expr::cust("LOWER(name)")) + .limit(limit) + .offset(offset) + .all(db) + .await?) +} + +pub async fn list_recent(db: &DatabaseConnection, limit: u64, offset: u64) -> DbResult> { + use crate::entities::track; + + // Find albums ordered by their most recently added track + let tracks = track::Entity::find() + .filter(track::Column::AlbumId.is_not_null()) + .order_by_desc(track::Column::AddedAt) + .all(db) + .await?; + + // Collect unique album IDs in order of most recent track + let mut seen = std::collections::HashSet::new(); + let album_ids: Vec = tracks + .into_iter() + .filter_map(|t| t.album_id) + .filter(|id| seen.insert(*id)) + .skip(offset as usize) + .take(limit as usize) + .collect(); + + if album_ids.is_empty() { + return Ok(vec![]); + } + + // Fetch albums and preserve the ordering + let albums = Albums::find() + .filter(album::Column::Id.is_in(album_ids.clone())) + .all(db) + .await?; + + let album_map: std::collections::HashMap = + albums.into_iter().map(|a| (a.id, a)).collect(); + Ok(album_ids + .into_iter() + .filter_map(|id| album_map.get(&id).cloned()) + .collect()) +} + pub async fn update(db: &DatabaseConnection, id: i32, model: ActiveModel) -> DbResult { let mut active = model; active.id = Set(id); diff --git a/src/queries/tracks.rs b/src/queries/tracks.rs index 1ca5b6f..3c2f58d 100644 --- a/src/queries/tracks.rs +++ b/src/queries/tracks.rs @@ -171,6 +171,54 @@ pub async fn get_random(db: &DatabaseConnection, count: u64) -> DbResult, + from_year: Option, + to_year: Option, +) -> DbResult> { + let mut query = Tracks::find(); + if let Some(g) = genre { + let pattern = format!("%{g}%"); + query = query.filter(Expr::cust_with_values( + "LOWER(genre) LIKE LOWER(?)", + [pattern], + )); + } + if let Some(y) = from_year { + query = query.filter(track::Column::Year.gte(y)); + } + if let Some(y) = to_year { + query = query.filter(track::Column::Year.lte(y)); + } + Ok(query + .order_by(Expr::cust("RANDOM()"), Order::Asc) + .limit(count) + .all(db) + .await?) +} + +/// Get tracks matching a genre with pagination. +pub async fn get_by_genre_paginated( + db: &DatabaseConnection, + genre: &str, + limit: u64, + offset: u64, +) -> DbResult> { + let pattern = format!("%{genre}%"); + Ok(Tracks::find() + .filter(Expr::cust_with_values( + "LOWER(genre) LIKE LOWER(?)", + [pattern], + )) + .limit(limit) + .offset(offset) + .all(db) + .await?) +} + /// Get tracks added within the last N days. pub async fn get_recent(db: &DatabaseConnection, days: u32, limit: u64) -> DbResult> { let cutoff = Utc::now().naive_utc() - chrono::Duration::days(i64::from(days));