diff --git a/crates/qbz-models/src/source.rs b/crates/qbz-models/src/source.rs index bfbadef2d..ab079eaf2 100644 --- a/crates/qbz-models/src/source.rs +++ b/crates/qbz-models/src/source.rs @@ -9,8 +9,8 @@ //! This is the frontend-agnostic contract behind the source-aware playback //! context: the now-playing bar, the queue, and the artwork pipeline consume //! `PlaybackSource` + [`ArtworkRef`] and never special-case a source themselves. -//! The same contract drives the Qobuz Connect queue gate (only Qobuz-streamable -//! tracks may be cast — see [`PlaybackSource::is_qobuz_streamable`]). +//! The same contract drives the Qobuz Connect queue gate (only castable tracks +//! may be cast — see [`PlaybackSource::is_castable_to_qconnect`]). use serde::{Deserialize, Serialize}; @@ -54,10 +54,9 @@ impl PlaybackSource { } } - /// Whether this source streams from Qobuz and may therefore be cast onto - /// a Qobuz Connect queue. Offline-cache copies, local files, and Plex are - /// **not** castable to a Qobuz Connect receiver — this is the first-class - /// gate the queue pipeline consults before syncing to Qobuz Connect. + /// Whether this source streams live from the Qobuz catalog. NOTE: NOT the + /// cast gate — offline-cache also carries a valid Qobuz id and IS castable. + /// Use is_castable_to_qconnect for the Qobuz Connect gate. pub fn is_qobuz_streamable(self) -> bool { matches!(self, Self::Qobuz) } @@ -193,6 +192,12 @@ mod tests { } } + #[test] + fn offline_cache_is_castable() { + assert!(PlaybackSource::OfflineCache.is_castable_to_qconnect()); + assert!(TrackOriginTag::OfflineCache.is_castable_to_qconnect()); + } + #[test] fn strict_parse_blocks_unknown_and_absent() { use TrackOriginTag::*; diff --git a/crates/qconnect-core/src/admission.rs b/crates/qconnect-core/src/admission.rs index 6f1a9d627..5d6cf5b1c 100644 --- a/crates/qconnect-core/src/admission.rs +++ b/crates/qconnect-core/src/admission.rs @@ -82,4 +82,9 @@ mod tests { ); } } + + #[test] + fn offline_cache_is_admitted() { + assert!(evaluate_remote_queue_admission(TrackOrigin::QobuzOfflineCache).accepted); + } } diff --git a/src/lib/services/queuePlaybackService.test.ts b/src/lib/services/queuePlaybackService.test.ts index 41e0e9f92..02e6acee4 100644 --- a/src/lib/services/queuePlaybackService.test.ts +++ b/src/lib/services/queuePlaybackService.test.ts @@ -66,3 +66,24 @@ describe('assessQconnectQueueSync', () => { }); }); }); + +const track = (over: Partial): BackendQueueTrack => + ({ id: 1, title: 't', artist: 'a', album: 'al', duration_secs: 0, artwork_url: null, + hires: false, bit_depth: null, sample_rate: null, is_local: false, album_id: null, + artist_id: null, streamable: true, source: 'qobuz', parental_warning: false, ...over }) as BackendQueueTrack; + +describe('assessQconnectQueueSync — offline-cache', () => { + it('treats qobuz_download as syncable (offline copy carries a Qobuz id)', () => { + const r = assessQconnectQueueSync([track({ id: 5, source: 'qobuz_download' })]); + expect(r.syncable).toBe(true); + expect(r.trackIds).toEqual([5]); + expect(r.blockedTrackIds).toEqual([]); + }); + it('still blocks local and plex', () => { + const r = assessQconnectQueueSync([ + track({ id: 1, source: 'qobuz' }), track({ id: 2, source: 'local' }), + ]); + expect(r.syncable).toBe(false); + expect(r.blockedTrackIds).toContain(2); + }); +}); diff --git a/src/lib/services/queuePlaybackService.ts b/src/lib/services/queuePlaybackService.ts index 08265880a..88456612d 100644 --- a/src/lib/services/queuePlaybackService.ts +++ b/src/lib/services/queuePlaybackService.ts @@ -23,7 +23,8 @@ function isQconnectSyncEligibleTrack(track: BackendQueueTrack): boolean { if (track.is_local) return false; const source = (track.source ?? '').toLowerCase(); - if (source === 'local' || source === 'plex' || source === 'qobuz_download') { + // Offline-cache (qobuz_download) IS eligible: its id is the real Qobuz id. + if (source === 'local' || source === 'plex') { return false; } diff --git a/src/test/mocks/app/environment.ts b/src/test/mocks/app/environment.ts new file mode 100644 index 000000000..c72dc3e7e --- /dev/null +++ b/src/test/mocks/app/environment.ts @@ -0,0 +1,8 @@ +// Test mock for SvelteKit's `$app/environment`. +// The vitest config aliases `$app` to this directory, but the module was never +// created, so any test whose import graph reaches `$app/environment` (e.g. via +// the i18n module) failed to load. This provides the minimal surface the app uses. +export const browser = false; +export const dev = false; +export const building = false; +export const version = 'test';