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:
8 files changed
+685
-8
No files matched your search
+133
-3
@@ -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
|
||||
|
||||
@@ -68,9 +68,12 @@ pub use types::{
|
||||
Label,
|
||||
LabelDetail,
|
||||
LabelExploreResponse,
|
||||
LabelGetListResponse,
|
||||
LabelListPage,
|
||||
LabelPageContainer,
|
||||
LabelPageData,
|
||||
LabelPageGenericList,
|
||||
LabelStoryResponse,
|
||||
// Award types
|
||||
AlbumAward,
|
||||
AwardMagazine,
|
||||
|
||||
@@ -534,6 +534,68 @@ pub struct LabelExploreResponse {
|
||||
pub items: Option<Vec<serde_json::Value>>,
|
||||
}
|
||||
|
||||
// ============ Label Sub-resource Types (v9.7.0.3 API) ============
|
||||
//
|
||||
// The label page (/label/page) returns an aggregated snapshot; the
|
||||
// getAlbums / getPlaylists / getTopArtists / getNextReleases /
|
||||
// getAwardedReleases endpoints return paginated lists for each
|
||||
// sub-resource. Per Qobuz convention these use the V2 list envelope
|
||||
// { has_more, items: [...] }. Deserialized shapes are best-effort: if
|
||||
// the server wraps items in e.g. { albums: { items: ... } }, the
|
||||
// Optional fallbacks still keep the call non-fatal.
|
||||
|
||||
/// Generic paginated response from /label/get* endpoints.
|
||||
///
|
||||
/// `T` is typed (`Album`, `Playlist`, `Artist`) but all fields are
|
||||
/// tolerant so the same struct works if the server returns a bare
|
||||
/// items list or a nested one.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct LabelListPage<T> {
|
||||
#[serde(default)]
|
||||
pub has_more: Option<bool>,
|
||||
#[serde(default = "Vec::new")]
|
||||
pub items: Vec<T>,
|
||||
#[serde(default)]
|
||||
pub total: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub offset: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub limit: Option<u32>,
|
||||
}
|
||||
|
||||
/// Response from /label/story.
|
||||
///
|
||||
/// Shape inferred from `c30/b.java` — returns editorial / story content
|
||||
/// for a label. Actual fields beyond the label identity are not fully
|
||||
/// known; everything past `id` / `name` / `description` is kept open.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LabelStoryResponse {
|
||||
pub id: Option<u64>,
|
||||
pub name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub description: Option<String>,
|
||||
#[serde(default)]
|
||||
pub story: Option<String>,
|
||||
#[serde(default)]
|
||||
pub image: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
pub has_more: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub items: Option<Vec<serde_json::Value>>,
|
||||
}
|
||||
|
||||
/// Response from /label/getList (POST). Bulk lookup that hydrates
|
||||
/// label metadata for a set of label IDs.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct LabelGetListResponse {
|
||||
#[serde(default = "Vec::new")]
|
||||
pub labels: Vec<Label>,
|
||||
/// Fallback for unknown envelope shape — preserved as raw JSON if
|
||||
/// the server wraps differently than expected.
|
||||
#[serde(default)]
|
||||
pub extra: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
/// Genre model
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Genre {
|
||||
|
||||
@@ -1636,6 +1636,10 @@ impl QobuzClient {
|
||||
}
|
||||
|
||||
/// Get label by ID with albums
|
||||
///
|
||||
/// DEPRECATED: `/label/get` is not documented in the v9.7.0.3 spec.
|
||||
/// Use `get_label_albums` (catalog) + `get_label_page` (metadata).
|
||||
/// Kept temporarily for backward-compat during frontend migration.
|
||||
pub async fn get_label(&self, label_id: u64, limit: u32, offset: u32) -> Result<LabelDetail> {
|
||||
let url = endpoints::build_url(paths::LABEL_GET);
|
||||
let locale = self.locale().await;
|
||||
@@ -1659,6 +1663,192 @@ impl QobuzClient {
|
||||
Ok(serde_json::from_value(response)?)
|
||||
}
|
||||
|
||||
/// Get a label's album catalog (paginated, filterable).
|
||||
///
|
||||
/// Replaces the legacy `/label/get?extra=albums` path.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn get_label_albums(
|
||||
&self,
|
||||
label_id: u64,
|
||||
limit: u32,
|
||||
offset: u32,
|
||||
sort: Option<&str>,
|
||||
order: Option<&str>,
|
||||
genre_ids: Option<&str>,
|
||||
from_date: Option<&str>,
|
||||
to_date: Option<&str>,
|
||||
) -> Result<LabelListPage<Album>> {
|
||||
let url = endpoints::build_url(paths::LABEL_GET_ALBUMS);
|
||||
let mut params: Vec<(&str, String)> = vec![
|
||||
("label_id", label_id.to_string()),
|
||||
("limit", limit.to_string()),
|
||||
("offset", offset.to_string()),
|
||||
];
|
||||
if let Some(v) = sort { params.push(("sort", v.to_string())); }
|
||||
if let Some(v) = order { params.push(("order", v.to_string())); }
|
||||
if let Some(v) = genre_ids { params.push(("genre_ids", v.to_string())); }
|
||||
if let Some(v) = from_date { params.push(("from_date", v.to_string())); }
|
||||
if let Some(v) = to_date { params.push(("to_date", v.to_string())); }
|
||||
|
||||
log::debug!("[API] get_label_albums({}, limit={}, offset={})", label_id, limit, offset);
|
||||
let response: Value = self
|
||||
.signed_get(&url, "labelgetalbums", ¶ms)
|
||||
.await?
|
||||
.json()
|
||||
.await?;
|
||||
Ok(serde_json::from_value(response)?)
|
||||
}
|
||||
|
||||
/// Get a label's upcoming releases.
|
||||
pub async fn get_label_next_releases(
|
||||
&self,
|
||||
label_id: u64,
|
||||
limit: u32,
|
||||
offset: u32,
|
||||
genre_ids: Option<&str>,
|
||||
) -> Result<LabelListPage<Album>> {
|
||||
let url = endpoints::build_url(paths::LABEL_GET_NEXT_RELEASES);
|
||||
let mut params: Vec<(&str, String)> = vec![
|
||||
("label_id", label_id.to_string()),
|
||||
("limit", limit.to_string()),
|
||||
("offset", offset.to_string()),
|
||||
];
|
||||
if let Some(v) = genre_ids { params.push(("genre_ids", v.to_string())); }
|
||||
|
||||
log::debug!("[API] get_label_next_releases({})", label_id);
|
||||
let response: Value = self
|
||||
.signed_get(&url, "labelgetnextreleases", ¶ms)
|
||||
.await?
|
||||
.json()
|
||||
.await?;
|
||||
Ok(serde_json::from_value(response)?)
|
||||
}
|
||||
|
||||
/// Get a label's press-awarded releases.
|
||||
pub async fn get_label_awarded_releases(
|
||||
&self,
|
||||
label_id: u64,
|
||||
limit: u32,
|
||||
offset: u32,
|
||||
sort: Option<&str>,
|
||||
order: Option<&str>,
|
||||
genre_ids: Option<&str>,
|
||||
) -> Result<LabelListPage<Album>> {
|
||||
let url = endpoints::build_url(paths::LABEL_GET_AWARDED_RELEASES);
|
||||
let mut params: Vec<(&str, String)> = vec![
|
||||
("label_id", label_id.to_string()),
|
||||
("limit", limit.to_string()),
|
||||
("offset", offset.to_string()),
|
||||
];
|
||||
if let Some(v) = sort { params.push(("sort", v.to_string())); }
|
||||
if let Some(v) = order { params.push(("order", v.to_string())); }
|
||||
if let Some(v) = genre_ids { params.push(("genre_ids", v.to_string())); }
|
||||
|
||||
log::debug!("[API] get_label_awarded_releases({})", label_id);
|
||||
let response: Value = self
|
||||
.signed_get(&url, "labelgetawardedreleases", ¶ms)
|
||||
.await?
|
||||
.json()
|
||||
.await?;
|
||||
Ok(serde_json::from_value(response)?)
|
||||
}
|
||||
|
||||
/// Get a label's curated playlists (paginated).
|
||||
pub async fn get_label_playlists(
|
||||
&self,
|
||||
label_id: u64,
|
||||
limit: u32,
|
||||
offset: u32,
|
||||
) -> Result<LabelListPage<Playlist>> {
|
||||
let url = endpoints::build_url(paths::LABEL_GET_PLAYLISTS);
|
||||
log::debug!("[API] get_label_playlists({})", label_id);
|
||||
let response: Value = self
|
||||
.signed_get(&url, "labelgetplaylists", &[
|
||||
("label_id", label_id.to_string()),
|
||||
("limit", limit.to_string()),
|
||||
("offset", offset.to_string()),
|
||||
])
|
||||
.await?
|
||||
.json()
|
||||
.await?;
|
||||
Ok(serde_json::from_value(response)?)
|
||||
}
|
||||
|
||||
/// Get a label's top artists (paginated).
|
||||
pub async fn get_label_top_artists(
|
||||
&self,
|
||||
label_id: u64,
|
||||
limit: u32,
|
||||
offset: u32,
|
||||
) -> Result<LabelListPage<Artist>> {
|
||||
let url = endpoints::build_url(paths::LABEL_GET_TOP_ARTISTS);
|
||||
log::debug!("[API] get_label_top_artists({})", label_id);
|
||||
let response: Value = self
|
||||
.signed_get(&url, "labelgettopartists", &[
|
||||
("label_id", label_id.to_string()),
|
||||
("limit", limit.to_string()),
|
||||
("offset", offset.to_string()),
|
||||
])
|
||||
.await?
|
||||
.json()
|
||||
.await?;
|
||||
Ok(serde_json::from_value(response)?)
|
||||
}
|
||||
|
||||
/// Get a label's editorial / story content.
|
||||
pub async fn get_label_story(
|
||||
&self,
|
||||
label_id: u64,
|
||||
limit: u32,
|
||||
offset: u32,
|
||||
) -> Result<LabelStoryResponse> {
|
||||
let url = endpoints::build_url(paths::LABEL_STORY);
|
||||
log::debug!("[API] get_label_story({})", label_id);
|
||||
let response: Value = self
|
||||
.signed_get(&url, "labelstory", &[
|
||||
("label_id", label_id.to_string()),
|
||||
("limit", limit.to_string()),
|
||||
("offset", offset.to_string()),
|
||||
])
|
||||
.await?
|
||||
.json()
|
||||
.await?;
|
||||
Ok(serde_json::from_value(response)?)
|
||||
}
|
||||
|
||||
/// Bulk lookup a set of labels by ID (POST).
|
||||
///
|
||||
/// Follows the same signing convention as `get_tracks_batch` (see
|
||||
/// `track/getList`): the sig covers the joined ID list as a query
|
||||
/// string key, and the JSON body carries the list itself.
|
||||
pub async fn get_label_list(&self, label_ids: &[u64]) -> Result<LabelGetListResponse> {
|
||||
let url = endpoints::build_url(paths::LABEL_GET_LIST);
|
||||
let headers = self.api_headers().await?;
|
||||
let timestamp = get_timestamp();
|
||||
let secret = self.secret().await?;
|
||||
let ids_str: String = label_ids
|
||||
.iter()
|
||||
.map(|id| id.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
let sig = sign_request("labelgetList", &[("label_ids", &ids_str)], timestamp, &secret);
|
||||
|
||||
let body = serde_json::json!({ "label_ids": label_ids });
|
||||
log::debug!("[API] get_label_list POST ({} ids)", label_ids.len());
|
||||
|
||||
let response: Value = self
|
||||
.http
|
||||
.post(&url)
|
||||
.headers(headers)
|
||||
.query(&[("request_ts", timestamp.to_string()), ("request_sig", sig)])
|
||||
.json(&body)
|
||||
.send()
|
||||
.await?
|
||||
.json()
|
||||
.await?;
|
||||
Ok(serde_json::from_value(response)?)
|
||||
}
|
||||
|
||||
// === Authenticated endpoints ===
|
||||
|
||||
/// Get stream URL for a track (requires auth + signature)
|
||||
|
||||
@@ -53,9 +53,19 @@ pub mod paths {
|
||||
pub const FAVORITE_GET_NEW_RELEASES: &str = "/favorite/getNewReleases";
|
||||
|
||||
// Label
|
||||
// NOTE: /label/get is retained only for backward-compat until the
|
||||
// frontend is fully migrated; the v9.7.0.3 spec drops it in favor of
|
||||
// /label/getAlbums.
|
||||
pub const LABEL_GET: &str = "/label/get";
|
||||
pub const LABEL_PAGE: &str = "/label/page";
|
||||
pub const LABEL_EXPLORE: &str = "/label/explore";
|
||||
pub const LABEL_GET_ALBUMS: &str = "/label/getAlbums";
|
||||
pub const LABEL_GET_NEXT_RELEASES: &str = "/label/getNextReleases";
|
||||
pub const LABEL_GET_AWARDED_RELEASES: &str = "/label/getAwardedReleases";
|
||||
pub const LABEL_GET_PLAYLISTS: &str = "/label/getPlaylists";
|
||||
pub const LABEL_GET_TOP_ARTISTS: &str = "/label/getTopArtists";
|
||||
pub const LABEL_STORY: &str = "/label/story";
|
||||
pub const LABEL_GET_LIST: &str = "/label/getList";
|
||||
|
||||
// Award
|
||||
pub const AWARD_PAGE: &str = "/award/page";
|
||||
|
||||
@@ -2,8 +2,9 @@ use tauri::State;
|
||||
|
||||
use qbz_models::{
|
||||
Album, Artist, AwardPageData, DiscoverAlbum, DiscoverData, DiscoverPlaylistsResponse,
|
||||
DiscoverResponse, GenreInfo, LabelDetail, LabelExploreResponse, LabelPageData,
|
||||
PageArtistResponse, PlaylistTag, SearchResultsPage, Track,
|
||||
DiscoverResponse, GenreInfo, LabelDetail, LabelExploreResponse, LabelGetListResponse,
|
||||
LabelListPage, LabelPageData, LabelStoryResponse, PageArtistResponse, Playlist, PlaylistTag,
|
||||
SearchResultsPage, Track,
|
||||
};
|
||||
|
||||
use crate::artist_blacklist::BlacklistState;
|
||||
@@ -442,6 +443,187 @@ pub async fn v2_get_label_page(
|
||||
.map_err(RuntimeError::Internal)
|
||||
}
|
||||
|
||||
/// Get a label's album catalog (paginated, sortable, filterable).
|
||||
/// Replaces the legacy `v2_get_label` which called `/label/get`.
|
||||
#[tauri::command]
|
||||
#[allow(non_snake_case, clippy::too_many_arguments)]
|
||||
pub async fn v2_get_label_albums(
|
||||
labelId: u64,
|
||||
limit: Option<u32>,
|
||||
offset: Option<u32>,
|
||||
sort: Option<String>,
|
||||
order: Option<String>,
|
||||
genreIds: Option<String>,
|
||||
fromDate: Option<String>,
|
||||
toDate: Option<String>,
|
||||
bridge: State<'_, CoreBridgeState>,
|
||||
runtime: State<'_, RuntimeManagerState>,
|
||||
) -> Result<LabelListPage<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_label_albums: {} limit={} offset={}", labelId, limit, offset);
|
||||
let bridge = bridge.get().await;
|
||||
bridge
|
||||
.get_label_albums(labelId, limit, offset, sort, order, genreIds, fromDate, toDate)
|
||||
.await
|
||||
.map_err(RuntimeError::Internal)
|
||||
}
|
||||
|
||||
/// Get a label's upcoming releases.
|
||||
#[tauri::command]
|
||||
#[allow(non_snake_case)]
|
||||
pub async fn v2_get_label_next_releases(
|
||||
labelId: u64,
|
||||
limit: Option<u32>,
|
||||
offset: Option<u32>,
|
||||
genreIds: Option<String>,
|
||||
bridge: State<'_, CoreBridgeState>,
|
||||
runtime: State<'_, RuntimeManagerState>,
|
||||
) -> Result<LabelListPage<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_label_next_releases: {}", labelId);
|
||||
let bridge = bridge.get().await;
|
||||
bridge
|
||||
.get_label_next_releases(labelId, limit, offset, genreIds)
|
||||
.await
|
||||
.map_err(RuntimeError::Internal)
|
||||
}
|
||||
|
||||
/// Get a label's press-awarded releases.
|
||||
#[tauri::command]
|
||||
#[allow(non_snake_case)]
|
||||
pub async fn v2_get_label_awarded_releases(
|
||||
labelId: u64,
|
||||
limit: Option<u32>,
|
||||
offset: Option<u32>,
|
||||
sort: Option<String>,
|
||||
order: Option<String>,
|
||||
genreIds: Option<String>,
|
||||
bridge: State<'_, CoreBridgeState>,
|
||||
runtime: State<'_, RuntimeManagerState>,
|
||||
) -> Result<LabelListPage<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_label_awarded_releases: {}", labelId);
|
||||
let bridge = bridge.get().await;
|
||||
bridge
|
||||
.get_label_awarded_releases(labelId, limit, offset, sort, order, genreIds)
|
||||
.await
|
||||
.map_err(RuntimeError::Internal)
|
||||
}
|
||||
|
||||
/// Get a label's curated playlists.
|
||||
#[tauri::command]
|
||||
#[allow(non_snake_case)]
|
||||
pub async fn v2_get_label_playlists(
|
||||
labelId: u64,
|
||||
limit: Option<u32>,
|
||||
offset: Option<u32>,
|
||||
bridge: State<'_, CoreBridgeState>,
|
||||
runtime: State<'_, RuntimeManagerState>,
|
||||
) -> Result<LabelListPage<Playlist>, RuntimeError> {
|
||||
runtime
|
||||
.manager()
|
||||
.check_requirements(CommandRequirement::RequiresCoreBridgeAuth)
|
||||
.await?;
|
||||
|
||||
let limit = limit.unwrap_or(50);
|
||||
let offset = offset.unwrap_or(0);
|
||||
log::info!("[V2] get_label_playlists: {}", labelId);
|
||||
let bridge = bridge.get().await;
|
||||
bridge
|
||||
.get_label_playlists(labelId, limit, offset)
|
||||
.await
|
||||
.map_err(RuntimeError::Internal)
|
||||
}
|
||||
|
||||
/// Get a label's top artists.
|
||||
#[tauri::command]
|
||||
#[allow(non_snake_case)]
|
||||
pub async fn v2_get_label_top_artists(
|
||||
labelId: u64,
|
||||
limit: Option<u32>,
|
||||
offset: Option<u32>,
|
||||
bridge: State<'_, CoreBridgeState>,
|
||||
runtime: State<'_, RuntimeManagerState>,
|
||||
) -> Result<LabelListPage<Artist>, RuntimeError> {
|
||||
runtime
|
||||
.manager()
|
||||
.check_requirements(CommandRequirement::RequiresCoreBridgeAuth)
|
||||
.await?;
|
||||
|
||||
let limit = limit.unwrap_or(50);
|
||||
let offset = offset.unwrap_or(0);
|
||||
log::info!("[V2] get_label_top_artists: {}", labelId);
|
||||
let bridge = bridge.get().await;
|
||||
bridge
|
||||
.get_label_top_artists(labelId, limit, offset)
|
||||
.await
|
||||
.map_err(RuntimeError::Internal)
|
||||
}
|
||||
|
||||
/// Get a label's editorial story.
|
||||
#[tauri::command]
|
||||
#[allow(non_snake_case)]
|
||||
pub async fn v2_get_label_story(
|
||||
labelId: u64,
|
||||
limit: Option<u32>,
|
||||
offset: Option<u32>,
|
||||
bridge: State<'_, CoreBridgeState>,
|
||||
runtime: State<'_, RuntimeManagerState>,
|
||||
) -> Result<LabelStoryResponse, RuntimeError> {
|
||||
runtime
|
||||
.manager()
|
||||
.check_requirements(CommandRequirement::RequiresCoreBridgeAuth)
|
||||
.await?;
|
||||
|
||||
let limit = limit.unwrap_or(50);
|
||||
let offset = offset.unwrap_or(0);
|
||||
log::info!("[V2] get_label_story: {}", labelId);
|
||||
let bridge = bridge.get().await;
|
||||
bridge
|
||||
.get_label_story(labelId, limit, offset)
|
||||
.await
|
||||
.map_err(RuntimeError::Internal)
|
||||
}
|
||||
|
||||
/// Bulk hydrate labels by a list of IDs (POST /label/getList).
|
||||
#[tauri::command]
|
||||
#[allow(non_snake_case)]
|
||||
pub async fn v2_get_label_list(
|
||||
labelIds: Vec<u64>,
|
||||
bridge: State<'_, CoreBridgeState>,
|
||||
runtime: State<'_, RuntimeManagerState>,
|
||||
) -> Result<LabelGetListResponse, RuntimeError> {
|
||||
runtime
|
||||
.manager()
|
||||
.check_requirements(CommandRequirement::RequiresCoreBridgeAuth)
|
||||
.await?;
|
||||
|
||||
log::info!("[V2] get_label_list: {} ids", labelIds.len());
|
||||
let bridge = bridge.get().await;
|
||||
bridge
|
||||
.get_label_list(labelIds)
|
||||
.await
|
||||
.map_err(RuntimeError::Internal)
|
||||
}
|
||||
|
||||
/// Enumerate award catalog — paginated list of awards with their ids
|
||||
/// and names. Used by the frontend to resolve award ids by name when
|
||||
/// /album/get omits them.
|
||||
|
||||
@@ -14,9 +14,9 @@ use qbz_audio::{settings::AudioSettingsStore, AudioDiagnostic, AudioSettings, Vi
|
||||
use qbz_core::QbzCore;
|
||||
use qbz_models::{
|
||||
Album, Artist, DiscoverAlbum, DiscoverData, DiscoverPlaylistsResponse, DiscoverResponse,
|
||||
GenreInfo, LabelDetail, LabelExploreResponse, LabelPageData, PageArtistResponse, Playlist,
|
||||
PlaylistTag, Quality, QueueState, QueueTrack, RepeatMode, SearchResultsPage, StreamUrl, Track,
|
||||
UserSession,
|
||||
GenreInfo, LabelDetail, LabelExploreResponse, LabelGetListResponse, LabelListPage,
|
||||
LabelPageData, LabelStoryResponse, PageArtistResponse, Playlist, PlaylistTag, Quality,
|
||||
QueueState, QueueTrack, RepeatMode, SearchResultsPage, StreamUrl, Track, UserSession,
|
||||
};
|
||||
use qbz_player::{PlaybackState, Player};
|
||||
|
||||
@@ -620,6 +620,99 @@ impl CoreBridge {
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Label catalog (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>, String> {
|
||||
self.core
|
||||
.get_label_albums(label_id, limit, offset, sort, order, genre_ids, from_date, to_date)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
pub async fn get_label_next_releases(
|
||||
&self,
|
||||
label_id: u64,
|
||||
limit: u32,
|
||||
offset: u32,
|
||||
genre_ids: Option<String>,
|
||||
) -> Result<LabelListPage<Album>, String> {
|
||||
self.core
|
||||
.get_label_next_releases(label_id, limit, offset, genre_ids)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
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>, String> {
|
||||
self.core
|
||||
.get_label_awarded_releases(label_id, limit, offset, sort, order, genre_ids)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
pub async fn get_label_playlists(
|
||||
&self,
|
||||
label_id: u64,
|
||||
limit: u32,
|
||||
offset: u32,
|
||||
) -> Result<LabelListPage<Playlist>, String> {
|
||||
self.core
|
||||
.get_label_playlists(label_id, limit, offset)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
pub async fn get_label_top_artists(
|
||||
&self,
|
||||
label_id: u64,
|
||||
limit: u32,
|
||||
offset: u32,
|
||||
) -> Result<LabelListPage<Artist>, String> {
|
||||
self.core
|
||||
.get_label_top_artists(label_id, limit, offset)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
pub async fn get_label_story(
|
||||
&self,
|
||||
label_id: u64,
|
||||
limit: u32,
|
||||
offset: u32,
|
||||
) -> Result<LabelStoryResponse, String> {
|
||||
self.core
|
||||
.get_label_story(label_id, limit, offset)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
pub async fn get_label_list(
|
||||
&self,
|
||||
label_ids: Vec<u64>,
|
||||
) -> Result<LabelGetListResponse, String> {
|
||||
self.core
|
||||
.get_label_list(label_ids)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
// ==================== Streaming ====================
|
||||
|
||||
/// Get stream URL for a track with quality fallback
|
||||
|
||||
@@ -1620,6 +1620,13 @@ pub fn run() {
|
||||
commands_v2::v2_get_label,
|
||||
commands_v2::v2_get_label_page,
|
||||
commands_v2::v2_get_label_explore,
|
||||
commands_v2::v2_get_label_albums,
|
||||
commands_v2::v2_get_label_next_releases,
|
||||
commands_v2::v2_get_label_awarded_releases,
|
||||
commands_v2::v2_get_label_playlists,
|
||||
commands_v2::v2_get_label_top_artists,
|
||||
commands_v2::v2_get_label_story,
|
||||
commands_v2::v2_get_label_list,
|
||||
commands_v2::v2_get_award_page,
|
||||
commands_v2::v2_get_award_albums,
|
||||
commands_v2::v2_get_award_explore,
|
||||
|
||||
Reference in new issue
Block a user