From a16d6d0c8196dd26d581a67d479b1b0118401af1 Mon Sep 17 00:00:00 2001 From: vicrodh <103399283+vicrodh@users.noreply.github.com> Date: Mon, 1 Jun 2026 01:52:22 -0600 Subject: [PATCH] qconnect: allow offline-cache to be cast (one consistent rule) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three sites disagreed: model blocked OfflineCache, live gate allowed it, TS sync filter blocked qobuz_download. Decision: ALLOW — an offline copy's queue id is the real Qobuz track id. Flip the TS sync filter and fix the stale model doc comment to match the live gate. Also add Rust lock-in tests (offline_cache_is_castable / offline_cache_is_admitted) and restore the missing $app/environment vitest mock the config already aliases, so queuePlaybackService.test.ts can load. cc --- crates/qbz-models/src/source.rs | 17 +++++++++------ crates/qconnect-core/src/admission.rs | 5 +++++ src/lib/services/queuePlaybackService.test.ts | 21 +++++++++++++++++++ src/lib/services/queuePlaybackService.ts | 3 ++- src/test/mocks/app/environment.ts | 8 +++++++ 5 files changed, 47 insertions(+), 7 deletions(-) create mode 100644 src/test/mocks/app/environment.ts 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';