Merge bugfix/pre-1.2.13: offline-tolerant CoreBridge + Continue Listening hydration

This commit is contained in:
vicrodh committed 2026-05-14 23:35:34 -06:00
commit 2b7b2312ea
3 files changed
+116 -16

No files matched your search

+41 -7
View File
@@ -53,26 +53,60 @@ impl<A: FrontendAdapter + Send + Sync + 'static> QbzCore<A> {
/// Initialize the core
///
/// This should be called once at startup to set up all subsystems.
/// This extracts Qobuz bundle tokens - user must still call login() to authenticate.
/// Best-effort: if bundle token extraction fails (e.g. no network), the
/// core still finishes initialization so queue manager and player remain
/// usable for offline/local playback. API calls will then return
/// `CoreError::NotInitialized` until the client is rebuilt with tokens.
pub async fn init(&self) -> Result<(), CoreError> {
let mut initialized = self.initialized.write().await;
if *initialized {
return Ok(());
}
// Initialize Qobuz client
let client = QobuzClient::new().map_err(|e| CoreError::Internal(e.to_string()))?;
// Extract bundle tokens (required before any API calls)
match client.init().await {
Ok(_) => {
*self.client.write().await = Some(client);
log::info!("QbzCore initialized with bundle tokens");
}
Err(e) => {
log::warn!(
"QbzCore: bundle token extraction failed ({}). Starting in offline-tolerant mode; API calls will be unavailable until next online start.",
e
);
}
}
*initialized = true;
Ok(())
}
/// Whether the Qobuz API client is initialized (bundle tokens extracted).
/// Returns false when the core is running in offline-tolerant mode after
/// a failed bundle extraction at startup.
pub async fn is_api_initialized(&self) -> bool {
self.client.read().await.is_some()
}
/// Best-effort attempt to rebuild the Qobuz API client. Useful when the
/// initial `init()` ran offline and the host has since regained network.
/// No-op when the client is already initialized.
pub async fn try_init_api(&self) -> Result<(), CoreError> {
{
let guard = self.client.read().await;
if guard.is_some() {
return Ok(());
}
}
let client = QobuzClient::new().map_err(|e| CoreError::Internal(e.to_string()))?;
client
.init()
.await
.map_err(|e| CoreError::Internal(format!("Failed to extract bundle tokens: {}", e)))?;
*self.client.write().await = Some(client);
*initialized = true;
log::info!("QbzCore initialized with bundle tokens");
log::info!("QbzCore: API client initialized lazily");
Ok(())
}
+57 -9
View File
@@ -43,6 +43,7 @@
import { toggleArtistFavorite } from '$lib/stores/artistFavoritesStore';
import { invoke } from '@tauri-apps/api/core';
import { replacePlaybackQueue } from '$lib/services/queuePlaybackService';
import { setPlaybackContext } from '$lib/stores/playbackContextStore';
import { playQueueIndex } from '$lib/stores/queueStore';
import type { BackendQueueTrack } from '$lib/stores/queueStore';
@@ -291,6 +292,61 @@
let spotlightTopTracksLoading = $state(false);
let spotlightRadioLoading = $state(false);
async function handleContinueListeningTrackClick(track: DiscoveryTrackCard) {
const trackIndex = continueListening.findIndex((entry) => entry.trackId === track.trackId);
if (trackIndex < 0) return;
const trackIds = continueListening.map((entry) => entry.trackId);
await setPlaybackContext(
'home_list',
'home:continue_listening',
'Continue Listening',
'qobuz',
trackIds,
trackIndex,
);
try {
const queueTracks: BackendQueueTrack[] = continueListening.map((entry) => ({
id: entry.trackId,
title: entry.title,
version: null,
artist: entry.artist,
album: entry.album,
duration_secs: entry.durationSeconds,
artwork_url: entry.artwork ?? null,
hires: entry.hires,
bit_depth: entry.bitDepth ?? null,
sample_rate: entry.samplingRate ?? null,
is_local: false,
album_id: entry.albumId ?? null,
artist_id: entry.artistId ?? null,
}));
await replacePlaybackQueue(queueTracks, trackIndex, {
debugLabel: 'discovery-v2:continue-listening',
});
} catch (err) {
console.error('[discovery-v2] continue listening queue failed', err);
}
onTrackPlay?.({
id: track.trackId,
title: track.title,
artist: track.artist,
album: track.album,
albumId: track.albumId,
artistId: track.artistId,
albumArt: track.artwork,
duration: track.duration,
durationSeconds: track.durationSeconds,
hires: track.hires,
bitDepth: track.bitDepth,
samplingRate: track.samplingRate,
isrc: track.isrc,
});
}
async function handleSpotlightTopTracks() {
if (!spotlight || spotlightTopTracksLoading || spotlight.topTracks.length === 0) return;
spotlightTopTracksLoading = true;
@@ -455,15 +511,7 @@
artist={track.artist}
artwork={track.artwork}
isPlaying={track.trackId === activeTrackId && isPlaybackActive === true}
onClick={() => onTrackPlay?.({
id: track.trackId,
title: track.title,
artist: track.artist,
album: track.album,
albumId: track.albumId,
artistId: track.artistId,
albumArt: track.artwork,
} as DisplayTrack)}
onClick={() => handleContinueListeningTrackClick(track)}
onArtistClick={track.artistId !== undefined
? () => onTrackGoToArtist?.(track.artistId!)
: undefined}
+18
View File
@@ -234,6 +234,12 @@ export interface DiscoveryTrackCard {
albumId?: string;
artistId?: number;
artwork?: string;
duration: string;
durationSeconds: number;
hires: boolean;
bitDepth?: number;
samplingRate?: number;
isrc?: string;
}
export interface DiscoveryArtistTile {
@@ -267,6 +273,12 @@ interface RecoTrackDisplayMeta {
albumArt: string;
albumId?: string;
artistId?: number;
duration: string;
durationSeconds: number;
hires: boolean;
bitDepth?: number;
samplingRate?: number;
isrc?: string;
}
interface RecoArtistCardMeta {
@@ -673,6 +685,12 @@ export async function fetchHomeResolved(
albumId: track.albumId,
artistId: track.artistId,
artwork: track.albumArt || undefined,
duration: track.duration,
durationSeconds: track.durationSeconds,
hires: track.hires,
bitDepth: track.bitDepth,
samplingRate: track.samplingRate,
isrc: track.isrc,
})),
topArtists: resp.topArtists.slice(0, perSection).map((a) => ({
artistId: a.id,