diff --git a/rave/src/rest/mod.rs b/rave/src/rest/mod.rs index 86cbd16..53cb163 100644 --- a/rave/src/rest/mod.rs +++ b/rave/src/rest/mod.rs @@ -30,5 +30,6 @@ pub fn build() -> Box> { .at("/stream", stream::stream) .at("/startScan", start_scan::start_scan) .at("/getScanStatus", get_scan_status::get_scan_status) + .at("/search3", search3::search3) .boxed() } diff --git a/rave/src/rest/search3.rs b/rave/src/rest/search3.rs index a670045..eef4b7c 100644 --- a/rave/src/rest/search3.rs +++ b/rave/src/rest/search3.rs @@ -1,13 +1,21 @@ +use color_eyre::Report; +use entities::{ + album, artist, + prelude::{Album, Artist, Track}, + track, +}; use poem::web::{Data, Query}; use poem_ext::db::DbTxn; +use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QuerySelect}; use serde::Deserialize; use tracing::{error, instrument}; use crate::{ authentication::Authentication, - scan, - subsonic::{Error, SubsonicResponse}, - utils::{self}, + subsonic::{ + Album as AlbumId3, Artist as ArtistId3, Child as ChildId3, Error, SubsonicResponse, + }, + utils, }; #[poem::handler] @@ -24,52 +32,150 @@ pub async fn search3( Err(e) => return e, }; - todo!("actually implement"); - - let artists = vec![]; - let albums = vec![]; - let songs = vec![]; + let artists = match search_artists(txn.clone(), ¶ms).await { + Ok(v) => v, + Err(e) => { + error!(error = &e.root_cause(), "failed to search artists: {e}"); + return SubsonicResponse::new_error(Error::Generic(None)); + } + }; + let albums = match search_albums(txn.clone(), ¶ms).await { + Ok(v) => v, + Err(e) => { + error!(error = &e.root_cause(), "failed to search albums: {e}"); + return SubsonicResponse::new_error(Error::Generic(None)); + } + }; + let songs = match search_songs(txn.clone(), ¶ms).await { + Ok(v) => v, + Err(e) => { + error!(error = &e.root_cause(), "failed to search songs: {e}"); + return SubsonicResponse::new_error(Error::Generic(None)); + } + }; SubsonicResponse::new_search_result3(artists, albums, songs) } +async fn search_artists(txn: DbTxn, params: &Search3Params) -> Result, Report> { + if params.query.is_empty() { + let artists = Artist::find() + .limit(params.artist_count) + .offset(params.artist_offset) + .all(&*txn) + .await?; + + Ok(artists.into_iter().map(Into::into).collect()) + } else { + let artists = Artist::find() + .filter(artist::Column::Name.contains(¶ms.query)) + .limit(params.artist_count) + .offset(params.artist_offset) + .all(&*txn) + .await?; + + Ok(artists.into_iter().map(Into::into).collect()) + } +} + +async fn search_albums(txn: DbTxn, params: &Search3Params) -> Result, Report> { + if params.query.is_empty() { + let albums = Album::find() + .limit(params.album_count) + .offset(params.album_offset) + .all(&*txn) + .await?; + + Ok(albums + .into_iter() + .map(|v| AlbumId3::new(v, None, None)) + .collect()) + } else { + let albums = Album::find() + .filter(album::Column::Name.contains(¶ms.query)) + .limit(params.album_count) + .offset(params.album_offset) + .all(&*txn) + .await?; + + Ok(albums + .into_iter() + .map(|v| AlbumId3::new(v, None, None)) + .collect()) + } +} + +async fn search_songs(txn: DbTxn, params: &Search3Params) -> Result, Report> { + if params.query.is_empty() { + let songs = Track::find() + .limit(params.song_count) + .offset(params.song_offset) + .find_also_related(Album) + .all(&*txn) + .await?; + + Ok(songs + .into_iter() + .filter_map(|(track, album)| { + album.map(|album| ChildId3::new(&track, &AlbumId3::new(album, None, None))) + }) + .collect()) + } else { + let songs = Track::find() + .filter(track::Column::Title.contains(¶ms.query)) + .limit(params.song_count) + .offset(params.song_offset) + .find_also_related(Album) + .all(&*txn) + .await?; + + Ok(songs + .into_iter() + .filter_map(|(track, album)| { + album.map(|album| ChildId3::new(&track, &AlbumId3::new(album, None, None))) + }) + .collect()) + } +} + #[derive(Debug, Clone, Deserialize)] pub struct Search3Params { + #[serde(default)] pub query: String, #[serde(rename = "artistCount", default = "default_artist_count")] - pub artist_count: i32, + pub artist_count: u64, #[serde(rename = "artistOffset", default = "default_artist_offset")] - pub artist_offset: i32, + pub artist_offset: u64, #[serde(rename = "albumCount", default = "default_album_count")] - pub album_count: i32, + pub album_count: u64, #[serde(rename = "albumOffset", default = "default_album_offset")] - pub album_offset: i32, + pub album_offset: u64, #[serde(rename = "songCount", default = "default_song_count")] - pub song_count: i32, + pub song_count: u64, #[serde(rename = "songOffset", default = "default_song_offset")] - pub song_offset: i32, + pub song_offset: u64, } -const fn default_artist_count() -> i32 { +const fn default_artist_count() -> u64 { 20 } -const fn default_artist_offset() -> i32 { +const fn default_artist_offset() -> u64 { 0 } -const fn default_album_count() -> i32 { +const fn default_album_count() -> u64 { 20 } -const fn default_album_offset() -> i32 { +const fn default_album_offset() -> u64 { 0 } -const fn default_song_count() -> i32 { +const fn default_song_count() -> u64 { 20 } -const fn default_song_offset() -> i32 { +const fn default_song_offset() -> u64 { 0 } diff --git a/rave/src/subsonic/mod.rs b/rave/src/subsonic/mod.rs index 9089f80..c76510f 100644 --- a/rave/src/subsonic/mod.rs +++ b/rave/src/subsonic/mod.rs @@ -8,11 +8,10 @@ mod error; mod types; pub use error::Error; pub use types::album::Album; +pub use types::artist::Artist; pub use types::child::Child; pub use types::music_folder::MusicFolder; -use self::types::artist::Artist; - impl IntoResponse for SubsonicResponse { fn into_response(self) -> poem::Response { let body = quick_xml::se::to_string(&self).expect("Failed to serialize response"); diff --git a/rave/src/subsonic/types/artist.rs b/rave/src/subsonic/types/artist.rs index e329a02..deee55e 100644 --- a/rave/src/subsonic/types/artist.rs +++ b/rave/src/subsonic/types/artist.rs @@ -1,4 +1,34 @@ +use entities::artist; use serde::Serialize; +use time::format_description::well_known::Iso8601; #[derive(Debug, Clone, Serialize)] -pub struct Artist {} +pub struct Artist { + #[serde(rename = "@id")] + pub id: String, + #[serde(rename = "@name")] + pub name: String, + #[serde(rename = "@coverArtId")] + pub cover_art: Option, + #[serde(rename = "@artistImageUrl")] + pub artist_image_url: Option, + #[serde(rename = "@albumCount")] + pub album_count: u64, + #[serde(rename = "@starred")] + pub starred: Option, +} + +impl From for Artist { + fn from(artist: artist::Model) -> Self { + Self { + id: format!("ar-{}", artist.id), + name: artist.name, + cover_art: artist.cover_art_id.map(|v| format!("ca-{}", v)), + artist_image_url: artist.artist_image_url, + album_count: artist.album_count as u64, + starred: artist + .starred + .map(|v| v.format(&Iso8601::DEFAULT).expect("failed to format date")), + } + } +}