feat: artist/page endpoint, multi-disc fix, playlist ownership, UX improvements

- Switch artist view to artist/page API (proper sections: Albums, Singles & EPs,
  Live, Compilations; version in titles like "Deluxe")
- Fix artist sections categorization using releases[].type from artist/page
- Add getUser() backend call; fetch on session restore when userId=0 to fix
  playlist ownership (Remove from playlist / Delete playlist were missing)
- Fix multi-disc double-click / Play Now queue start index (disc headers were
  counted in row index but excluded from currentTracksJson)
- Hide redundant Album column when viewing an album
- Make artist name in context header clickable (navigates to artist page)
- Fix gap between title and artist name in context header (addStretch)
- Fix queue panel track titles to include version field
- Fix album list to show version in title column

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
joren
2026-03-24 23:09:04 +01:00
parent 69fb818c38
commit 872fdecdce
16 changed files with 272 additions and 58 deletions

View File

@@ -34,6 +34,7 @@ enum QobuzEvent {
EV_PLAYLIST_CREATED = 20,
EV_PLAYLIST_DELETED = 21,
EV_PLAYLIST_TRACK_ADDED = 22,
EV_USER_OK = 23,
};
// Callback signature
@@ -46,6 +47,7 @@ void qobuz_backend_free(QobuzBackendOpaque *backend);
// Auth
void qobuz_backend_login(QobuzBackendOpaque *backend, const char *email, const char *password);
void qobuz_backend_set_token(QobuzBackendOpaque *backend, const char *token);
void qobuz_backend_get_user(QobuzBackendOpaque *backend);
// Catalog
void qobuz_backend_search(QobuzBackendOpaque *backend, const char *query, uint32_t offset, uint32_t limit);

View File

@@ -266,6 +266,15 @@ impl QobuzClient {
Ok(serde_json::from_value(body)?)
}
pub async fn get_artist_page(&self, artist_id: i64) -> Result<Value> {
let resp = self
.get_request("artist/page")
.query(&[("artist_id", artist_id.to_string())])
.send()
.await?;
Self::check_response(resp).await
}
// --- Search ---
pub async fn search(&self, query: &str, offset: u32, limit: u32) -> Result<SearchCatalogDto> {

View File

@@ -46,6 +46,7 @@ pub struct SubscriptionDto {
pub struct TrackDto {
pub id: i64,
pub title: Option<String>,
pub version: Option<String>,
pub duration: Option<i64>,
pub track_number: Option<i32>,
pub playlist_track_id: Option<i64>,
@@ -120,6 +121,10 @@ pub struct ArtistDto {
pub image: Option<ImageDto>,
pub biography: Option<BiographyDto>,
pub albums: Option<SearchResultItems<AlbumDto>>,
#[serde(rename = "epSingles")]
pub ep_singles: Option<SearchResultItems<AlbumDto>>,
#[serde(rename = "liveAlbums")]
pub live_albums: Option<SearchResultItems<AlbumDto>>,
}
#[derive(Debug, Deserialize, Clone, Serialize)]

View File

@@ -239,7 +239,7 @@ pub unsafe extern "C" fn qobuz_backend_get_artist(ptr: *mut Backend, artist_id:
let cb = inner.cb; let ud = inner.ud;
spawn(inner, async move {
let result = client.lock().await.get_artist(artist_id).await;
let result = client.lock().await.get_artist_page(artist_id).await;
let (ev, json) = match result {
Ok(r) => (EV_ARTIST_OK, serde_json::to_string(&r).unwrap_or_default()),
Err(e) => (EV_ARTIST_ERR, err_json(&e.to_string())),
@@ -563,6 +563,25 @@ pub unsafe extern "C" fn qobuz_backend_remove_fav_album(ptr: *mut Backend, album
});
}
// ---------- User ----------
pub const EV_USER_OK: c_int = 23;
#[no_mangle]
pub unsafe extern "C" fn qobuz_backend_get_user(ptr: *mut Backend) {
let inner = &(*ptr).0;
let client = inner.client.clone();
let cb = inner.cb; let ud = inner.ud;
spawn(inner, async move {
let result = client.lock().await.get_user().await;
let (ev, json) = match result {
Ok(r) => (EV_USER_OK, serde_json::to_string(&r).unwrap_or_default()),
Err(e) => (EV_GENERIC_ERR, err_json(&e.to_string())),
};
call_cb(cb, ud, ev, &json);
});
}
// ---------- Playlist management ----------
pub const EV_PLAYLIST_CREATED: c_int = 20;