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
This commit is contained in:
1 parent
bb996a4002
commit
6f8dc41a98
5 files changed
+121
-2
No files matched your search
@@ -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<SearchResultsPage<Album>> {
|
||||
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<Track> {
|
||||
let url = endpoints::build_url(paths::TRACK_GET);
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -53,6 +53,21 @@ pub async fn get_album(album_id: String, state: State<'_, AppState>) -> Result<A
|
||||
client.get_album(&album_id).await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Get featured albums by type (new-releases, press-awards)
|
||||
#[tauri::command]
|
||||
pub async fn get_featured_albums(
|
||||
featured_type: String,
|
||||
limit: Option<u32>,
|
||||
offset: Option<u32>,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<SearchResultsPage<Album>, 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<Track, String> {
|
||||
let client = state.client.lock().await;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<string | null>(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<AlbumCardData[]>([]);
|
||||
let pressAwards = $state<AlbumCardData[]>([]);
|
||||
|
||||
// User-specific content
|
||||
let recentAlbums = $state<AlbumCardData[]>([]);
|
||||
let continueTracks = $state<DisplayTrack[]>([]);
|
||||
let topArtists = $state<ArtistCardData[]>([]);
|
||||
@@ -68,7 +76,9 @@
|
||||
let failedArtistImages = $state<Set<number>>(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<AlbumCardData[]> {
|
||||
try {
|
||||
const response = await invoke<FeaturedAlbumsResponse>('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<HomeSeeds>('reco_get_home', {
|
||||
@@ -273,6 +317,40 @@
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
{:else if hasContent}
|
||||
{#if newReleases.length > 0}
|
||||
<HorizontalScrollRow title="New Releases">
|
||||
{#snippet children()}
|
||||
{#each newReleases as album}
|
||||
<AlbumCard
|
||||
artwork={album.artwork}
|
||||
title={album.title}
|
||||
artist={album.artist}
|
||||
quality={album.quality}
|
||||
onclick={() => onAlbumClick?.(album.id)}
|
||||
/>
|
||||
{/each}
|
||||
<div class="spacer"></div>
|
||||
{/snippet}
|
||||
</HorizontalScrollRow>
|
||||
{/if}
|
||||
|
||||
{#if pressAwards.length > 0}
|
||||
<HorizontalScrollRow title="Press Awards">
|
||||
{#snippet children()}
|
||||
{#each pressAwards as album}
|
||||
<AlbumCard
|
||||
artwork={album.artwork}
|
||||
title={album.title}
|
||||
artist={album.artist}
|
||||
quality={album.quality}
|
||||
onclick={() => onAlbumClick?.(album.id)}
|
||||
/>
|
||||
{/each}
|
||||
<div class="spacer"></div>
|
||||
{/snippet}
|
||||
</HorizontalScrollRow>
|
||||
{/if}
|
||||
|
||||
{#if recentAlbums.length > 0}
|
||||
<HorizontalScrollRow title="Recently Played">
|
||||
{#snippet children()}
|
||||
|
||||
Reference in new issue
Block a user