use {
serde::{Deserialize, Serialize},
serde_json::Value,
};
use crate::{
api::{
content::{
artists::Artist,
catalog::{AlbumSearchResponse, ItemSearchResult},
get_by_id, search,
},
service::QobuzApiService,
},
errors::QobuzApiError,
};
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct Album {
pub id: Option<String>,
pub title: Option<String>,
pub version: Option<String>,
pub upc: Option<String>,
pub url: Option<String>,
pub artist: Option<Box<Artist>>,
pub artists: Option<Vec<Box<Artist>>>,
pub composer: Option<Box<Artist>>,
pub label: Option<Label>,
pub genre: Option<Genre>,
pub genres: Option<Vec<Genre>>,
pub image: Option<Image>,
pub duration: Option<i32>,
pub tracks_count: Option<i32>,
pub media_count: Option<i32>,
pub release_date_original: Option<String>,
pub release_date_stream: Option<String>,
pub release_date_download: Option<String>,
pub product_type: Option<String>,
pub release_type: Option<String>,
pub hires: Option<bool>,
pub hires_streamable: Option<bool>,
pub downloadable: Option<bool>,
pub streamable: Option<bool>,
pub track_ids: Option<Vec<i32>>,
pub product_url: Option<String>,
pub released_at: Option<i64>,
pub copyright: Option<String>,
pub product_sales_factors: Option<Value>,
pub maximum_bit_depth: Option<i32>,
pub maximum_sampling_rate: Option<f64>,
pub maximum_channel_count: Option<i32>,
}
impl Album {
#[must_use]
pub const fn extension_for_format(format_id: i32) -> &'static str {
match format_id {
5 => "mp3",
_ => "flac",
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
pub struct Genre {
pub id: Option<i32>,
pub name: Option<String>,
pub slug: Option<String>,
pub color: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
pub struct Image {
pub small: Option<String>,
pub thumbnail: Option<String>,
pub medium: Option<String>,
pub large: Option<String>,
#[serde(rename = "extralarge")]
pub extra_large: Option<String>,
pub mega: Option<String>,
pub back: Option<String>,
pub url: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
pub struct Label {
pub id: Option<i32>,
pub name: Option<String>,
pub slug: Option<String>,
}
pub async fn search_albums(
service: &QobuzApiService,
query: &str,
limit: Option<i32>,
offset: Option<i32>,
) -> Result<ItemSearchResult<Box<Album>>, QobuzApiError> {
let resp: AlbumSearchResponse = search(service, "/album/search", query, limit, offset).await?;
Ok(resp.albums)
}
pub async fn get_album(
service: &QobuzApiService,
album_id: &str,
extra: Option<&str>,
) -> Result<Album, QobuzApiError> {
get_by_id(service, "/album/get", "album_id", album_id, extra).await
}
#[cfg(test)]
mod tests {
use {
anyhow::{Result, anyhow, ensure},
tokio::runtime::Runtime,
};
use crate::{
api::{
content::albums::{Album, get_album, search_albums},
fixture::{MockServer, make_service},
},
assert_empty_search_test,
};
#[test]
fn extension_for_format_maps_types() -> Result<()> {
ensure!(
Album::extension_for_format(5) == "mp3",
"MP3 should map to mp3"
);
ensure!(
Album::extension_for_format(6) == "flac",
"FLAC should map to flac"
);
ensure!(
Album::extension_for_format(27) == "flac",
"hi-res should map to flac"
);
Ok(())
}
#[test]
fn search_albums_deserializes_results() -> Result<()> {
let body = r#"{"albums":{"items":[{"id":"123","title":"Test Album"}],"total":1}}"#;
let server = MockServer::start(200, body)?;
let service = make_service(&server.base_url())?;
let rt = Runtime::new()?;
let result = rt.block_on(search_albums(&service, "Test", Some(5), None))?;
let items = result.items.ok_or_else(|| anyhow!("no items"))?;
ensure!(items.len() == 1);
ensure!(items.first().and_then(|i| i.title.as_deref()) == Some("Test Album"));
Ok(())
}
#[test]
fn search_albums_empty_results() -> Result<()> {
assert_empty_search_test!(
search_albums,
"Nonexistent",
r#"{"albums":{"items":[],"total":0}}"#
);
Ok(())
}
#[test]
fn search_albums_error_response() -> Result<()> {
let body = r#"{"status":"error","code":400,"message":"Bad request"}"#;
let server = MockServer::start(400, body)?;
let service = make_service(&server.base_url())?;
let rt = Runtime::new()?;
let result = rt.block_on(search_albums(&service, "Test", None, None));
ensure!(result.is_err());
Ok(())
}
#[test]
fn get_album_by_id() -> Result<()> {
let body = r#"{"id":"sr6843","title":"Kind of Blue","tracks_count":5}"#;
let server = MockServer::start(200, body)?;
let service = make_service(&server.base_url())?;
let rt = Runtime::new()?;
let album = rt.block_on(get_album(&service, "sr6843", None))?;
ensure!(album.title.as_deref() == Some("Kind of Blue"));
ensure!(album.tracks_count == Some(5));
Ok(())
}
#[test]
fn get_album_with_extra_param() -> Result<()> {
let body = r#"{"id":"sr6843","title":"Kind of Blue","track_ids":[1,2,3]}"#;
let server = MockServer::start(200, body)?;
let service = make_service(&server.base_url())?;
let rt = Runtime::new()?;
let album = rt.block_on(get_album(&service, "sr6843", Some("track_ids")))?;
let ids = album.track_ids.ok_or_else(|| anyhow!("no track_ids"))?;
ensure!(ids == vec![1, 2, 3]);
Ok(())
}
#[test]
fn get_album_not_found() -> Result<()> {
let body = r#"{"status":"error","code":404,"message":"Album not found"}"#;
let server = MockServer::start(404, body)?;
let service = make_service(&server.base_url())?;
let rt = Runtime::new()?;
let result = rt.block_on(get_album(&service, "nonexistent", None));
ensure!(result.is_err());
Ok(())
}
}