feat(app): subscription grace clock and offline purge in the shared activation

The 30-day offline grace (D4) was produced by the Qt shell alone, and only
when a login was REJECTED; its purge predicate had no caller in the live
tree, so past the window the CMAF cache was blocked from playing but never
removed (Tauri purged it; Slint recorded the gap). With member accounts now
signing in, the clock has to key on the entitlement, not on a rejection,
and it has to run wherever a session activates.

`qbz_app::subscription_lifecycle` records the verdict from the core's
session (`offline_streaming` clears the clock; a member keeps it running
from its first observation) and purges the offline cache once the window
has passed, through the cache a host registers with
`AppRuntime::set_offline_cache`. `activate_at` runs it for every host (Qt
via `activate`, qbzd directly), a daily ticker per session re-checks it,
and a cache registered after activation is enforced against on
registration. qbzd registers no cache and keeps the verdict. The Qt
producers are gone; `offline_playback_allowed` stays the read gate.

cc
This commit is contained in:
vicrodh committed 2026-09-02 07:41:09 -06:00
1 parent 2b7d804f09
commit 2e34112868
8 files changed
+681 -214

No files matched your search

+340 -135
View File
@@ -485,6 +485,12 @@ impl QobuzClient {
.and_then(|s| s.language_code.clone())
}
/// A copy of the active session (entitlements included), `None` before
/// login. The shared session lifecycle reads the verdict from here.
pub async fn session(&self) -> Option<UserSession> {
self.session.read().await.clone()
}
/// Exchange an OAuth code for a full user session.
///
/// This implements the new Qobuz OAuth flow:
@@ -792,7 +798,15 @@ impl QobuzClient {
let url = endpoints::build_url(paths::ALBUM_SEARCH);
let timestamp = get_timestamp();
let secret = self.secret().await?;
let signature = sign_search("albumsearch", query, limit, offset, search_type, timestamp, &secret);
let signature = sign_search(
"albumsearch",
query,
limit,
offset,
search_type,
timestamp,
&secret,
);
let limit_str = limit.to_string();
let offset_str = offset.to_string();
let ts_str = timestamp.to_string();
@@ -838,7 +852,15 @@ impl QobuzClient {
let url = endpoints::build_url(paths::TRACK_SEARCH);
let timestamp = get_timestamp();
let secret = self.secret().await?;
let signature = sign_search("tracksearch", query, limit, offset, search_type, timestamp, &secret);
let signature = sign_search(
"tracksearch",
query,
limit,
offset,
search_type,
timestamp,
&secret,
);
let limit_str = limit.to_string();
let offset_str = offset.to_string();
let ts_str = timestamp.to_string();
@@ -884,7 +906,15 @@ impl QobuzClient {
let url = endpoints::build_url(paths::ARTIST_SEARCH);
let timestamp = get_timestamp();
let secret = self.secret().await?;
let signature = sign_search("artistsearch", query, limit, offset, search_type, timestamp, &secret);
let signature = sign_search(
"artistsearch",
query,
limit,
offset,
search_type,
timestamp,
&secret,
);
let limit_str = limit.to_string();
let offset_str = offset.to_string();
let ts_str = timestamp.to_string();
@@ -924,7 +954,15 @@ impl QobuzClient {
let url = endpoints::build_url(paths::CATALOG_SEARCH);
let timestamp = get_timestamp();
let secret = self.secret().await?;
let signature = sign_search("catalogsearch", query, limit, offset, None, timestamp, &secret);
let signature = sign_search(
"catalogsearch",
query,
limit,
offset,
None,
timestamp,
&secret,
);
let limit_str = limit.to_string();
let offset_str = offset.to_string();
let ts_str = timestamp.to_string();
@@ -956,11 +994,15 @@ impl QobuzClient {
) -> Result<SearchResultsPage<Artist>> {
let url = endpoints::build_url(paths::ARTIST_GET_SIMILAR);
let http_response = self
.signed_get(&url, "artistgetSimilarArtists", &[
("artist_id", artist_id.to_string()),
("limit", limit.to_string()),
("offset", offset.to_string()),
])
.signed_get(
&url,
"artistgetSimilarArtists",
&[
("artist_id", artist_id.to_string()),
("limit", limit.to_string()),
("offset", offset.to_string()),
],
)
.await?;
log::debug!(
"[API] get_similar_artists({}) status={}",
@@ -987,13 +1029,17 @@ impl QobuzClient {
let locale = self.locale().await;
let http_response = self
.signed_get(&url, "artistget", &[
("artist_id", artist_id.to_string()),
("extra", "tracks".to_string()),
("lang", locale),
("limit", limit.to_string()),
("offset", offset.to_string()),
])
.signed_get(
&url,
"artistget",
&[
("artist_id", artist_id.to_string()),
("extra", "tracks".to_string()),
("lang", locale),
("limit", limit.to_string()),
("offset", offset.to_string()),
],
)
.await?;
log::debug!(
"[API] get_artist_tracks({}) status={}",
@@ -1061,10 +1107,9 @@ impl QobuzClient {
query.push(("genre_id".to_string(), gid.to_string()));
}
let params: Vec<(&str, String)> = query.iter().map(|(k, v)| (k.as_str(), v.clone())).collect();
let http_response = self
.signed_get(&url, "albumgetFeatured", &params)
.await?;
let params: Vec<(&str, String)> =
query.iter().map(|(k, v)| (k.as_str(), v.clone())).collect();
let http_response = self.signed_get(&url, "albumgetFeatured", &params).await?;
log::debug!(
"[API] get_featured_albums({}) status={}",
featured_type,
@@ -1148,10 +1193,7 @@ impl QobuzClient {
if let Some(items_arr) = items_value.as_array_mut() {
for item in items_arr {
if let Some(obj) = item.as_object_mut() {
let needs_backfill = obj
.get("artist")
.map(|v| v.is_null())
.unwrap_or(true);
let needs_backfill = obj.get("artist").map(|v| v.is_null()).unwrap_or(true);
if needs_backfill {
if let Some(first_artist) = obj
.get("artists")
@@ -1269,17 +1311,20 @@ impl QobuzClient {
/// Qobuz radio for an artist (`/radio/artist`) — a generated track
/// list. Ported from the legacy api client.
pub async fn get_radio_artist(&self, artist_id: &str) -> Result<RadioResponse> {
self.get_radio(paths::RADIO_ARTIST, "artist_id", artist_id).await
self.get_radio(paths::RADIO_ARTIST, "artist_id", artist_id)
.await
}
/// Qobuz radio for a track (`/radio/track`).
pub async fn get_radio_track(&self, track_id: &str) -> Result<RadioResponse> {
self.get_radio(paths::RADIO_TRACK, "track_id", track_id).await
self.get_radio(paths::RADIO_TRACK, "track_id", track_id)
.await
}
/// Qobuz radio for an album (`/radio/album`).
pub async fn get_radio_album(&self, album_id: &str) -> Result<RadioResponse> {
self.get_radio(paths::RADIO_ALBUM, "album_id", album_id).await
self.get_radio(paths::RADIO_ALBUM, "album_id", album_id)
.await
}
async fn get_radio(&self, path: &str, key: &str, id: &str) -> Result<RadioResponse> {
@@ -1312,7 +1357,14 @@ impl QobuzClient {
}
let http_response = self
.signed_get(&url, "genrelist", &query.iter().map(|(k, v)| (*k, v.clone())).collect::<Vec<_>>())
.signed_get(
&url,
"genrelist",
&query
.iter()
.map(|(k, v)| (*k, v.clone()))
.collect::<Vec<_>>(),
)
.await?;
log::debug!(
"[API] get_genres(parent={:?}) status={}",
@@ -1350,7 +1402,14 @@ impl QobuzClient {
}
let http_response = self
.signed_get_auth(&url, "discoverindex", &query.iter().map(|(k, v)| (*k, v.clone())).collect::<Vec<_>>())
.signed_get_auth(
&url,
"discoverindex",
&query
.iter()
.map(|(k, v)| (*k, v.clone()))
.collect::<Vec<_>>(),
)
.await?;
log::info!(
"[API] get_discover_index genre_ids={:?} status={}",
@@ -1404,7 +1463,14 @@ impl QobuzClient {
// Derive method name from endpoint path: "/discover/newReleases" -> "discovernewReleases"
let method_name = endpoint.replace('/', "").replace('.', "");
let http_response = self
.signed_get_auth(&url, &method_name, &query.iter().map(|(k, v)| (*k, v.clone())).collect::<Vec<_>>())
.signed_get_auth(
&url,
&method_name,
&query
.iter()
.map(|(k, v)| (*k, v.clone()))
.collect::<Vec<_>>(),
)
.await?;
log::info!(
"[API] get_discover_albums({}) query={:?} status={}",
@@ -1462,7 +1528,14 @@ impl QobuzClient {
// First get raw JSON to debug structure
let raw_response: serde_json::Value = self
.signed_get_auth(&url, "discoverplaylists", &query.iter().map(|(k, v)| (*k, v.clone())).collect::<Vec<_>>())
.signed_get_auth(
&url,
"discoverplaylists",
&query
.iter()
.map(|(k, v)| (*k, v.clone()))
.collect::<Vec<_>>(),
)
.await?
.json()
.await?;
@@ -1497,9 +1570,7 @@ impl QobuzClient {
pub async fn get_playlist_tags(&self) -> Result<Vec<PlaylistTag>> {
let url = endpoints::build_url(paths::PLAYLIST_GET_TAGS);
let http_response = self
.signed_get_auth(&url, "playlistgetTags", &[])
.await?;
let http_response = self.signed_get_auth(&url, "playlistgetTags", &[]).await?;
log::info!("[API] get_playlist_tags status={}", http_response.status());
let raw: PlaylistTagsResponse = http_response.json().await?;
@@ -1737,9 +1808,7 @@ impl QobuzClient {
match serde_json::from_str::<QobuzLyricsDocument>(&body) {
Ok(mut doc) if doc.original.is_some() || doc.translation.is_some() => {
if language.is_some() && doc.original.is_some() && doc.translation.is_none() {
doc.translation = self
.fetch_translation_fallback(track_id, &urls)
.await;
doc.translation = self.fetch_translation_fallback(track_id, &urls).await;
}
Ok(Some(doc))
}
@@ -1811,7 +1880,14 @@ impl QobuzClient {
];
let http_response = self
.signed_get(&url, "artistget", &query.iter().map(|(k, v)| (*k, v.clone())).collect::<Vec<_>>())
.signed_get(
&url,
"artistget",
&query
.iter()
.map(|(k, v)| (*k, v.clone()))
.collect::<Vec<_>>(),
)
.await?;
log::debug!(
"[API] get_artist_basic({}) status={}",
@@ -1851,7 +1927,14 @@ impl QobuzClient {
}
let http_response = self
.signed_get(&url, "artistget", &query.iter().map(|(k, v)| (*k, v.clone())).collect::<Vec<_>>())
.signed_get(
&url,
"artistget",
&query
.iter()
.map(|(k, v)| (*k, v.clone()))
.collect::<Vec<_>>(),
)
.await?;
log::debug!(
"[API] get_artist_detail({}) status={}",
@@ -1902,7 +1985,14 @@ impl QobuzClient {
}
let http_response = self
.signed_get(&url, "artistget", &query.iter().map(|(k, v)| (*k, v.clone())).collect::<Vec<_>>())
.signed_get(
&url,
"artistget",
&query
.iter()
.map(|(k, v)| (*k, v.clone()))
.collect::<Vec<_>>(),
)
.await?;
log::debug!(
"[API] get_artist({}, albums={}) status={}",
@@ -1921,10 +2011,14 @@ impl QobuzClient {
pub async fn get_playlist_track_ids(&self, playlist_id: u64) -> Result<PlaylistWithTrackIds> {
let url = endpoints::build_url(paths::PLAYLIST_GET);
let http_response = self
.signed_get(&url, "playlistget", &[
("playlist_id", playlist_id.to_string()),
("extra", "track_ids".to_string()),
])
.signed_get(
&url,
"playlistget",
&[
("playlist_id", playlist_id.to_string()),
("extra", "track_ids".to_string()),
],
)
.await?;
log::debug!(
"[API] get_playlist_track_ids({}) status={}",
@@ -1976,8 +2070,17 @@ impl QobuzClient {
let headers = self.api_headers().await?;
let timestamp = get_timestamp();
let secret = self.secret().await?;
let ids_str: String = track_ids.iter().map(|id| id.to_string()).collect::<Vec<_>>().join(",");
let sig = sign_request("trackgetList", &[("tracks_id", &ids_str)], timestamp, &secret);
let ids_str: String = track_ids
.iter()
.map(|id| id.to_string())
.collect::<Vec<_>>()
.join(",");
let sig = sign_request(
"trackgetList",
&[("tracks_id", &ids_str)],
timestamp,
&secret,
);
let body = serde_json::json!({ "tracks_id": track_ids });
log::debug!("[API] get_tracks_batch POST ({} IDs)", track_ids.len());
@@ -2029,12 +2132,16 @@ impl QobuzClient {
// First page — gives us metadata + total track count
let http_response = self
.signed_get(&url, "playlistget", &[
("playlist_id", playlist_id.to_string()),
("limit", PAGE_SIZE.to_string()),
("offset", "0".to_string()),
("extra", "tracks".to_string()),
])
.signed_get(
&url,
"playlistget",
&[
("playlist_id", playlist_id.to_string()),
("limit", PAGE_SIZE.to_string()),
("offset", "0".to_string()),
("extra", "tracks".to_string()),
],
)
.await?;
log::debug!(
"[API] get_playlist({}) status={}",
@@ -2082,7 +2189,12 @@ impl QobuzClient {
let ts = get_timestamp();
let sig = sign_request(
"playlistget",
&[("extra", "tracks"), ("limit", &limit), ("offset", &offset_str), ("playlist_id", &pid)],
&[
("extra", "tracks"),
("limit", &limit),
("offset", &offset_str),
("playlist_id", &pid),
],
ts,
&secret,
);
@@ -2165,21 +2277,18 @@ impl QobuzClient {
/// Enumerate the award catalog (/award/explore). Paginated.
/// Returns raw JSON items so callers can extract just (id, name).
pub async fn get_award_explore(
&self,
limit: u32,
offset: u32,
) -> Result<serde_json::Value> {
pub async fn get_award_explore(&self, limit: u32, offset: u32) -> Result<serde_json::Value> {
let url = endpoints::build_url(paths::AWARD_EXPLORE);
log::debug!("[API] get_award_explore(limit={}, offset={})", limit, offset);
log::debug!(
"[API] get_award_explore(limit={}, offset={})",
limit,
offset
);
let response: serde_json::Value = self
.signed_get_auth(
&url,
"awardexplore",
&[
("limit", limit.to_string()),
("offset", offset.to_string()),
],
&[("limit", limit.to_string()), ("offset", offset.to_string())],
)
.await?
.json()
@@ -2250,14 +2359,14 @@ impl QobuzClient {
}
// Current shape: V2AlbumGenericListDto {has_more, items}.
let mut items_value = response.get("items").cloned().unwrap_or(serde_json::Value::Null);
let mut items_value = response
.get("items")
.cloned()
.unwrap_or(serde_json::Value::Null);
if let Some(items_arr) = items_value.as_array_mut() {
for item in items_arr {
if let Some(obj) = item.as_object_mut() {
let needs_backfill = obj
.get("artist")
.map(|v| v.is_null())
.unwrap_or(true);
let needs_backfill = obj.get("artist").map(|v| v.is_null()).unwrap_or(true);
if needs_backfill {
if let Some(first_artist) = obj
.get("artists")
@@ -2313,7 +2422,11 @@ impl QobuzClient {
offset
);
let response: serde_json::Value = self
.signed_get(&url, "labelexplore", &[("limit", limit.to_string()), ("offset", offset.to_string())])
.signed_get(
&url,
"labelexplore",
&[("limit", limit.to_string()), ("offset", offset.to_string())],
)
.await?
.json()
.await?;
@@ -2342,13 +2455,28 @@ impl QobuzClient {
("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())); }
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);
log::debug!(
"[API] get_label_albums({}, limit={}, offset={})",
label_id,
limit,
offset
);
let response: Value = self
.signed_get(&url, "labelgetalbums", &params)
.await?
@@ -2371,7 +2499,9 @@ impl QobuzClient {
("limit", limit.to_string()),
("offset", offset.to_string()),
];
if let Some(v) = genre_ids { params.push(("genre_ids", v.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
@@ -2398,9 +2528,15 @@ impl QobuzClient {
("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) = 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
@@ -2421,11 +2557,15 @@ impl QobuzClient {
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()),
])
.signed_get(
&url,
"labelgetplaylists",
&[
("label_id", label_id.to_string()),
("limit", limit.to_string()),
("offset", offset.to_string()),
],
)
.await?
.json()
.await?;
@@ -2442,11 +2582,15 @@ impl QobuzClient {
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()),
])
.signed_get(
&url,
"labelgettopartists",
&[
("label_id", label_id.to_string()),
("limit", limit.to_string()),
("offset", offset.to_string()),
],
)
.await?
.json()
.await?;
@@ -2463,11 +2607,15 @@ impl QobuzClient {
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()),
])
.signed_get(
&url,
"labelstory",
&[
("label_id", label_id.to_string()),
("limit", limit.to_string()),
("offset", offset.to_string()),
],
)
.await?
.json()
.await?;
@@ -2489,7 +2637,12 @@ impl QobuzClient {
.map(|id| id.to_string())
.collect::<Vec<_>>()
.join(",");
let sig = sign_request("labelgetList", &[("label_ids", &ids_str)], timestamp, &secret);
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());
@@ -2669,9 +2822,11 @@ impl QobuzClient {
// restriction — every quality would 403 the same way. Abort the
// whole fallback loop immediately instead of firing 5 more
// requests per track and feeding the storm (issue #637).
Err(e @ (ApiError::Forbidden(_)
| ApiError::ForbiddenCircuitOpen(_)
| ApiError::AuthenticationError(_))) => {
Err(
e @ (ApiError::Forbidden(_)
| ApiError::ForbiddenCircuitOpen(_)
| ApiError::AuthenticationError(_)),
) => {
log::warn!("Stream URL aborting quality fallback: {}", e);
return Err(e);
}
@@ -2769,7 +2924,15 @@ impl QobuzClient {
let url = endpoints::build_url(paths::PLAYLIST_SEARCH);
let timestamp = get_timestamp();
let secret = self.secret().await?;
let signature = sign_search("playlistsearch", query, limit, offset, None, timestamp, &secret);
let signature = sign_search(
"playlistsearch",
query,
limit,
offset,
None,
timestamp,
&secret,
);
let limit_str = limit.to_string();
let offset_str = offset.to_string();
let ts_str = timestamp.to_string();
@@ -2828,8 +2991,12 @@ impl QobuzClient {
pub async fn delete_playlist(&self, playlist_id: u64) -> Result<()> {
let url = endpoints::build_url(paths::PLAYLIST_DELETE);
self.signed_get_auth(&url, "playlistdelete", &[("playlist_id", playlist_id.to_string())])
.await?;
self.signed_get_auth(
&url,
"playlistdelete",
&[("playlist_id", playlist_id.to_string())],
)
.await?;
Ok(())
}
@@ -2843,10 +3010,14 @@ impl QobuzClient {
.collect::<Vec<_>>()
.join(",");
self.signed_get_auth(&url, "playlistaddTracks", &[
("playlist_id", playlist_id.to_string()),
("track_ids", track_ids_str),
])
self.signed_get_auth(
&url,
"playlistaddTracks",
&[
("playlist_id", playlist_id.to_string()),
("track_ids", track_ids_str),
],
)
.await?;
Ok(())
@@ -2865,10 +3036,14 @@ impl QobuzClient {
.collect::<Vec<_>>()
.join(",");
self.signed_get_auth(&url, "playlistdeleteTracks", &[
("playlist_id", playlist_id.to_string()),
("playlist_track_ids", track_ids_str),
])
self.signed_get_auth(
&url,
"playlistdeleteTracks",
&[
("playlist_id", playlist_id.to_string()),
("playlist_track_ids", track_ids_str),
],
)
.await?;
Ok(())
@@ -2919,11 +3094,15 @@ impl QobuzClient {
insert_before
);
self.signed_get_auth(&url, "playlistupdateTracksPosition", &[
("playlist_id", playlist_id.to_string()),
("playlist_track_ids", track_ids_str),
("insert_before", insert_before.to_string()),
])
self.signed_get_auth(
&url,
"playlistupdateTracksPosition",
&[
("playlist_id", playlist_id.to_string()),
("playlist_track_ids", track_ids_str),
("insert_before", insert_before.to_string()),
],
)
.await?;
Ok(())
@@ -2965,7 +3144,11 @@ impl QobuzClient {
let url = endpoints::build_url(paths::PLAYLIST_SUBSCRIBE);
let response = self
.signed_get_auth(&url, "playlistsubscribe", &[("playlist_id", playlist_id.to_string())])
.signed_get_auth(
&url,
"playlistsubscribe",
&[("playlist_id", playlist_id.to_string())],
)
.await?;
let status = response.status();
@@ -2985,7 +3168,11 @@ impl QobuzClient {
let url = endpoints::build_url(paths::PLAYLIST_UNSUBSCRIBE);
let response = self
.signed_get_auth(&url, "playlistunsubscribe", &[("playlist_id", playlist_id.to_string())])
.signed_get_auth(
&url,
"playlistunsubscribe",
&[("playlist_id", playlist_id.to_string())],
)
.await?;
let status = response.status();
@@ -3006,7 +3193,11 @@ impl QobuzClient {
let type_key = format!("{}_ids", fav_type); // album_ids, track_ids, artist_ids
let response = self
.signed_get_auth(&url, "favoritecreate", &[(type_key.as_str(), item_id.to_string())])
.signed_get_auth(
&url,
"favoritecreate",
&[(type_key.as_str(), item_id.to_string())],
)
.await?;
if response.status().is_success() {
@@ -3025,7 +3216,11 @@ impl QobuzClient {
let type_key = format!("{}_ids", fav_type);
let response = self
.signed_get_auth(&url, "favoritedelete", &[(type_key.as_str(), item_id.to_string())])
.signed_get_auth(
&url,
"favoritedelete",
&[(type_key.as_str(), item_id.to_string())],
)
.await?;
if response.status().is_success() {
@@ -3054,7 +3249,14 @@ impl QobuzClient {
log::debug!("[API] get_artist_page({}) sort={:?}", artist_id, sort);
let response: serde_json::Value = self
.signed_get(&url, "artistpage", &query.iter().map(|(k, v)| (*k, v.clone())).collect::<Vec<_>>())
.signed_get(
&url,
"artistpage",
&query
.iter()
.map(|(k, v)| (*k, v.clone()))
.collect::<Vec<_>>(),
)
.await?
.json()
.await?;
@@ -3090,7 +3292,14 @@ impl QobuzClient {
offset
);
let response: serde_json::Value = self
.signed_get(&url, "artistgetReleasesGrid", &query.iter().map(|(k, v)| (*k, v.clone())).collect::<Vec<_>>())
.signed_get(
&url,
"artistgetReleasesGrid",
&query
.iter()
.map(|(k, v)| (*k, v.clone()))
.collect::<Vec<_>>(),
)
.await?
.json()
.await?;
@@ -3120,7 +3329,14 @@ impl QobuzClient {
limit
);
let response: serde_json::Value = self
.signed_get(&url, "artiststory", &query.iter().map(|(k, v)| (*k, v.clone())).collect::<Vec<_>>())
.signed_get(
&url,
"artiststory",
&query
.iter()
.map(|(k, v)| (*k, v.clone()))
.collect::<Vec<_>>(),
)
.await?
.json()
.await?;
@@ -3230,11 +3446,7 @@ impl QobuzClient {
///
/// This is the new streaming endpoint that returns encrypted CMAF segments
/// instead of a direct file URL.
pub async fn get_file_url(
&self,
track_id: u64,
quality: Quality,
) -> Result<TrackFileUrl> {
pub async fn get_file_url(&self, track_id: u64, quality: Quality) -> Result<TrackFileUrl> {
let format_id = quality.id();
let url = endpoints::build_url(paths::FILE_URL);
@@ -3295,11 +3507,7 @@ impl QobuzClient {
let code = status.as_u16();
if status == reqwest::StatusCode::FORBIDDEN {
let preview = body_preview(response).await;
log::warn!(
"[CMAF] file/url 403 for track {}{}",
track_id,
preview
);
log::warn!("[CMAF] file/url 403 for track {}{}", track_id, preview);
return Err(ApiError::Forbidden(preview));
}
return Err(if code == 404 {
@@ -3316,10 +3524,7 @@ impl QobuzClient {
} else if status.is_server_error() {
ApiError::ServerError(code)
} else {
ApiError::ApiResponse(format!(
"file/url failed with status {}",
status
))
ApiError::ApiResponse(format!("file/url failed with status {}", status))
});
}