diff --git a/crates/qbz-core/src/core.rs b/crates/qbz-core/src/core.rs index 0605b9ccf..2150918c5 100644 --- a/crates/qbz-core/src/core.rs +++ b/crates/qbz-core/src/core.rs @@ -760,11 +760,11 @@ impl QbzCore { .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, CoreError> { @@ -772,7 +772,7 @@ impl QbzCore { 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) } diff --git a/crates/qbz-qobuz/src/client.rs b/crates/qbz-qobuz/src/client.rs index b218eb02d..2b71da243 100644 --- a/crates/qbz-qobuz/src/client.rs +++ b/crates/qbz-qobuz/src/client.rs @@ -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> { - 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 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::>(page_value) { Ok(page) => Ok(page), diff --git a/crates/qbz-qobuz/src/endpoints.rs b/crates/qbz-qobuz/src/endpoints.rs index c47b89240..d1d17e166 100644 --- a/crates/qbz-qobuz/src/endpoints.rs +++ b/crates/qbz-qobuz/src/endpoints.rs @@ -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"; diff --git a/src-tauri/src/commands_v2/catalog.rs b/src-tauri/src/commands_v2/catalog.rs index 662a49356..50898e00a 100644 --- a/src-tauri/src/commands_v2/catalog.rs +++ b/src-tauri/src/commands_v2/catalog.rs @@ -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, limit: Option, offset: Option, 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)?; diff --git a/src-tauri/src/core_bridge.rs b/src-tauri/src/core_bridge.rs index d1a2b12d1..c66ce39cc 100644 --- a/src-tauri/src/core_bridge.rs +++ b/src-tauri/src/core_bridge.rs @@ -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, String> { self.core - .get_release_watch(limit, offset) + .get_release_watch(release_type, limit, offset) .await .map_err(|e| e.to_string()) }