feat: add seamless lazy loading for genre and playlist views
Some checks failed
Build for Windows / build-windows (push) Has been cancelled

Introduce paged loading with early prefetch for genre albums/playlists and playlist tracks, while preserving full-data behavior for deep shuffle and playlist play-all actions.
This commit is contained in:
joren
2026-03-31 10:43:36 +02:00
parent 4ebd5ed3f0
commit e453f8acf3
12 changed files with 543 additions and 17 deletions

View File

@@ -67,6 +67,7 @@ void qobuz_backend_get_dynamic_suggestions(QobuzBackendOpaque *backend, const ch
void qobuz_backend_get_album(QobuzBackendOpaque *backend, const char *album_id);
void qobuz_backend_get_artist(QobuzBackendOpaque *backend, int64_t artist_id);
void qobuz_backend_get_playlist(QobuzBackendOpaque *backend, int64_t playlist_id, uint32_t offset, uint32_t limit);
void qobuz_backend_get_playlist_all(QobuzBackendOpaque *backend, int64_t playlist_id);
// Favorites
void qobuz_backend_get_fav_tracks(QobuzBackendOpaque *backend, uint32_t offset, uint32_t limit);

View File

@@ -639,6 +639,51 @@ impl QobuzClient {
Ok(serde_json::from_value(body)?)
}
pub async fn get_playlist_all(&self, playlist_id: i64) -> Result<PlaylistDto> {
const PAGE_LIMIT: u32 = 500;
let mut playlist = self.get_playlist(playlist_id, 0, PAGE_LIMIT).await?;
let mut all_items = playlist
.tracks
.as_ref()
.and_then(|t| t.items.clone())
.unwrap_or_default();
let mut total = playlist
.tracks
.as_ref()
.and_then(|t| t.total)
.unwrap_or(all_items.len() as i32);
if total < all_items.len() as i32 {
total = all_items.len() as i32;
}
let mut offset = all_items.len() as u32;
while (offset as i32) < total {
let page = self.get_playlist(playlist_id, offset, PAGE_LIMIT).await?;
let mut page_items = page
.tracks
.as_ref()
.and_then(|t| t.items.clone())
.unwrap_or_default();
if page_items.is_empty() {
break;
}
all_items.append(&mut page_items);
offset = all_items.len() as u32;
}
if let Some(tracks) = playlist.tracks.as_mut() {
tracks.items = Some(all_items);
tracks.total = Some(total);
tracks.offset = Some(0);
tracks.limit = Some(PAGE_LIMIT as i32);
}
Ok(playlist)
}
/// Fetch all favorite IDs (tracks, albums, artists) in one call.
async fn get_fav_ids(&self) -> Result<FavIdsDto> {
let resp = self

View File

@@ -514,6 +514,8 @@ pub unsafe extern "C" fn qobuz_backend_get_featured_albums(
"total": total,
"type": kind_str,
"genre_ids": genre_ids_str,
"offset": offset,
"limit": limit,
});
call_cb(
cb,
@@ -557,6 +559,8 @@ pub unsafe extern "C" fn qobuz_backend_get_featured_playlists(
"total": total,
"type": kind_str,
"genre_ids": genre_ids_str,
"offset": offset,
"limit": limit,
});
call_cb(
cb,
@@ -600,6 +604,8 @@ pub unsafe extern "C" fn qobuz_backend_discover_playlists(
"total": total,
"genre_ids": genre_ids_str,
"tags": tags_str,
"offset": offset,
"limit": limit,
});
call_cb(
cb,
@@ -640,6 +646,8 @@ pub unsafe extern "C" fn qobuz_backend_search_playlists(
"items": items,
"total": total,
"query": query_str,
"offset": offset,
"limit": limit,
});
call_cb(
cb,
@@ -684,6 +692,28 @@ pub unsafe extern "C" fn qobuz_backend_get_playlist(
});
}
#[no_mangle]
pub unsafe extern "C" fn qobuz_backend_get_playlist_all(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 {
let result = client.lock().await.get_playlist_all(playlist_id).await;
let (ev, json) = match result {
Ok(r) => {
let mut v = serde_json::to_value(&r).unwrap_or_default();
if let serde_json::Value::Object(ref mut obj) = v {
obj.insert("full_load".to_string(), serde_json::Value::Bool(true));
}
(EV_PLAYLIST_OK, serde_json::to_string(&v).unwrap_or_default())
}
Err(e) => (EV_PLAYLIST_ERR, err_json(&e.to_string())),
};
call_cb(cb, ud, ev, &json);
});
}
// ---------- Favorites ----------
#[no_mangle]