fix(api): Release Watch lives at /favorite/getNewReleases?type=artists
The inferred OpenAPI spec
(qbz-nix-docs/qobuz-api-inferred-openapi-v9.7.0.3.yaml, source:
x20/b.java) documents the endpoint as /favorite/getNewReleases with
three required query params:
@GET favorite/getNewReleases
- type: String (artists | labels | awards — matches the three
tabs in the mobile UI)
- limit: Integer
- offset: Integer
My earlier attempts at /albums/releaseWatch were chasing the Android
route-table entry for the Release Watch *screen*, not the HTTP call
that screen makes. Different layer.
Also removes my made-up speculation about mobile app_id gating — the
/favorite/* endpoints are standard web-API calls the existing client
already knows how to sign (via authenticated_headers + /0.2/ base URL).
v2_get_release_watch now takes an optional releaseType arg defaulting
to 'artists' so callers can switch tabs later without another command.
cc
This commit is contained in:
5 files changed
+39
-31
No files matched your search
@@ -760,11 +760,11 @@ impl<A: FrontendAdapter + Send + Sync + 'static> QbzCore<A> {
|
||||
.map_err(CoreError::Api)
|
||||
}
|
||||
|
||||
/// Get Release Watch — new releases from followed artists, labels and
|
||||
/// awards (Qobuz mobile "Radar de Novedades"). REST endpoint called with
|
||||
/// authenticated headers; no RPC signing.
|
||||
/// Get Release Watch — new releases from followed artists/labels/awards.
|
||||
/// `release_type` must be one of "artists" | "labels" | "awards".
|
||||
pub async fn get_release_watch(
|
||||
&self,
|
||||
release_type: &str,
|
||||
limit: u32,
|
||||
offset: u32,
|
||||
) -> Result<SearchResultsPage<Album>, CoreError> {
|
||||
@@ -772,7 +772,7 @@ impl<A: FrontendAdapter + Send + Sync + 'static> QbzCore<A> {
|
||||
let client = client.as_ref().ok_or(CoreError::NotInitialized)?;
|
||||
|
||||
client
|
||||
.get_release_watch(limit, offset)
|
||||
.get_release_watch(release_type, limit, offset)
|
||||
.await
|
||||
.map_err(CoreError::Api)
|
||||
}
|
||||
|
||||
@@ -767,21 +767,20 @@ impl QobuzClient {
|
||||
Ok(serde_json::from_value(albums.clone())?)
|
||||
}
|
||||
|
||||
/// Get Release Watch — new releases from artists, labels and awards the
|
||||
/// user follows. The Qobuz mobile client surfaces this as "Radar de
|
||||
/// Novedades" / "Release Watch".
|
||||
/// Get Release Watch — new releases from artists, labels or awards the
|
||||
/// user follows. Qobuz mobile surfaces this as "Radar de Novedades" /
|
||||
/// "Release Watch". Endpoint and signature confirmed in
|
||||
/// `qbz-nix-docs/qobuz-api-inferred-openapi-v9.7.0.3.yaml`.
|
||||
///
|
||||
/// This is a REST endpoint in the `/albums/*` plural namespace — called
|
||||
/// with authenticated headers (X-App-Id + X-User-Auth-Token) and NO
|
||||
/// request_sig/request_ts, same shape as the `/radio/*` endpoints. The
|
||||
/// RPC signing protocol does NOT apply here and using it yields the
|
||||
/// "no Service found" error.
|
||||
/// `release_type` is required and must be one of `artists`, `labels`,
|
||||
/// `awards` (matches the three tabs in the mobile UI).
|
||||
pub async fn get_release_watch(
|
||||
&self,
|
||||
release_type: &str,
|
||||
limit: u32,
|
||||
offset: u32,
|
||||
) -> Result<SearchResultsPage<Album>> {
|
||||
let url = endpoints::build_url(paths::ALBUMS_RELEASE_WATCH);
|
||||
let url = endpoints::build_url(paths::FAVORITE_GET_NEW_RELEASES);
|
||||
let limit_str = limit.to_string();
|
||||
let offset_str = offset.to_string();
|
||||
|
||||
@@ -790,6 +789,7 @@ impl QobuzClient {
|
||||
.get(&url)
|
||||
.headers(self.authenticated_headers().await?)
|
||||
.query(&[
|
||||
("type", release_type),
|
||||
("limit", limit_str.as_str()),
|
||||
("offset", offset_str.as_str()),
|
||||
])
|
||||
@@ -798,14 +798,13 @@ impl QobuzClient {
|
||||
|
||||
let status = http_response.status();
|
||||
log::info!(
|
||||
"[API] get_release_watch(limit={}, offset={}) status={}",
|
||||
"[API] get_release_watch(type={}, limit={}, offset={}) status={}",
|
||||
release_type,
|
||||
limit,
|
||||
offset,
|
||||
status
|
||||
);
|
||||
|
||||
// Capture body as text first so we can log it on unexpected shape
|
||||
// regardless of JSON parse success.
|
||||
let body_text = http_response.text().await?;
|
||||
if !status.is_success() {
|
||||
log::warn!(
|
||||
@@ -827,8 +826,9 @@ impl QobuzClient {
|
||||
);
|
||||
}
|
||||
|
||||
// Typical Qobuz paginated album wrapper: {albums: {items, total, ...}}.
|
||||
// Fall back to root-level items if the server flattened it.
|
||||
// Response is V2GenericListDto<AlbumDto> per x20/b.java. Qobuz
|
||||
// wraps paginated album lists under `albums` by convention; try that
|
||||
// first, fall back to root.
|
||||
let page_value = response.get("albums").cloned().unwrap_or(response);
|
||||
match serde_json::from_value::<SearchResultsPage<Album>>(page_value) {
|
||||
Ok(page) => Ok(page),
|
||||
|
||||
@@ -20,11 +20,6 @@ pub mod paths {
|
||||
pub const ALBUM_GET: &str = "/album/get";
|
||||
pub const ALBUM_SEARCH: &str = "/album/search";
|
||||
pub const ALBUM_GET_FEATURED: &str = "/album/getFeatured";
|
||||
// Mobile/plural namespace. Unlike /album/* (singular), these endpoints
|
||||
// are REST-style — called with X-App-Id + X-User-Auth-Token headers and
|
||||
// NO request_sig/request_ts. See ALBUMS_RELEASE_WATCH (issue: mobile
|
||||
// "Radar de Novedades" feed of releases from followed artists/labels/awards).
|
||||
pub const ALBUMS_RELEASE_WATCH: &str = "/albums/releaseWatch";
|
||||
|
||||
// Artist
|
||||
pub const ARTIST_GET: &str = "/artist/get";
|
||||
@@ -51,6 +46,11 @@ pub mod paths {
|
||||
pub const FAVORITE_GET_USER_FAVORITES: &str = "/favorite/getUserFavorites";
|
||||
pub const FAVORITE_CREATE: &str = "/favorite/create";
|
||||
pub const FAVORITE_DELETE: &str = "/favorite/delete";
|
||||
// New releases from followed artists / labels / awards — the "Radar de
|
||||
// Novedades" feed in the mobile Qobuz client. Path confirmed in
|
||||
// qbz-nix-docs/qobuz-api-inferred-openapi-v9.7.0.3.yaml
|
||||
// (source: x20/b.java Retrofit interface).
|
||||
pub const FAVORITE_GET_NEW_RELEASES: &str = "/favorite/getNewReleases";
|
||||
|
||||
// Label
|
||||
pub const LABEL_GET: &str = "/label/get";
|
||||
|
||||
@@ -258,14 +258,15 @@ pub async fn v2_get_featured_albums(
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// Get Release Watch (V2 — releases from followed artists, labels and awards).
|
||||
/// Get Release Watch (V2 — releases from followed artists/labels/awards).
|
||||
///
|
||||
/// Calls the mobile "Radar de Novedades" endpoint (`/albums/releaseWatch`)
|
||||
/// with authenticated headers (no RPC signing). Runs the result through the
|
||||
/// artist blacklist filter.
|
||||
/// Calls `/favorite/getNewReleases?type=...` — the same endpoint the mobile
|
||||
/// client uses for its "Radar de Novedades" feed. `release_type` defaults to
|
||||
/// `"artists"` (the mobile default tab) and accepts `"labels"` or `"awards"`.
|
||||
#[tauri::command]
|
||||
#[allow(non_snake_case)]
|
||||
pub async fn v2_get_release_watch(
|
||||
releaseType: Option<String>,
|
||||
limit: Option<u32>,
|
||||
offset: Option<u32>,
|
||||
bridge: State<'_, CoreBridgeState>,
|
||||
@@ -277,13 +278,19 @@ pub async fn v2_get_release_watch(
|
||||
.check_requirements(CommandRequirement::RequiresCoreBridgeAuth)
|
||||
.await?;
|
||||
|
||||
let release_type = releaseType.unwrap_or_else(|| "artists".to_string());
|
||||
let limit = limit.unwrap_or(20);
|
||||
let offset = offset.unwrap_or(0);
|
||||
log::info!("[V2] get_release_watch: limit={}, offset={}", limit, offset);
|
||||
log::info!(
|
||||
"[V2] get_release_watch: type={}, limit={}, offset={}",
|
||||
release_type,
|
||||
limit,
|
||||
offset
|
||||
);
|
||||
|
||||
let bridge = bridge.get().await;
|
||||
let mut results = bridge
|
||||
.get_release_watch(limit, offset)
|
||||
.get_release_watch(&release_type, limit, offset)
|
||||
.await
|
||||
.map_err(RuntimeError::Internal)?;
|
||||
|
||||
|
||||
@@ -498,14 +498,15 @@ impl CoreBridge {
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Get Release Watch — releases from followed artists, labels and awards.
|
||||
/// Get Release Watch — releases from followed artists/labels/awards.
|
||||
pub async fn get_release_watch(
|
||||
&self,
|
||||
release_type: &str,
|
||||
limit: u32,
|
||||
offset: u32,
|
||||
) -> Result<SearchResultsPage<Album>, String> {
|
||||
self.core
|
||||
.get_release_watch(limit, offset)
|
||||
.get_release_watch(release_type, limit, offset)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user