diff --git a/src/app/collection/index.rs b/src/app/collection/index.rs index b57255a..3feb8a0 100644 --- a/src/app/collection/index.rs +++ b/src/app/collection/index.rs @@ -65,6 +65,17 @@ impl IndexManager { Ok(true) } + pub async fn get_album( + &self, + album_key: &AlbumKey, + ) -> Result { + let index = self.index.read().await; + let album_id = album_key.into(); + index + .get_album(album_id) + .ok_or_else(|| collection::Error::AlbumNotFound) + } + pub async fn get_random_albums( &self, count: usize, @@ -134,7 +145,7 @@ impl IndexBuilder { if !song.album_artists.0.is_empty() { album.artists = song.album_artists.0.clone(); - } else if !song.album_artists.0.is_empty() { + } else if !song.artists.0.is_empty() { album.artists = song.artists.0.clone(); } @@ -166,15 +177,17 @@ pub(super) struct Index { } impl Index { - pub fn get_album(&self, album_id: AlbumID) -> Option { + pub(self) fn get_album(&self, album_id: AlbumID) -> Option { self.albums.get(&album_id).map(|a| { - let songs = a + let mut songs = a .songs .iter() .filter_map(|s| self.songs.get(s)) .cloned() .collect::>(); + songs.sort_by_key(|s| (s.disc_number.unwrap_or(-1), s.track_number.unwrap_or(-1))); + collection::Album { name: a.name.clone(), artwork: a.artwork.clone(), @@ -188,7 +201,7 @@ impl Index { } #[derive(Clone, Copy, Eq, Hash, PartialEq, Serialize, Deserialize)] -pub struct SongID(u64); +struct SongID(u64); #[derive(Clone, Eq, Hash, PartialEq, Serialize, Deserialize)] pub struct SongKey { @@ -227,10 +240,10 @@ struct Album { } #[derive(Clone, Copy, Eq, Hash, PartialEq, Serialize, Deserialize)] -pub struct AlbumID(u64); +struct AlbumID(u64); #[derive(Clone, Eq, Hash, PartialEq)] -struct AlbumKey { +pub struct AlbumKey { pub artists: Vec, pub name: Option, } diff --git a/src/app/collection/types.rs b/src/app/collection/types.rs index 69cf3ae..7f26876 100644 --- a/src/app/collection/types.rs +++ b/src/app/collection/types.rs @@ -28,6 +28,8 @@ impl From> for MultiString { pub enum Error { #[error("Directory not found: {0}")] DirectoryNotFound(PathBuf), + #[error("Album not found")] + AlbumNotFound, #[error(transparent)] Database(#[from] sqlx::Error), #[error(transparent)] diff --git a/src/server.rs b/src/server.rs index 5e21bd1..77538c0 100644 --- a/src/server.rs +++ b/src/server.rs @@ -25,6 +25,7 @@ impl TryFrom for APIMajorVersion { pub const API_MAJOR_VERSION: i32 = 8; pub const API_MINOR_VERSION: i32 = 0; +pub const API_ARRAY_SEPARATOR: &'static str = "\u{000C}"; mod axum; pub use axum::*; diff --git a/src/server/axum/api.rs b/src/server/axum/api.rs index e291090..b2fd00a 100644 --- a/src/server/axum/api.rs +++ b/src/server/axum/api.rs @@ -12,7 +12,10 @@ use percent_encoding::percent_decode_str; use crate::{ app::{collection, config, ddns, lastfm, playlist, settings, thumbnail, user, vfs, App}, - server::{dto, error::APIError, APIMajorVersion, API_MAJOR_VERSION, API_MINOR_VERSION}, + server::{ + dto, error::APIError, APIMajorVersion, API_ARRAY_SEPARATOR, API_MAJOR_VERSION, + API_MINOR_VERSION, + }, }; use super::auth::{AdminRights, Auth}; @@ -40,6 +43,7 @@ pub fn router() -> Router { .route("/browse/*path", get(get_browse)) .route("/flatten", get(get_flatten_root)) .route("/flatten/*path", get(get_flatten)) + .route("/artists/:artists/albums/:name", get(get_album)) .route("/random", get(get_random)) .route("/recent", get(get_recent)) .route("/search", get(get_search_root)) @@ -365,6 +369,21 @@ async fn get_flatten( songs_to_response(songs, api_version) } +async fn get_album( + _auth: Auth, + State(index_manager): State, + Path((artists, name)): Path<(String, String)>, +) -> Result, APIError> { + let album_key = collection::AlbumKey { + artists: artists + .split(API_ARRAY_SEPARATOR) + .map(str::to_owned) + .collect::>(), + name: (!name.is_empty()).then_some(name), + }; + Ok(Json(index_manager.get_album(&album_key).await?.into())) +} + async fn get_random( _auth: Auth, api_version: APIMajorVersion, diff --git a/src/server/axum/error.rs b/src/server/axum/error.rs index 3623527..ad04345 100644 --- a/src/server/axum/error.rs +++ b/src/server/axum/error.rs @@ -21,6 +21,7 @@ impl IntoResponse for APIError { APIError::Database(_) => StatusCode::INTERNAL_SERVER_ERROR, APIError::DeletingOwnAccount => StatusCode::CONFLICT, APIError::DirectoryNotFound(_) => StatusCode::NOT_FOUND, + APIError::AlbumNotFound => StatusCode::NOT_FOUND, APIError::EmbeddedArtworkNotFound => StatusCode::NOT_FOUND, APIError::EmptyPassword => StatusCode::BAD_REQUEST, APIError::EmptyUsername => StatusCode::BAD_REQUEST, diff --git a/src/server/error.rs b/src/server/error.rs index 11e18c7..fd79d67 100644 --- a/src/server/error.rs +++ b/src/server/error.rs @@ -26,6 +26,8 @@ pub enum APIError { Database(sqlx::Error), #[error("Directory not found: {0}")] DirectoryNotFound(PathBuf), + #[error("Album not found")] + AlbumNotFound, #[error("DDNS update query failed with HTTP status {0}")] DdnsUpdateQueryFailed(u16), #[error("Cannot delete your own account")] @@ -86,6 +88,7 @@ impl From for APIError { fn from(error: collection::Error) -> APIError { match error { collection::Error::DirectoryNotFound(d) => APIError::DirectoryNotFound(d), + collection::Error::AlbumNotFound => APIError::AlbumNotFound, collection::Error::Database(e) => APIError::Database(e), collection::Error::DatabaseConnection(e) => e.into(), collection::Error::Vfs(e) => e.into(),