feat(award): backend V2 commands for /award/page and /award/getAlbums

Backend plumbing for Phase 3 AwardView. New endpoints:
- v2_get_award_page(awardId)       → hero + categorized releases
- v2_get_award_albums(awardId, …)  → paginated album list

Mirrors Android's AwardDto shape (id, name, image, awarded_at,
magazine, releases, playlists). Follows the label/page pattern for
signing and error handling. Deserializer accepts the V2 generic
{has_more, items} envelope as well as the legacy search-style
{albums: {items, total}} in case Qobuz swaps shapes per account.

cc
This commit is contained in:
vicrodh committed 2026-04-14 12:12:01 -06:00
1 parent f0b4499dab
commit fe3213ece5
7 files changed
+174 -3

No files matched your search

+28
View File
@@ -851,6 +851,34 @@ impl<A: FrontendAdapter + Send + Sync + 'static> QbzCore<A> {
.map_err(CoreError::Api) .map_err(CoreError::Api)
} }
/// Get award page — hero info + award-winning releases.
pub async fn get_award_page(
&self,
award_id: &str,
) -> Result<qbz_models::AwardPageData, CoreError> {
let client = self.client.read().await;
let client = client.as_ref().ok_or(CoreError::NotInitialized)?;
client
.get_award_page(award_id)
.await
.map_err(CoreError::Api)
}
/// Get paginated albums for an award (/award/getAlbums).
pub async fn get_award_albums(
&self,
award_id: &str,
limit: u32,
offset: u32,
) -> Result<SearchResultsPage<Album>, CoreError> {
let client = self.client.read().await;
let client = client.as_ref().ok_or(CoreError::NotInitialized)?;
client
.get_award_albums(award_id, limit, offset)
.await
.map_err(CoreError::Api)
}
/// Get label explore (discover more labels) /// Get label explore (discover more labels)
pub async fn get_label_explore( pub async fn get_label_explore(
&self, &self,
+5
View File
@@ -71,6 +71,11 @@ pub use types::{
LabelPageContainer, LabelPageContainer,
LabelPageData, LabelPageData,
LabelPageGenericList, LabelPageGenericList,
// Award page types
AwardMagazine,
AwardPageContainer,
AwardPageData,
AwardPageGenericList,
// Artist page types // Artist page types
PageArtistAward, PageArtistAward,
PageArtistBiography, PageArtistBiography,
+60
View File
@@ -1477,6 +1477,66 @@ impl QobuzClient {
Ok(serde_json::from_value(response)?) Ok(serde_json::from_value(response)?)
} }
/// Get award page — hero info + categorized award-winning releases.
/// Mirrors Android's AwardDto shape from /award/page.
pub async fn get_award_page(&self, award_id: &str) -> Result<qbz_models::AwardPageData> {
let url = endpoints::build_url(paths::AWARD_PAGE);
log::debug!("[API] get_award_page({})", award_id);
let response: serde_json::Value = self
.signed_get(&url, "awardpage", &[("award_id", award_id.to_string())])
.await?
.json()
.await?;
Ok(serde_json::from_value(response)?)
}
/// Get paginated albums for a single award (/award/getAlbums).
pub async fn get_award_albums(
&self,
award_id: &str,
limit: u32,
offset: u32,
) -> Result<SearchResultsPage<Album>> {
let url = endpoints::build_url(paths::AWARD_GET_ALBUMS);
log::debug!("[API] get_award_albums({}, limit={}, offset={})", award_id, limit, offset);
let response: serde_json::Value = self
.signed_get(
&url,
"awardgetAlbums",
&[
("award_id", award_id.to_string()),
("limit", limit.to_string()),
("offset", offset.to_string()),
],
)
.await?
.json()
.await?;
// Response envelope may be V2GenericListDto {has_more,items} or the
// search-style {albums: {items,total,offset,limit}}. Try both.
if let Some(albums_obj) = response.get("albums") {
return Ok(serde_json::from_value(albums_obj.clone())?);
}
let items_value = response.get("items").cloned().unwrap_or(serde_json::Value::Null);
let items: Vec<Album> = serde_json::from_value(items_value).unwrap_or_default();
let has_more = response
.get("has_more")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let total_hint = if has_more {
offset + items.len() as u32 + 1
} else {
offset + items.len() as u32
};
Ok(SearchResultsPage::<Album> {
items,
total: total_hint,
offset,
limit,
})
}
/// Get label explore (discover more labels) /// Get label explore (discover more labels)
pub async fn get_label_explore(&self, limit: u32, offset: u32) -> Result<LabelExploreResponse> { pub async fn get_label_explore(&self, limit: u32, offset: u32) -> Result<LabelExploreResponse> {
let url = endpoints::build_url(paths::LABEL_EXPLORE); let url = endpoints::build_url(paths::LABEL_EXPLORE);
+4
View File
@@ -57,6 +57,10 @@ pub mod paths {
pub const LABEL_PAGE: &str = "/label/page"; pub const LABEL_PAGE: &str = "/label/page";
pub const LABEL_EXPLORE: &str = "/label/explore"; pub const LABEL_EXPLORE: &str = "/label/explore";
// Award
pub const AWARD_PAGE: &str = "/award/page";
pub const AWARD_GET_ALBUMS: &str = "/award/getAlbums";
// Genre // Genre
pub const GENRE_LIST: &str = "/genre/list"; pub const GENRE_LIST: &str = "/genre/list";
+54 -3
View File
@@ -1,9 +1,9 @@
use tauri::State; use tauri::State;
use qbz_models::{ use qbz_models::{
Album, Artist, DiscoverAlbum, DiscoverData, DiscoverPlaylistsResponse, DiscoverResponse, Album, Artist, AwardPageData, DiscoverAlbum, DiscoverData, DiscoverPlaylistsResponse,
GenreInfo, LabelDetail, LabelExploreResponse, LabelPageData, PageArtistResponse, DiscoverResponse, GenreInfo, LabelDetail, LabelExploreResponse, LabelPageData,
PlaylistTag, SearchResultsPage, Track, PageArtistResponse, PlaylistTag, SearchResultsPage, Track,
}; };
use crate::artist_blacklist::BlacklistState; use crate::artist_blacklist::BlacklistState;
@@ -442,6 +442,57 @@ pub async fn v2_get_label_page(
.map_err(RuntimeError::Internal) .map_err(RuntimeError::Internal)
} }
/// Get award page (hero info + award-winning releases).
#[tauri::command]
#[allow(non_snake_case)]
pub async fn v2_get_award_page(
awardId: String,
bridge: State<'_, CoreBridgeState>,
runtime: State<'_, RuntimeManagerState>,
) -> Result<AwardPageData, RuntimeError> {
runtime
.manager()
.check_requirements(CommandRequirement::RequiresCoreBridgeAuth)
.await?;
log::info!("[V2] get_award_page: {}", awardId);
let bridge = bridge.get().await;
bridge
.get_award_page(&awardId)
.await
.map_err(RuntimeError::Internal)
}
/// Paginated albums for a single award.
#[tauri::command]
#[allow(non_snake_case)]
pub async fn v2_get_award_albums(
awardId: String,
limit: Option<u32>,
offset: Option<u32>,
bridge: State<'_, CoreBridgeState>,
runtime: State<'_, RuntimeManagerState>,
) -> Result<SearchResultsPage<Album>, RuntimeError> {
runtime
.manager()
.check_requirements(CommandRequirement::RequiresCoreBridgeAuth)
.await?;
let limit = limit.unwrap_or(50);
let offset = offset.unwrap_or(0);
log::info!(
"[V2] get_award_albums: award={}, limit={}, offset={}",
awardId,
limit,
offset
);
let bridge = bridge.get().await;
bridge
.get_award_albums(&awardId, limit, offset)
.await
.map_err(RuntimeError::Internal)
}
/// Get label explore (discover more labels) /// Get label explore (discover more labels)
#[tauri::command] #[tauri::command]
#[allow(non_snake_case)] #[allow(non_snake_case)]
+21
View File
@@ -570,6 +570,27 @@ impl CoreBridge {
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())
} }
/// Get award page — hero info + award-winning releases.
pub async fn get_award_page(&self, award_id: &str) -> Result<qbz_models::AwardPageData, String> {
self.core
.get_award_page(award_id)
.await
.map_err(|e| e.to_string())
}
/// Get paginated albums for an award (/award/getAlbums).
pub async fn get_award_albums(
&self,
award_id: &str,
limit: u32,
offset: u32,
) -> Result<SearchResultsPage<Album>, String> {
self.core
.get_award_albums(award_id, limit, offset)
.await
.map_err(|e| e.to_string())
}
/// Get label explore (discover more labels) /// Get label explore (discover more labels)
pub async fn get_label_explore( pub async fn get_label_explore(
&self, &self,
+2
View File
@@ -1601,6 +1601,8 @@ pub fn run() {
commands_v2::v2_get_label, commands_v2::v2_get_label,
commands_v2::v2_get_label_page, commands_v2::v2_get_label_page,
commands_v2::v2_get_label_explore, commands_v2::v2_get_label_explore,
commands_v2::v2_get_award_page,
commands_v2::v2_get_award_albums,
commands_v2::v2_pause_playback, commands_v2::v2_pause_playback,
commands_v2::v2_resume_playback, commands_v2::v2_resume_playback,
commands_v2::v2_stop_playback, commands_v2::v2_stop_playback,