Merge bugfix/pre-1.2.13: offline-tolerant CoreBridge + Continue Listening hydration
This commit is contained in:
3 files changed
+116
-16
No files matched your search
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user