Addresses the reported behavior where tracks marked available offline
still buffered from Qobuz after the v1.2.2 upgrade. Three layers:
1. **Snapshot / restore stream-first-track.** offline_settings gains a
new nullable column pre_offline_stream_first_track (SQLite ALTER
migration). v2_set_manual_offline now coordinates with
AudioSettingsState: entering offline mode stashes the user's current
stream_first_track preference and forces it to false; exiting
restores the snapshot. User's preference survives the round-trip
even across app restarts.
2. **Explicit offline refusal.** v2_play_track now takes
OfflineState, reads manual_offline_mode up front, and — when every
local cache miss has been exhausted — returns a new
RuntimeError::TrackNotAvailableOffline instead of silently reaching
for the network. Low-quality offline hits still play (better than
nothing). The playback path also masks stream_first_enabled while
offline as a belt-and-suspenders defense.
3. **Cache-miss visibility.** Every call site that consults the
offline cache DB now logs [V2/CACHE STALE] at warn level when the
DB says a track is cached but the filesystem path is missing. This
made the issue #279 behavior invisible; the orphan entries from
per-user isolation migration (aab3e6764) are now diagnosable from
logs.
Frontend:
- playbackService surfaces TrackNotAvailableOffline as a toast ('…
isn't saved for offline playback') instead of the generic error
- SettingsView listens for a new audio-settings-changed event so the
stream-first-track toggle in the UI flips when the offline toggle
mutates it
- i18n key offline.trackNotAvailableOffline added to all 5 locales
cc
Additive changes only: adds isAutoUpdateEligible() getter and
eligibility fetch in initUpdatesStore (keeps existing skipIfRemote
guards); adds performAutoUpdate() in updatesService for the
download/install/relaunch flow with cancellation support.
cc
When target is qbzd, playTrack() sends play-track to daemon and
returns immediately. No local stop (was killing daemon audio),
no cmdStop (was sending HTTP stop to daemon), no metadata update,
no scrobbling, no QConnect handoff.
The daemon handles stop-before-play internally in its play_track
endpoint. The desktop sends ONE command and exits. Zero leakage.
cc
When playbackTarget is 'qbzd', the play-track command goes directly
to the daemon HTTP API. Previously, it also went through
handoffPlayTrackToRemoteRenderer (QConnect) which sent play to ALL
connected renderers, causing audio on both local desktop AND remote
daemon simultaneously.
Now: qbzd mode = HTTP API only. QConnect handoff only runs in
local mode. Local player stopped explicitly via invoke before
sending to daemon.
cc
LAN-only mode means no token auth. The empty string token ('') was
failing the truthy check in requireRemote(), causing 'Not in remote
mode' error on every command after connecting.
Now: token header only sent when token is non-empty. requireRemote
only checks type === 'qbzd' and baseUrl presence.
This was THE bug preventing QBZ Desktop from controlling the daemon.
cc
playbackService.ts: v2_play_track and v2_stop_playback now route
to daemon HTTP API when in remote mode.
9 views: bulk queue operations (add_tracks_to_queue, add_tracks_to_queue_next)
now use cmdAddTracksToQueue/cmdAddTracksToQueueNext instead of
direct invoke(). 14 call sites total.
This was the root cause of 'connects but nothing plays' — the
play/queue commands were always going to local Tauri, never to
the remote daemon.
cc
Daemon: 5 new audio settings endpoints:
- GET /api/audio/settings (full settings)
- PATCH /api/audio/settings (partial update)
- GET /api/audio/backends (available backends)
- GET /api/audio/devices?backend= (devices for backend)
- GET /api/audio/hardware-status (current hw state)
Frontend: commandRouter gains audio settings commands that route
to daemon API when in remote mode, local invoke when local.
Total daemon endpoints: 32.
cc
New commandRouter.ts provides a single bifurcation point for all
playback and queue operations. When playbackTarget is 'qbzd', commands
route to the daemon HTTP API. When 'local', they use Tauri invoke().
Stores updated:
- playerStore.ts: pause, resume, stop, seek, volume, play_track (6 sites)
- queueStore.ts: shuffle, repeat, add, add-next, set, clear (8 sites)
No behavioral change in local mode — invoke() calls are identical.
cc
playTrack() was always sending the user's max quality setting (Hi-Res+)
regardless of what the track actually offers. A 44.1kHz/24bit track
would trigger 3 API fallback calls (UltraHiRes -> HiRes -> Lossless)
and the cache was discarded as "lower quality" for re-download.
New effectiveQuality() picks the minimum of user max and track metadata
(bit_depth + sampling_rate), so a 24bit/96kHz track requests Hi-Res
directly (1 API call) and cache matches perfectly.
cc
Track 504/server errors and show orange warning triangle in
NowPlayingBar when Qobuz is having issues. Auto-clears after
10 minutes without errors. Shows status in Settings too.
cc
When v2_play_track returns QualityExhausted, the service checks the
persisted fallback behavior: auto-retry at lowest quality, auto-skip,
or emit a custom event so the UI can show the QualityFallbackModal.
Also passes forceLowestQuality option through to the backend invoke.
cc
When image cache is disabled or unavailable, images were returned
as original HTTPS URLs for WebKit to load directly. On AppImage
distros where GnuTLS can't find system CA certs, this fails silently.
Now all images go through reqwest (rustls, bundled CA) and are
written to temp files as file:// URLs. WebKit never needs to
resolve HTTPS for images.
cc
Tauri invoke errors are objects with { code, details, message } fields.
Template literal interpolation with ${err} produces [object Object].
Extract .message or .details, falling back to JSON.stringify().
The quality fallback itself (UltraHiRes -> HiRes) is already handled
by get_stream_url_with_fallback in qbz-core.
cc
- setAutoplayMode now sends autoplay preference to QConnect server
- queue_load_tracks syncs local autoplay mode after loading
- QConnect panel shows autoplay_mode status
When QBZ is the local renderer in a QConnect session and the web player
presses next, the server sends SET_STATE with the new track. The backend
correctly starts playback, but the frontend's gapless transition handler
saw the track_id change, treated it as a local gapless event, and called
nextTrack() — which advanced the CoreBridge queue every 1s polling cycle,
flooding the queue and destroying the shuffle order.
Root cause: shouldQconnectSuppressLocalPlaybackAutomation only suppressed
when a peer renderer was active (activeRendererId !== localRendererId).
When QBZ IS the renderer, remoteControlMode stayed false, so all local
automation (gapless, auto-advance, resume-from-stop) remained active and
fought the server.
Fix: suppress local playback automation whenever QConnect is connected,
regardless of which renderer is active. The server is the authority for
track transitions in all QConnect scenarios.
cc
Add real streaming path to v2_play_track: when stream_first_track
is enabled, uses play_streaming_dynamic + background chunked
download instead of full download before playback.
Fix seekbar not showing position for streamed tracks by passing
duration_secs from frontend to the backend. Without it, the
player stored 0 as duration, making the seekbar appear empty.
Prioritize image.small (230px) over image.large (600px) for all
AlbumCard artwork and queue track thumbnails. Hero/detail views
retain 600px via getQobuzImageForSize(image, 'large').
Display parental_warning badge (E icon) next to track titles in all
views: TrackRow, QueuePanel, NowPlayingBar, ImmersivePlayer with all
13 visualizer panels, MiniPlayer surfaces, and AlbumDetailView.
Propagate parental_warning field through the full data chain from
Rust backend (qbz-models, queue, commands_v2, session_store, API
server) to frontend stores, adapters, and UI components. Badge uses
CSS mask technique with explicit.svg for theme compatibility.
Detect when QBZ runs inside a Snap sandbox (via SNAP env var) and show
a one-time welcome modal with required plug connection commands. Adds a
Snap Sandbox section in Settings with copyable commands, mirroring the
existing Flatpak pattern.
Backend: snap.rs module, DB table snap_welcome_shown, V2 commands.
Frontend: updatesService, +page.svelte modal queue, SettingsView section.
Add Shift+M toggle shortcut for mini player mode (main and mini window).
Add 1-5 number keys for surface switching (micro/compact/artwork/queue/lyrics)
in mini player context. Add playback shortcuts (Space, Ctrl+Arrow) to mini
player window. Fix X button to close app (or hide to tray) instead of
duplicating the Expand button behavior. Closes#134.
Download checkmarks now only show for the currently selected format.
In-memory store tracks formatId per album download, and the album
detail view checks track.downloaded_format_ids from backend against
the selected format dropdown. Switching quality hides checkmarks
from a different format's downloads.
Quality folder was being prepended to the destination path by the
frontend, resulting in Quality/Artist/Album/track. Now quality_dir
is passed as a separate parameter through the full stack and inserted
between album and filename by the backend path builder.
Three fixes for QConnect seekbar and queue sync:
1. Position reports now include currentTrackId from frontend. Backend
looks up queue_item_id by matching track_id against QConnect queue
state when renderer sync state has no cached value. This resolves
the root cause of seekbar showing position 0 on controllers.
2. loadQconnectQueue changed from fire-and-forget (void) to proper
error handling with logging at every step. Added logging before
isQconnectConnected check to diagnose silent failures.
3. When TransportConnected event fires while QBZ is already playing,
automatically push the local queue to QConnect server so controllers
immediately see the correct tracks.
When playing an album or playlist from QBZ, the local queue was
replaced but the QConnect remote queue was never updated, causing
controllers to show a stale queue.
Add loadQconnectQueue() function that sends queue_load_tracks to the
remote with all track IDs. Wire it into playAlbumById() and
playPlaylistById() so the remote queue updates when a new
album/playlist starts playing locally.