fix(award): normalize award.id as string end-to-end so /award/* calls stop sending 'null'
The log showed 'award=null' reaching /award/getAlbums and Qobuz replying '400 Bad Request: Invalid argument: award_id (accepted type are number)'. Root cause: AlbumAward.id was typed Option<u64> and /album/get returns award ids as strings; serde dropped them to None, frontend stringified null → 'null', we sent that as the query param. Fix: - AlbumAward.id becomes Option<String> with a custom deserializer that accepts both string and integer JSON values. - TS types + AlbumDetailView Award interface + pickAlbumRibbon all use string id (compared as '88'/'151' now). - Sidebar button is disabled when id is falsy; handleAwardClick early-returns on empty id so we never navigate to award 'null'. - Better body/status logging in get_award_albums stays so future 4xx responses are visible in the log. Also switches /award/page and /award/getAlbums to signed_get_auth (app_id + user token). Mobile does the same — catalog visibility is region-scoped and the unsigned variant returns 400. cc
This commit is contained in:
7 files changed
+117
-33
No files matched your search
@@ -851,19 +851,31 @@ pub struct PageArtistAward {
|
||||
/// Award attached to an album. Shape is intentionally lenient because
|
||||
/// Qobuz returns different field types across endpoints:
|
||||
/// - `/discover/index` — id=int, name=string, awarded_at="YYYY-MM-DD"
|
||||
/// - `/album/get` — id may be missing, awarded_at may be Unix epoch int
|
||||
/// Keeps the parent Album struct from blowing up on any of those
|
||||
/// variations while still giving the UI enough to render a ribbon.
|
||||
/// - `/album/get` — id=string, awarded_at may be Unix epoch int
|
||||
/// id is emitted as String downstream so the frontend has a single
|
||||
/// type to carry into /award/page and /award/getAlbums.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct AlbumAward {
|
||||
#[serde(default)]
|
||||
pub id: Option<u64>,
|
||||
#[serde(default, deserialize_with = "deserialize_award_id")]
|
||||
pub id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub name: String,
|
||||
#[serde(default, deserialize_with = "deserialize_award_awarded_at")]
|
||||
pub awarded_at: Option<String>,
|
||||
}
|
||||
|
||||
fn deserialize_award_id<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let value = Option::<serde_json::Value>::deserialize(deserializer)?;
|
||||
Ok(match value {
|
||||
Some(serde_json::Value::String(s)) if !s.is_empty() => Some(s),
|
||||
Some(serde_json::Value::Number(n)) => Some(n.to_string()),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
fn deserialize_award_awarded_at<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
|
||||
Reference in new issue
Block a user