feat(api): add 7 label endpoints from v9.7.0.3 spec

Closes the gap between our client and the Qobuz Android app's current
label API (source: c30/b.java). Qobuz is rolling out a web beta so the
legacy /label/get is likely on borrowed time — adding the v9.7.0.3
endpoints now so the migration can happen in follow-up commits.

New endpoints (paths + client + core + bridge + v2 command + registered):
- /label/getAlbums           → v2_get_label_albums
    paginated catalog with sort/order/genre_ids/from_date/to_date
- /label/getNextReleases     → v2_get_label_next_releases
- /label/getAwardedReleases  → v2_get_label_awarded_releases
- /label/getPlaylists        → v2_get_label_playlists
- /label/getTopArtists       → v2_get_label_top_artists
- /label/story               → v2_get_label_story
- /label/getList             → v2_get_label_list (POST, mirrors
    track/getList signing)

New response types in qbz-models:
- LabelListPage<T> — generic { has_more, items: [T], total?, offset?,
  limit? } envelope, tolerant to either V2 list or SearchResultsPage
  shapes until samples land
- LabelStoryResponse, LabelGetListResponse — best-effort with optional
  fields so a shape mismatch is non-fatal

The legacy /label/get + get_label() + v2_get_label + LabelDetail are
retained untouched for now; frontend migration and deletion come next.

cc
This commit is contained in:
vicrodh committed 2026-04-16 22:48:38 -06:00
1 parent e83caa7648
commit 595b71e4eb
8 files changed
+685 -8

No files matched your search

+133 -3
View File
@@ -8,9 +8,10 @@ use tokio::sync::RwLock;
use qbz_models::{
Album, Artist, CoreEvent, DiscoverAlbum, DiscoverData, DiscoverPlaylistsResponse,
DiscoverResponse, FrontendAdapter, GenreInfo, LabelDetail, LabelExploreResponse, LabelPageData,
PageArtistResponse, Playlist, PlaylistTag, Quality, QueueState, QueueTrack, RepeatMode,
SearchResultsPage, StreamUrl, Track, UserSession,
DiscoverResponse, FrontendAdapter, GenreInfo, LabelDetail, LabelExploreResponse,
LabelGetListResponse, LabelListPage, LabelPageData, LabelStoryResponse, PageArtistResponse,
Playlist, PlaylistTag, Quality, QueueState, QueueTrack, RepeatMode, SearchResultsPage,
StreamUrl, Track, UserSession,
};
use qbz_player::{PlaybackState, Player, QueueManager};
use qbz_qobuz::QobuzClient;
@@ -921,6 +922,135 @@ impl<A: FrontendAdapter + Send + Sync + 'static> QbzCore<A> {
.map_err(CoreError::Api)
}
/// Get a label's album catalog (paginated, replaces legacy /label/get).
#[allow(clippy::too_many_arguments)]
pub async fn get_label_albums(
&self,
label_id: u64,
limit: u32,
offset: u32,
sort: Option<String>,
order: Option<String>,
genre_ids: Option<String>,
from_date: Option<String>,
to_date: Option<String>,
) -> Result<LabelListPage<Album>, CoreError> {
let client = self.client.read().await;
let client = client.as_ref().ok_or(CoreError::NotInitialized)?;
client
.get_label_albums(
label_id,
limit,
offset,
sort.as_deref(),
order.as_deref(),
genre_ids.as_deref(),
from_date.as_deref(),
to_date.as_deref(),
)
.await
.map_err(CoreError::Api)
}
/// Get a label's upcoming releases.
pub async fn get_label_next_releases(
&self,
label_id: u64,
limit: u32,
offset: u32,
genre_ids: Option<String>,
) -> Result<LabelListPage<Album>, CoreError> {
let client = self.client.read().await;
let client = client.as_ref().ok_or(CoreError::NotInitialized)?;
client
.get_label_next_releases(label_id, limit, offset, genre_ids.as_deref())
.await
.map_err(CoreError::Api)
}
/// Get a label's press-awarded releases.
pub async fn get_label_awarded_releases(
&self,
label_id: u64,
limit: u32,
offset: u32,
sort: Option<String>,
order: Option<String>,
genre_ids: Option<String>,
) -> Result<LabelListPage<Album>, CoreError> {
let client = self.client.read().await;
let client = client.as_ref().ok_or(CoreError::NotInitialized)?;
client
.get_label_awarded_releases(
label_id,
limit,
offset,
sort.as_deref(),
order.as_deref(),
genre_ids.as_deref(),
)
.await
.map_err(CoreError::Api)
}
/// Get a label's curated playlists.
pub async fn get_label_playlists(
&self,
label_id: u64,
limit: u32,
offset: u32,
) -> Result<LabelListPage<Playlist>, CoreError> {
let client = self.client.read().await;
let client = client.as_ref().ok_or(CoreError::NotInitialized)?;
client
.get_label_playlists(label_id, limit, offset)
.await
.map_err(CoreError::Api)
}
/// Get a label's top artists.
pub async fn get_label_top_artists(
&self,
label_id: u64,
limit: u32,
offset: u32,
) -> Result<LabelListPage<Artist>, CoreError> {
let client = self.client.read().await;
let client = client.as_ref().ok_or(CoreError::NotInitialized)?;
client
.get_label_top_artists(label_id, limit, offset)
.await
.map_err(CoreError::Api)
}
/// Get a label's editorial story.
pub async fn get_label_story(
&self,
label_id: u64,
limit: u32,
offset: u32,
) -> Result<LabelStoryResponse, CoreError> {
let client = self.client.read().await;
let client = client.as_ref().ok_or(CoreError::NotInitialized)?;
client
.get_label_story(label_id, limit, offset)
.await
.map_err(CoreError::Api)
}
/// Bulk hydrate labels by ID list.
pub async fn get_label_list(
&self,
label_ids: Vec<u64>,
) -> Result<LabelGetListResponse, CoreError> {
let client = self.client.read().await;
let client = client.as_ref().ok_or(CoreError::NotInitialized)?;
client
.get_label_list(&label_ids)
.await
.map_err(CoreError::Api)
}
// ==================== Event Emission ====================
/// Emit an event to the frontend adapter