refactor(discover): read Release Watch from /discover/index response

The initial implementation added a dedicated get_release_watch client
method calling /albums/releaseWatch, but that path is only routed on
the mobile API; the web API (api.json/0.2) that qbz-qobuz speaks to
returned 'no Service found with those values'.

The correct source is already available: /discover/index — which we
were already calling for newReleases/pressAwards/qobuzissimes/etc —
also returns a release_watch container when the user has follows. We
were silently dropping it because DiscoverContainers didn't declare
the field.

Now:
- DiscoverContainers (Rust + TS mirror) declares the release_watch
  field with a serde alias that accepts both camelCase and snake_case
  spellings, so future naming churn upstream is tolerated
- HomeView reads it in fetchAllDiscoverData next to the other editorial
  sections (one less network round-trip)
- ForYouTab calls v2_get_discover_index and extracts only the
  release_watch container
- Reverted the custom /albums/releaseWatch endpoint, client method,
  core passthrough, bridge wrapper and v2_get_release_watch command

Section identity (homeSettingsStore id, i18n keys, render blocks)
is unchanged.

cc
This commit is contained in:
vicrodh committed 2026-04-13 22:49:34 -06:00
1 parent 9945e93e0c
commit 60e0861e1d
10 files changed
+48 -196

No files matched your search

-16
View File
@@ -760,22 +760,6 @@ impl<A: FrontendAdapter + Send + Sync + 'static> QbzCore<A> {
.map_err(CoreError::Api)
}
/// Get Release Watch — new releases from followed artists, labels and
/// awards (the "Radar de Novedades" feed in the Qobuz mobile client).
pub async fn get_release_watch(
&self,
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_release_watch(limit, offset)
.await
.map_err(CoreError::Api)
}
/// Get artist page (full artist details with albums, tracks, similar)
pub async fn get_artist_page(
&self,
+8
View File
@@ -548,6 +548,14 @@ pub struct DiscoverContainers {
pub most_streamed: Option<DiscoverContainer<DiscoverAlbum>>,
pub press_awards: Option<DiscoverContainer<DiscoverAlbum>>,
pub album_of_the_week: Option<DiscoverContainer<DiscoverAlbum>>,
/// Release Watch — new releases from artists, labels and awards the user
/// follows. The mobile client surfaces this as "Radar de Novedades" /
/// "Release Watch". Comes back from `/discover/index` alongside the
/// editorial sections; we consume it in the same response rather than
/// calling the separate `/albums/releaseWatch` endpoint used by the
/// Android app (which the web API doesn't route).
#[serde(default, alias = "releaseWatch", alias = "releasewatch")]
pub release_watch: Option<DiscoverContainer<DiscoverAlbum>>,
}
/// Generic discover container
-85
View File
@@ -767,91 +767,6 @@ impl QobuzClient {
Ok(serde_json::from_value(albums.clone())?)
}
/// Get Release Watch — new and upcoming releases from artists, labels
/// and awards the user follows. Exposed on mobile/web as "Release Watch"
/// or "Radar de Novedades".
///
/// Requires the user to be authenticated; the endpoint uses the follows
/// on the caller's account.
pub async fn get_release_watch(
&self,
limit: u32,
offset: u32,
) -> Result<SearchResultsPage<Album>> {
let url = endpoints::build_url(paths::ALBUMS_RELEASE_WATCH);
let params: Vec<(&str, String)> = vec![
("limit", limit.to_string()),
("offset", offset.to_string()),
];
let http_response = self
.signed_get_auth(&url, "albumsreleaseWatch", &params)
.await?;
log::debug!(
"[API] get_release_watch(limit={}, offset={}) status={}",
limit,
offset,
http_response.status()
);
let response: Value = http_response.json().await?;
// The mobile client's /albums/releaseWatch response shape isn't
// documented publicly — it may wrap items in `albums`, `releaseWatch`
// or serve a flat page at the root. Try every known shape and log
// the top-level keys when we fall through so future breakage is
// diagnosable from logs.
if let Some(obj) = response.as_object() {
log::debug!(
"[API] get_release_watch response keys: {:?}",
obj.keys().collect::<Vec<_>>()
);
}
// Candidate wrappers in order of plausibility.
let candidates: &[&str] = &["albums", "releaseWatch", "release_watch", "items_albums"];
for key in candidates {
if let Some(inner) = response.get(*key) {
// The wrapper might be the page directly, or an object with
// a nested `items` array.
if inner.get("items").is_some() {
return Ok(serde_json::from_value(inner.clone())?);
}
// If it's an array, wrap it into a page manually
if let Some(arr) = inner.as_array() {
let page = SearchResultsPage::<Album> {
items: serde_json::from_value(Value::Array(arr.clone()))?,
total: arr.len() as u32,
offset,
limit,
};
return Ok(page);
}
}
}
// Root-level `{items, total, ...}` shape
if response.get("items").is_some() {
return Ok(serde_json::from_value(response)?);
}
// Nothing matched — log and return an empty page instead of erroring,
// so the UI section just hides rather than killing adjacent loaders.
log::warn!(
"[API] get_release_watch: unrecognised response shape; returning empty page. Raw body: {}",
serde_json::to_string(&response)
.unwrap_or_else(|_| "<unserializable>".to_string())
.chars()
.take(400)
.collect::<String>()
);
Ok(SearchResultsPage::<Album> {
items: vec![],
total: 0,
offset,
limit,
})
}
/// Get list of genres
pub async fn get_genres(&self, parent_id: Option<u64>) -> Result<Vec<GenreInfo>> {
let url = endpoints::build_url(paths::GENRE_LIST);
-4
View File
@@ -20,10 +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";
// Release Watch: releases from artists, labels, and awards the user
// follows. Mobile/web apps expose this in Discover as "Release Watch" /
// "Radar de Novedades". Endpoint path uses the `/albums` (plural) prefix.
pub const ALBUMS_RELEASE_WATCH: &str = "/albums/releaseWatch";
// Artist
pub const ARTIST_GET: &str = "/artist/get";