feat: implement searching

This commit is contained in:
Lys 2023-10-14 16:06:35 +03:00
parent a27194816c
commit 07197a6703
Signed by: lyssieth
GPG key ID: C9CF3D614FAA3940
4 changed files with 159 additions and 23 deletions

View file

@ -30,5 +30,6 @@ pub fn build() -> Box<dyn Endpoint<Output = poem::Response>> {
.at("/stream", stream::stream) .at("/stream", stream::stream)
.at("/startScan", start_scan::start_scan) .at("/startScan", start_scan::start_scan)
.at("/getScanStatus", get_scan_status::get_scan_status) .at("/getScanStatus", get_scan_status::get_scan_status)
.at("/search3", search3::search3)
.boxed() .boxed()
} }

View file

@ -1,13 +1,21 @@
use color_eyre::Report;
use entities::{
album, artist,
prelude::{Album, Artist, Track},
track,
};
use poem::web::{Data, Query}; use poem::web::{Data, Query};
use poem_ext::db::DbTxn; use poem_ext::db::DbTxn;
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QuerySelect};
use serde::Deserialize; use serde::Deserialize;
use tracing::{error, instrument}; use tracing::{error, instrument};
use crate::{ use crate::{
authentication::Authentication, authentication::Authentication,
scan, subsonic::{
subsonic::{Error, SubsonicResponse}, Album as AlbumId3, Artist as ArtistId3, Child as ChildId3, Error, SubsonicResponse,
utils::{self}, },
utils,
}; };
#[poem::handler] #[poem::handler]
@ -24,52 +32,150 @@ pub async fn search3(
Err(e) => return e, Err(e) => return e,
}; };
todo!("actually implement"); let artists = match search_artists(txn.clone(), &params).await {
Ok(v) => v,
let artists = vec![]; Err(e) => {
let albums = vec![]; error!(error = &e.root_cause(), "failed to search artists: {e}");
let songs = vec![]; return SubsonicResponse::new_error(Error::Generic(None));
}
};
let albums = match search_albums(txn.clone(), &params).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(), &params).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) SubsonicResponse::new_search_result3(artists, albums, songs)
} }
async fn search_artists(txn: DbTxn, params: &Search3Params) -> Result<Vec<ArtistId3>, 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(&params.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<Vec<AlbumId3>, 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(&params.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<Vec<ChildId3>, 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(&params.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)] #[derive(Debug, Clone, Deserialize)]
pub struct Search3Params { pub struct Search3Params {
#[serde(default)]
pub query: String, pub query: String,
#[serde(rename = "artistCount", default = "default_artist_count")] #[serde(rename = "artistCount", default = "default_artist_count")]
pub artist_count: i32, pub artist_count: u64,
#[serde(rename = "artistOffset", default = "default_artist_offset")] #[serde(rename = "artistOffset", default = "default_artist_offset")]
pub artist_offset: i32, pub artist_offset: u64,
#[serde(rename = "albumCount", default = "default_album_count")] #[serde(rename = "albumCount", default = "default_album_count")]
pub album_count: i32, pub album_count: u64,
#[serde(rename = "albumOffset", default = "default_album_offset")] #[serde(rename = "albumOffset", default = "default_album_offset")]
pub album_offset: i32, pub album_offset: u64,
#[serde(rename = "songCount", default = "default_song_count")] #[serde(rename = "songCount", default = "default_song_count")]
pub song_count: i32, pub song_count: u64,
#[serde(rename = "songOffset", default = "default_song_offset")] #[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 20
} }
const fn default_artist_offset() -> i32 { const fn default_artist_offset() -> u64 {
0 0
} }
const fn default_album_count() -> i32 { const fn default_album_count() -> u64 {
20 20
} }
const fn default_album_offset() -> i32 { const fn default_album_offset() -> u64 {
0 0
} }
const fn default_song_count() -> i32 { const fn default_song_count() -> u64 {
20 20
} }
const fn default_song_offset() -> i32 { const fn default_song_offset() -> u64 {
0 0
} }

View file

@ -8,11 +8,10 @@ mod error;
mod types; mod types;
pub use error::Error; pub use error::Error;
pub use types::album::Album; pub use types::album::Album;
pub use types::artist::Artist;
pub use types::child::Child; pub use types::child::Child;
pub use types::music_folder::MusicFolder; pub use types::music_folder::MusicFolder;
use self::types::artist::Artist;
impl IntoResponse for SubsonicResponse { impl IntoResponse for SubsonicResponse {
fn into_response(self) -> poem::Response { fn into_response(self) -> poem::Response {
let body = quick_xml::se::to_string(&self).expect("Failed to serialize response"); let body = quick_xml::se::to_string(&self).expect("Failed to serialize response");

View file

@ -1,4 +1,34 @@
use entities::artist;
use serde::Serialize; use serde::Serialize;
use time::format_description::well_known::Iso8601;
#[derive(Debug, Clone, Serialize)] #[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<String>,
#[serde(rename = "@artistImageUrl")]
pub artist_image_url: Option<String>,
#[serde(rename = "@albumCount")]
pub album_count: u64,
#[serde(rename = "@starred")]
pub starred: Option<String>,
}
impl From<artist::Model> 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")),
}
}
}