From 6f8dc41a987358d9a9c2dd9e45aacfd458ccee84 Mon Sep 17 00:00:00 2001 From: vicrodh <103399283+vicrodh@users.noreply.github.com> Date: Sun, 11 Jan 2026 00:25:57 -0600 Subject: [PATCH] feat: add Qobuz featured albums (New Releases, Press Awards) - Add album/getFeatured endpoint to API - Create get_featured_albums Tauri command - Add New Releases and Press Awards sections to HomeView - Featured albums load first for faster perceived performance --- src-tauri/src/api/client.rs | 24 +++++++ src-tauri/src/api/endpoints.rs | 1 + src-tauri/src/commands/search.rs | 15 +++++ src-tauri/src/lib.rs | 1 + src/lib/components/views/HomeView.svelte | 82 +++++++++++++++++++++++- 5 files changed, 121 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/api/client.rs b/src-tauri/src/api/client.rs index 723084187..f33183363 100644 --- a/src-tauri/src/api/client.rs +++ b/src-tauri/src/api/client.rs @@ -246,6 +246,30 @@ impl QobuzClient { Ok(serde_json::from_value(response)?) } + /// Get featured albums by type (new-releases, press-awards, most-streamed) + pub async fn get_featured_albums(&self, featured_type: &str, limit: u32, offset: u32) -> Result> { + let url = endpoints::build_url(paths::ALBUM_GET_FEATURED); + let response: Value = self + .http + .get(&url) + .header("X-App-Id", self.app_id().await?) + .query(&[ + ("type", featured_type), + ("limit", &limit.to_string()), + ("offset", &offset.to_string()), + ]) + .send() + .await? + .json() + .await?; + + let albums = response + .get("albums") + .ok_or_else(|| ApiError::ApiResponse("No albums in response".to_string()))?; + + Ok(serde_json::from_value(albums.clone())?) + } + /// Get track by ID pub async fn get_track(&self, track_id: u64) -> Result { let url = endpoints::build_url(paths::TRACK_GET); diff --git a/src-tauri/src/api/endpoints.rs b/src-tauri/src/api/endpoints.rs index 9bbcf5dae..887e13c64 100644 --- a/src-tauri/src/api/endpoints.rs +++ b/src-tauri/src/api/endpoints.rs @@ -15,6 +15,7 @@ pub mod paths { // Album pub const ALBUM_GET: &str = "/album/get"; pub const ALBUM_SEARCH: &str = "/album/search"; + pub const ALBUM_GET_FEATURED: &str = "/album/getFeatured"; // Artist pub const ARTIST_GET: &str = "/artist/get"; diff --git a/src-tauri/src/commands/search.rs b/src-tauri/src/commands/search.rs index f6a61743a..749767d88 100644 --- a/src-tauri/src/commands/search.rs +++ b/src-tauri/src/commands/search.rs @@ -53,6 +53,21 @@ pub async fn get_album(album_id: String, state: State<'_, AppState>) -> Result, + offset: Option, + state: State<'_, AppState>, +) -> Result, String> { + let client = state.client.lock().await; + client + .get_featured_albums(&featured_type, limit.unwrap_or(12), offset.unwrap_or(0)) + .await + .map_err(|e| e.to_string()) +} + #[tauri::command] pub async fn get_track(track_id: u64, state: State<'_, AppState>) -> Result { let client = state.client.lock().await; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 825eda93b..e735879c8 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -191,6 +191,7 @@ pub fn run() { commands::search_tracks, commands::search_artists, commands::get_album, + commands::get_featured_albums, commands::get_track, commands::get_artist, // Playback commands diff --git a/src/lib/components/views/HomeView.svelte b/src/lib/components/views/HomeView.svelte index e013eeb13..e79fc7c99 100644 --- a/src/lib/components/views/HomeView.svelte +++ b/src/lib/components/views/HomeView.svelte @@ -49,17 +49,25 @@ continueTracks: 8, topArtists: 10, favoriteAlbums: 12, - favoriteTracks: 10 + favoriteTracks: 10, + featuredAlbums: 12 }; // Loading states for progressive render let isInitializing = $state(true); let error = $state(null); + let loadingNewReleases = $state(true); + let loadingPressAwards = $state(true); let loadingRecentAlbums = $state(true); let loadingContinueTracks = $state(true); let loadingTopArtists = $state(true); let loadingFavoriteAlbums = $state(true); + // Featured albums (from Qobuz editorial) + let newReleases = $state([]); + let pressAwards = $state([]); + + // User-specific content let recentAlbums = $state([]); let continueTracks = $state([]); let topArtists = $state([]); @@ -68,7 +76,9 @@ let failedArtistImages = $state>(new Set()); const hasContent = $derived( - recentAlbums.length > 0 + newReleases.length > 0 + || pressAwards.length > 0 + || recentAlbums.length > 0 || continueTracks.length > 0 || topArtists.length > 0 || favoriteAlbums.length > 0 @@ -127,6 +137,24 @@ return artists; } + interface FeaturedAlbumsResponse { + items: QobuzAlbum[]; + total: number; + } + + async function fetchFeaturedAlbums(featuredType: string, limit: number): Promise { + try { + const response = await invoke('get_featured_albums', { + featuredType, + limit + }); + return response.items.map(toAlbumCard); + } catch (err) { + console.error(`Failed to fetch ${featuredType}:`, err); + return []; + } + } + function toAlbumCard(album: QobuzAlbum): AlbumCardData { return { id: album.id, @@ -184,11 +212,27 @@ async function loadHome() { isInitializing = true; error = null; + loadingNewReleases = true; + loadingPressAwards = true; loadingRecentAlbums = true; loadingContinueTracks = true; loadingTopArtists = true; loadingFavoriteAlbums = true; + // Start loading featured albums immediately (no seeds needed) + // These show first since they're fast API calls + fetchFeaturedAlbums('new-releases', LIMITS.featuredAlbums).then(albums => { + newReleases = albums; + loadingNewReleases = false; + // Hide initializing as soon as we have some content + if (albums.length > 0) isInitializing = false; + }); + + fetchFeaturedAlbums('press-awards', LIMITS.featuredAlbums).then(albums => { + pressAwards = albums; + loadingPressAwards = false; + }); + try { // Fast query for seeds (uses simple DB queries, falls back if no ML scores) const seeds = await invoke('reco_get_home', { @@ -273,6 +317,40 @@

{error}

{:else if hasContent} + {#if newReleases.length > 0} + + {#snippet children()} + {#each newReleases as album} + onAlbumClick?.(album.id)} + /> + {/each} +
+ {/snippet} +
+ {/if} + + {#if pressAwards.length > 0} + + {#snippet children()} + {#each pressAwards as album} + onAlbumClick?.(album.id)} + /> + {/each} +
+ {/snippet} +
+ {/if} + {#if recentAlbums.length > 0} {#snippet children()}