feat(api): add purchases-by-type, purchase-ids, and dynamic-suggest endpoints
New API client methods: - get_user_purchases_page_typed: fetch purchases filtered by type (album/track) - get_user_purchases_ids_page_typed: fetch purchase IDs by type - get_user_purchases_all_typed: paginated fetch for a single purchase type - get_user_purchases_ids_page: generic purchase IDs page - get_dynamic_suggest / get_dynamic_suggest_raw: Qobuz dynamic suggestions New models: DynamicSuggestRequest/Response/Track, DynamicTrackToAnalyse, PurchaseIdsResponse, lenient serde deserializers for robust API parsing.
This commit is contained in:
1 parent
3a297c0b2b
commit
a7ffe13469
3 files changed
+681
-125
No files matched your search
+507
-124
File diff suppressed because it is too large.
Load diff
@@ -7,6 +7,8 @@ pub mod paths {
|
|||||||
// User
|
// User
|
||||||
pub const USER_LOGIN: &str = "/user/login";
|
pub const USER_LOGIN: &str = "/user/login";
|
||||||
pub const PURCHASE_GET_USER_PURCHASES: &str = "/purchase/getUserPurchases";
|
pub const PURCHASE_GET_USER_PURCHASES: &str = "/purchase/getUserPurchases";
|
||||||
|
pub const PURCHASE_GET_USER_PURCHASES_IDS: &str = "/purchase/getUserPurchasesIds";
|
||||||
|
pub const DYNAMIC_SUGGEST: &str = "/dynamic/suggest";
|
||||||
|
|
||||||
// Track
|
// Track
|
||||||
pub const TRACK_GET: &str = "/track/get";
|
pub const TRACK_GET: &str = "/track/get";
|
||||||
|
|||||||
+172
-1
@@ -17,6 +17,103 @@ where
|
|||||||
Ok(serde_json::from_value(value).ok())
|
Ok(serde_json::from_value(value).ok())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn lenient_page<'de, D, T>(deserializer: D) -> Result<SearchResultsPage<T>, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
T: DeserializeOwned,
|
||||||
|
{
|
||||||
|
let value = serde_json::Value::deserialize(deserializer)?;
|
||||||
|
if value.is_null() {
|
||||||
|
return Ok(SearchResultsPage {
|
||||||
|
items: Vec::new(),
|
||||||
|
total: 0,
|
||||||
|
offset: 0,
|
||||||
|
limit: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(serde_json::from_value(value).unwrap_or(SearchResultsPage {
|
||||||
|
items: Vec::new(),
|
||||||
|
total: 0,
|
||||||
|
offset: 0,
|
||||||
|
limit: 0,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lenient_page_flexible<'de, D, T>(deserializer: D) -> Result<SearchResultsPage<T>, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
T: DeserializeOwned,
|
||||||
|
{
|
||||||
|
let value = serde_json::Value::deserialize(deserializer)?;
|
||||||
|
if value.is_null() {
|
||||||
|
return Ok(SearchResultsPage {
|
||||||
|
items: Vec::new(),
|
||||||
|
total: 0,
|
||||||
|
offset: 0,
|
||||||
|
limit: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let items: Vec<T> = value
|
||||||
|
.get("items")
|
||||||
|
.and_then(|v| v.as_array())
|
||||||
|
.map(|arr| {
|
||||||
|
arr.iter()
|
||||||
|
.filter_map(|item| serde_json::from_value::<T>(item.clone()).ok())
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let limit = value
|
||||||
|
.get("limit")
|
||||||
|
.and_then(|v| v.as_u64())
|
||||||
|
.map(|v| v as u32)
|
||||||
|
.unwrap_or(items.len() as u32);
|
||||||
|
|
||||||
|
let offset = value
|
||||||
|
.get("offset")
|
||||||
|
.and_then(|v| v.as_u64())
|
||||||
|
.map(|v| v as u32)
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
let total = value
|
||||||
|
.get("total")
|
||||||
|
.and_then(|v| v.as_u64())
|
||||||
|
.map(|v| v as u32)
|
||||||
|
.unwrap_or(items.len() as u32);
|
||||||
|
|
||||||
|
Ok(SearchResultsPage {
|
||||||
|
items,
|
||||||
|
total,
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_string_id<'de, D>(deserializer: D) -> Result<String, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let value = serde_json::Value::deserialize(deserializer)?;
|
||||||
|
Ok(match value {
|
||||||
|
serde_json::Value::String(v) => v,
|
||||||
|
serde_json::Value::Number(v) => v.to_string(),
|
||||||
|
_ => String::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_u64_id<'de, D>(deserializer: D) -> Result<u64, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let value = serde_json::Value::deserialize(deserializer)?;
|
||||||
|
Ok(match value {
|
||||||
|
serde_json::Value::Number(v) => v.as_u64().unwrap_or(0),
|
||||||
|
serde_json::Value::String(v) => v.parse::<u64>().unwrap_or(0),
|
||||||
|
_ => 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn serde_true() -> bool {
|
fn serde_true() -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -396,17 +493,40 @@ pub struct SearchResultsPage<T> {
|
|||||||
pub limit: u32,
|
pub limit: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T> Default for SearchResultsPage<T> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
items: Vec::new(),
|
||||||
|
total: 0,
|
||||||
|
offset: 0,
|
||||||
|
limit: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============ Purchases API Models ============
|
// ============ Purchases API Models ============
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct PurchaseResponse {
|
pub struct PurchaseResponse {
|
||||||
|
#[serde(default, deserialize_with = "lenient_page")]
|
||||||
pub albums: SearchResultsPage<PurchaseAlbum>,
|
pub albums: SearchResultsPage<PurchaseAlbum>,
|
||||||
|
#[serde(default, deserialize_with = "lenient_page")]
|
||||||
pub tracks: SearchResultsPage<PurchaseTrack>,
|
pub tracks: SearchResultsPage<PurchaseTrack>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct PurchaseIdsResponse {
|
||||||
|
#[serde(default, deserialize_with = "lenient_page")]
|
||||||
|
pub albums: SearchResultsPage<serde_json::Value>,
|
||||||
|
#[serde(default, deserialize_with = "lenient_page")]
|
||||||
|
pub tracks: SearchResultsPage<serde_json::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct PurchaseAlbum {
|
pub struct PurchaseAlbum {
|
||||||
|
#[serde(default, deserialize_with = "deserialize_string_id")]
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
#[serde(default)]
|
||||||
pub title: String,
|
pub title: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub artist: Artist,
|
pub artist: Artist,
|
||||||
@@ -434,13 +554,15 @@ pub struct PurchaseAlbum {
|
|||||||
pub downloaded: bool,
|
pub downloaded: bool,
|
||||||
#[serde(default, deserialize_with = "lenient_option")]
|
#[serde(default, deserialize_with = "lenient_option")]
|
||||||
pub purchased_at: Option<i64>,
|
pub purchased_at: Option<i64>,
|
||||||
#[serde(default)]
|
#[serde(default, deserialize_with = "lenient_option")]
|
||||||
pub tracks: Option<SearchResultsPage<PurchaseTrack>>,
|
pub tracks: Option<SearchResultsPage<PurchaseTrack>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct PurchaseTrack {
|
pub struct PurchaseTrack {
|
||||||
|
#[serde(default, deserialize_with = "deserialize_u64_id")]
|
||||||
pub id: u64,
|
pub id: u64,
|
||||||
|
#[serde(default)]
|
||||||
pub title: String,
|
pub title: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub track_number: u32,
|
pub track_number: u32,
|
||||||
@@ -466,6 +588,55 @@ pub struct PurchaseTrack {
|
|||||||
pub purchased_at: Option<i64>,
|
pub purchased_at: Option<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============ Dynamic Suggest API Models ============
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct DynamicSuggestRequest {
|
||||||
|
pub limit: u32,
|
||||||
|
#[serde(default)]
|
||||||
|
pub listened_tracks_ids: Vec<u64>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub track_to_analysed: Vec<DynamicTrackToAnalyse>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct DynamicTrackToAnalyse {
|
||||||
|
pub track_id: u64,
|
||||||
|
pub artist_id: u64,
|
||||||
|
pub genre_id: u64,
|
||||||
|
pub label_id: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct DynamicSuggestResponse {
|
||||||
|
#[serde(default)]
|
||||||
|
pub algorithm: String,
|
||||||
|
#[serde(default, deserialize_with = "lenient_page_flexible")]
|
||||||
|
pub tracks: SearchResultsPage<DynamicSuggestTrack>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct DynamicSuggestTrack {
|
||||||
|
#[serde(default, deserialize_with = "deserialize_u64_id")]
|
||||||
|
pub id: u64,
|
||||||
|
#[serde(default)]
|
||||||
|
pub title: String,
|
||||||
|
#[serde(default, deserialize_with = "lenient_option")]
|
||||||
|
pub duration: Option<u32>,
|
||||||
|
#[serde(default, deserialize_with = "lenient_option")]
|
||||||
|
pub performer: Option<Artist>,
|
||||||
|
#[serde(default, deserialize_with = "lenient_option")]
|
||||||
|
pub album: Option<AlbumSummary>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub hires: bool,
|
||||||
|
#[serde(default, deserialize_with = "lenient_option")]
|
||||||
|
pub maximum_sampling_rate: Option<f64>,
|
||||||
|
#[serde(default, deserialize_with = "lenient_option")]
|
||||||
|
pub maximum_bit_depth: Option<u32>,
|
||||||
|
#[serde(default = "serde_true")]
|
||||||
|
pub streamable: bool,
|
||||||
|
}
|
||||||
|
|
||||||
/// Favorites container
|
/// Favorites container
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Favorites {
|
pub struct Favorites {
|
||||||
|
|||||||
Reference in new issue
Block a user