feat: add playlist browse/search discovery and follow controls
Some checks failed
Build for Windows / build-windows (push) Has been cancelled
Some checks failed
Build for Windows / build-windows (push) Has been cancelled
This commit is contained in:
@@ -41,6 +41,11 @@ enum QobuzEvent {
|
||||
EV_GENRES_OK = 27,
|
||||
EV_FEATURED_ALBUMS_OK = 28,
|
||||
EV_DYNAMIC_SUGGEST_OK = 29,
|
||||
EV_FEATURED_PLAYLISTS_OK = 30,
|
||||
EV_DISCOVER_PLAYLISTS_OK = 31,
|
||||
EV_PLAYLIST_SEARCH_OK = 32,
|
||||
EV_PLAYLIST_SUBSCRIBED = 33,
|
||||
EV_PLAYLIST_UNSUBSCRIBED = 34,
|
||||
};
|
||||
|
||||
// Callback signature
|
||||
@@ -96,13 +101,18 @@ void qobuz_backend_get_albums_tracks(QobuzBackendOpaque *backend, const char *al
|
||||
|
||||
// Browse
|
||||
void qobuz_backend_get_genres(QobuzBackendOpaque *backend);
|
||||
void qobuz_backend_get_featured_albums(QobuzBackendOpaque *backend, int64_t genre_id, const char *kind, uint32_t limit, uint32_t offset);
|
||||
void qobuz_backend_get_featured_albums(QobuzBackendOpaque *backend, const char *genre_ids, const char *kind, uint32_t limit, uint32_t offset);
|
||||
void qobuz_backend_get_featured_playlists(QobuzBackendOpaque *backend, const char *genre_ids, const char *kind, uint32_t limit, uint32_t offset);
|
||||
void qobuz_backend_discover_playlists(QobuzBackendOpaque *backend, const char *genre_ids, const char *tags, uint32_t limit, uint32_t offset);
|
||||
void qobuz_backend_search_playlists(QobuzBackendOpaque *backend, const char *query, uint32_t limit, uint32_t offset);
|
||||
|
||||
// Playlist management
|
||||
void qobuz_backend_create_playlist(QobuzBackendOpaque *backend, const char *name);
|
||||
void qobuz_backend_delete_playlist(QobuzBackendOpaque *backend, int64_t playlist_id);
|
||||
void qobuz_backend_add_track_to_playlist(QobuzBackendOpaque *backend, int64_t playlist_id, int64_t track_id);
|
||||
void qobuz_backend_delete_track_from_playlist(QobuzBackendOpaque *backend, int64_t playlist_id, int64_t playlist_track_id);
|
||||
void qobuz_backend_subscribe_playlist(QobuzBackendOpaque *backend, int64_t playlist_id);
|
||||
void qobuz_backend_unsubscribe_playlist(QobuzBackendOpaque *backend, int64_t playlist_id);
|
||||
|
||||
// Favorites modification
|
||||
void qobuz_backend_add_fav_track(QobuzBackendOpaque *backend, int64_t track_id);
|
||||
|
||||
@@ -416,7 +416,7 @@ impl QobuzClient {
|
||||
|
||||
pub async fn get_featured_albums(
|
||||
&self,
|
||||
genre_id: i64,
|
||||
genre_ids: &str,
|
||||
kind: &str,
|
||||
limit: u32,
|
||||
offset: u32,
|
||||
@@ -425,7 +425,64 @@ impl QobuzClient {
|
||||
.get_request("album/getFeatured")
|
||||
.query(&[
|
||||
("type", kind.to_string()),
|
||||
("genre_id", genre_id.to_string()),
|
||||
("genre_id", genre_ids.to_string()),
|
||||
("limit", limit.to_string()),
|
||||
("offset", offset.to_string()),
|
||||
])
|
||||
.send()
|
||||
.await?;
|
||||
Self::check_response(resp).await
|
||||
}
|
||||
|
||||
pub async fn get_featured_playlists(
|
||||
&self,
|
||||
genre_ids: &str,
|
||||
kind: &str,
|
||||
limit: u32,
|
||||
offset: u32,
|
||||
) -> Result<Value> {
|
||||
let resp = self
|
||||
.get_request("playlist/getFeatured")
|
||||
.query(&[
|
||||
("type", kind.to_string()),
|
||||
("genre_ids", genre_ids.to_string()),
|
||||
("limit", limit.to_string()),
|
||||
("offset", offset.to_string()),
|
||||
])
|
||||
.send()
|
||||
.await?;
|
||||
Self::check_response(resp).await
|
||||
}
|
||||
|
||||
pub async fn discover_playlists(
|
||||
&self,
|
||||
genre_ids: &str,
|
||||
tags: &str,
|
||||
limit: u32,
|
||||
offset: u32,
|
||||
) -> Result<Value> {
|
||||
let mut query = vec![
|
||||
("genre_ids", genre_ids.to_string()),
|
||||
("limit", limit.to_string()),
|
||||
("offset", offset.to_string()),
|
||||
];
|
||||
if !tags.is_empty() {
|
||||
query.push(("tags", tags.to_string()));
|
||||
}
|
||||
|
||||
let resp = self
|
||||
.get_request("discover/playlists")
|
||||
.query(&query)
|
||||
.send()
|
||||
.await?;
|
||||
Self::check_response(resp).await
|
||||
}
|
||||
|
||||
pub async fn search_playlists(&self, query: &str, limit: u32, offset: u32) -> Result<Value> {
|
||||
let resp = self
|
||||
.get_request("playlist/search")
|
||||
.query(&[
|
||||
("query", query.to_string()),
|
||||
("limit", limit.to_string()),
|
||||
("offset", offset.to_string()),
|
||||
])
|
||||
@@ -779,6 +836,26 @@ impl QobuzClient {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn subscribe_playlist(&self, playlist_id: i64) -> Result<()> {
|
||||
let resp = self
|
||||
.get_request("playlist/subscribe")
|
||||
.query(&[("playlist_id", playlist_id.to_string())])
|
||||
.send()
|
||||
.await?;
|
||||
Self::check_response(resp).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn unsubscribe_playlist(&self, playlist_id: i64) -> Result<()> {
|
||||
let resp = self
|
||||
.get_request("playlist/unsubscribe")
|
||||
.query(&[("playlist_id", playlist_id.to_string())])
|
||||
.send()
|
||||
.await?;
|
||||
Self::check_response(resp).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn add_fav_track(&self, track_id: i64) -> Result<()> {
|
||||
let resp = self
|
||||
.get_request("favorite/create")
|
||||
|
||||
176
rust/src/lib.rs
176
rust/src/lib.rs
@@ -49,6 +49,11 @@ pub const EV_MOST_POPULAR_OK: c_int = 26;
|
||||
pub const EV_GENRES_OK: c_int = 27;
|
||||
pub const EV_FEATURED_ALBUMS_OK: c_int = 28;
|
||||
pub const EV_DYNAMIC_SUGGEST_OK: c_int = 29;
|
||||
pub const EV_FEATURED_PLAYLISTS_OK: c_int = 30;
|
||||
pub const EV_DISCOVER_PLAYLISTS_OK: c_int = 31;
|
||||
pub const EV_PLAYLIST_SEARCH_OK: c_int = 32;
|
||||
pub const EV_PLAYLIST_SUBSCRIBED: c_int = 33;
|
||||
pub const EV_PLAYLIST_UNSUBSCRIBED: c_int = 34;
|
||||
|
||||
// ---------- Callback ----------
|
||||
|
||||
@@ -481,7 +486,7 @@ pub unsafe extern "C" fn qobuz_backend_get_genres(ptr: *mut Backend) {
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn qobuz_backend_get_featured_albums(
|
||||
ptr: *mut Backend,
|
||||
genre_id: i64,
|
||||
genre_ids: *const c_char,
|
||||
kind: *const c_char,
|
||||
limit: u32,
|
||||
offset: u32,
|
||||
@@ -490,13 +495,14 @@ pub unsafe extern "C" fn qobuz_backend_get_featured_albums(
|
||||
let client = inner.client.clone();
|
||||
let cb = inner.cb;
|
||||
let ud = inner.ud;
|
||||
let genre_ids_str = CStr::from_ptr(genre_ids).to_string_lossy().into_owned();
|
||||
let kind_str = CStr::from_ptr(kind).to_string_lossy().into_owned();
|
||||
|
||||
spawn(inner, async move {
|
||||
let result = client
|
||||
.lock()
|
||||
.await
|
||||
.get_featured_albums(genre_id, &kind_str, limit, offset)
|
||||
.get_featured_albums(&genre_ids_str, &kind_str, limit, offset)
|
||||
.await;
|
||||
match result {
|
||||
Ok(r) => {
|
||||
@@ -506,7 +512,7 @@ pub unsafe extern "C" fn qobuz_backend_get_featured_albums(
|
||||
"items": items,
|
||||
"total": total,
|
||||
"type": kind_str,
|
||||
"genre_id": genre_id,
|
||||
"genre_ids": genre_ids_str,
|
||||
});
|
||||
call_cb(
|
||||
cb,
|
||||
@@ -520,6 +526,132 @@ pub unsafe extern "C" fn qobuz_backend_get_featured_albums(
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn qobuz_backend_get_featured_playlists(
|
||||
ptr: *mut Backend,
|
||||
genre_ids: *const c_char,
|
||||
kind: *const c_char,
|
||||
limit: u32,
|
||||
offset: u32,
|
||||
) {
|
||||
let inner = &(*ptr).0;
|
||||
let client = inner.client.clone();
|
||||
let cb = inner.cb;
|
||||
let ud = inner.ud;
|
||||
let kind_str = CStr::from_ptr(kind).to_string_lossy().into_owned();
|
||||
let genre_ids_str = CStr::from_ptr(genre_ids).to_string_lossy().into_owned();
|
||||
|
||||
spawn(inner, async move {
|
||||
let result = client
|
||||
.lock()
|
||||
.await
|
||||
.get_featured_playlists(&genre_ids_str, &kind_str, limit, offset)
|
||||
.await;
|
||||
match result {
|
||||
Ok(r) => {
|
||||
let items = r["playlists"]["items"].clone();
|
||||
let total = r["playlists"]["total"].as_i64().unwrap_or(0);
|
||||
let out = serde_json::json!({
|
||||
"items": items,
|
||||
"total": total,
|
||||
"type": kind_str,
|
||||
"genre_ids": genre_ids_str,
|
||||
});
|
||||
call_cb(
|
||||
cb,
|
||||
ud,
|
||||
EV_FEATURED_PLAYLISTS_OK,
|
||||
&serde_json::to_string(&out).unwrap_or_default(),
|
||||
);
|
||||
}
|
||||
Err(e) => call_cb(cb, ud, EV_GENERIC_ERR, &err_json(&e.to_string())),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn qobuz_backend_discover_playlists(
|
||||
ptr: *mut Backend,
|
||||
genre_ids: *const c_char,
|
||||
tags: *const c_char,
|
||||
limit: u32,
|
||||
offset: u32,
|
||||
) {
|
||||
let inner = &(*ptr).0;
|
||||
let client = inner.client.clone();
|
||||
let cb = inner.cb;
|
||||
let ud = inner.ud;
|
||||
let genre_ids_str = CStr::from_ptr(genre_ids).to_string_lossy().into_owned();
|
||||
let tags_str = CStr::from_ptr(tags).to_string_lossy().into_owned();
|
||||
|
||||
spawn(inner, async move {
|
||||
let result = client
|
||||
.lock()
|
||||
.await
|
||||
.discover_playlists(&genre_ids_str, &tags_str, limit, offset)
|
||||
.await;
|
||||
match result {
|
||||
Ok(r) => {
|
||||
let items = r["items"].clone();
|
||||
let total = r["total"].as_i64().unwrap_or(0);
|
||||
let out = serde_json::json!({
|
||||
"items": items,
|
||||
"total": total,
|
||||
"genre_ids": genre_ids_str,
|
||||
"tags": tags_str,
|
||||
});
|
||||
call_cb(
|
||||
cb,
|
||||
ud,
|
||||
EV_DISCOVER_PLAYLISTS_OK,
|
||||
&serde_json::to_string(&out).unwrap_or_default(),
|
||||
);
|
||||
}
|
||||
Err(e) => call_cb(cb, ud, EV_GENERIC_ERR, &err_json(&e.to_string())),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn qobuz_backend_search_playlists(
|
||||
ptr: *mut Backend,
|
||||
query: *const c_char,
|
||||
limit: u32,
|
||||
offset: u32,
|
||||
) {
|
||||
let inner = &(*ptr).0;
|
||||
let client = inner.client.clone();
|
||||
let cb = inner.cb;
|
||||
let ud = inner.ud;
|
||||
let query_str = CStr::from_ptr(query).to_string_lossy().into_owned();
|
||||
|
||||
spawn(inner, async move {
|
||||
let result = client
|
||||
.lock()
|
||||
.await
|
||||
.search_playlists(&query_str, limit, offset)
|
||||
.await;
|
||||
match result {
|
||||
Ok(r) => {
|
||||
let items = r["playlists"]["items"].clone();
|
||||
let total = r["playlists"]["total"].as_i64().unwrap_or(0);
|
||||
let out = serde_json::json!({
|
||||
"items": items,
|
||||
"total": total,
|
||||
"query": query_str,
|
||||
});
|
||||
call_cb(
|
||||
cb,
|
||||
ud,
|
||||
EV_PLAYLIST_SEARCH_OK,
|
||||
&serde_json::to_string(&out).unwrap_or_default(),
|
||||
);
|
||||
}
|
||||
Err(e) => call_cb(cb, ud, EV_GENERIC_ERR, &err_json(&e.to_string())),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- Playlist ----------
|
||||
|
||||
#[no_mangle]
|
||||
@@ -1129,3 +1261,41 @@ pub unsafe extern "C" fn qobuz_backend_delete_track_from_playlist(
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn qobuz_backend_subscribe_playlist(ptr: *mut Backend, playlist_id: i64) {
|
||||
let inner = &(*ptr).0;
|
||||
let client = inner.client.clone();
|
||||
let cb = inner.cb;
|
||||
let ud = inner.ud;
|
||||
spawn(inner, async move {
|
||||
match client.lock().await.subscribe_playlist(playlist_id).await {
|
||||
Ok(()) => call_cb(
|
||||
cb,
|
||||
ud,
|
||||
EV_PLAYLIST_SUBSCRIBED,
|
||||
&serde_json::json!({"playlist_id": playlist_id}).to_string(),
|
||||
),
|
||||
Err(e) => call_cb(cb, ud, EV_GENERIC_ERR, &err_json(&e.to_string())),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn qobuz_backend_unsubscribe_playlist(ptr: *mut Backend, playlist_id: i64) {
|
||||
let inner = &(*ptr).0;
|
||||
let client = inner.client.clone();
|
||||
let cb = inner.cb;
|
||||
let ud = inner.ud;
|
||||
spawn(inner, async move {
|
||||
match client.lock().await.unsubscribe_playlist(playlist_id).await {
|
||||
Ok(()) => call_cb(
|
||||
cb,
|
||||
ud,
|
||||
EV_PLAYLIST_UNSUBSCRIBED,
|
||||
&serde_json::json!({"playlist_id": playlist_id}).to_string(),
|
||||
),
|
||||
Err(e) => call_cb(cb, ud, EV_GENERIC_ERR, &err_json(&e.to_string())),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user