Recovered from 63ecc324 (authored on the native-port branch, never merged to
pre-release). Backend-only, no Slint. Scales the prefetch cap down on slow
links and during audio underruns, and exposes the per-segment download signal
the offline detector reuses as a positive liveness override (#467).
cc
Extracts two self-contained, framework-agnostic modules out of
src-tauri so they can be reused by other clients without duplicating
logic:
- ImageCacheService / ImageCacheStats move into the qbz-cache crate
(LRU disk cache, MD5-keyed, SQLite index).
- The credential store (keyring + AES-256-GCM encrypted file for Qobuz
credentials and the OAuth token) moves into a new qbz-credentials
crate.
src-tauri/image_cache.rs and src-tauri/credentials/mod.rs become thin
re-exports, keeping their Tauri managed-state wrappers. The Tauri app
is behavior-identical; both modules were already self-contained (no
Tauri and no crate:: references).
cc
Task 2 of the Slint POC readiness audit. AppRuntime now performs
framework-agnostic per-user session activation: activate, activate_at,
activate_offline, deactivate, plus is_session_active, active_user_id,
and a with_session_store accessor.
This is the minimal version per the 2026-05-18 audit correction. It
opens only the session store and does the portable scaffolding (user
paths, directories, last_user_id marker, runtime state machine). It
does not touch Tauri's session_lifecycle, does not initialize the
src-tauri-side per-user stores, and does not run the flat-to-user
migration. The shipping app keeps identical behavior; this is additive.
activate_at takes explicit directories and is the testable core;
activate resolves real per-user paths through UserDataPaths.
cc
Adds a framework-agnostic composition root for non-Tauri UI shells
(Slint, TUI, headless) as Task 1 of the Slint POC readiness audit
(session-20-slint-poc-readiness-audit).
AppRuntime<A> owns an Arc<QbzCore<A>> plus the runtime state machine,
built with the same recipe as the Tauri CoreBridge but with no Tauri
dependency. Scope is composition and accessors only; per-user store
ownership and session activation are deferred to Task 2.
qbz-app now depends on qbz-core, qbz-player, qbz-audio, and qbz-models.
The Tauri app does not consume this module; it is purely additive.
cc
Clicking a track inside an expanded Mixtape/Collection item now plays
it. Qobuz albums and playlists build the queue directly; Plex and
local-library albums delegate to the backend collection-item resolver,
then jump the queue cursor to the clicked track.
cc
Expanded playlist items showed "No results found"; fetch their tracks
via v2_get_playlist. Also carry the remix/edition version field so
titles render consistently.
cc
Add playback/audio/graphics settings to the diagnostics snapshot via
v2_get_playback_preferences. No sensitive data (no username or paths);
gated by the existing diagnostics opt-in toggle.
cc
Switch the session store SQLite connection from synchronous=NORMAL to
synchronous=FULL. With WAL + NORMAL the last writes can sit unsynced in
the WAL and be lost on a hard reboot / power loss, dropping the restored
session.
cc
Qobuz Connect renderer mode and casting are mutually exclusive output modes. While qbz is registered as a QConnect renderer, the Qobuz cloud drives playback through the renderer-command path, which bypasses the cast path entirely — the cast device receives no LOAD and stays silent.
connectToDevice() now checks QConnect status and, if it is on, suspends it (v2_qconnect_disconnect) before connecting the cast device. When the cast ends, disconnect() restores QConnect only if it was on beforehand; a failed cast connection restores it too. Applies to Chromecast and DLNA alike.
Fixes#439.
cc
TAG_DETAILS.md is the curated, user-facing release summary rendered by the website and the in-app About modal. CHANGELOG_FULL.md is the complete commit list for v1.2.12..v1.2.13, restoring the two-file release-docs layout last used at 1.2.5.
cc
Version bumped across Cargo.toml, tauri.conf.json, package.json, snapcraft.yaml (version + source-tag), flake.nix, and the lockfiles. metainfo.xml gains a 1.2.13 release entry, the About modal codename becomes "400+ stars", and the README contributors list adds @Vudgekek and @DoubleGate.
cc
Google Cast closes a control connection that goes roughly 10 seconds without a heartbeat PING. QBZ pinged every 25 seconds, so any cast connection left idle between connect and load — for example while the track downloads, or across a delayed second play attempt — was dropped by the receiver. The next LOAD then failed with EPIPE (broken pipe): the device chimed on connect but never played audio, and the heartbeat thread logged repeating broken-pipe warnings.
Pinging every 5s is the cadence the Cast protocol expects and keeps the connection alive through those idle gaps.
Fixes#439.
cc
Session resume of a Qobuz track no longer forces the streaming start-position path, which buffered the CMAF stream sequentially from byte 0 up to the saved offset (up to ~30s wait, with no buffering indication).
v2_play_track is now always called without a start position. The track resumes at the saved offset only on a cache hit (format_id === null), where the audio is a full in-memory buffer and Seek lands like a local file. On a cache miss it starts from 0:00 instead of blocking on a sequential buffer fill.
No backend or audio-engine change; the start_position_secs streaming path is now unused.
cc
The graphics recommendation banner no longer writes settings: the Apply button and the v2_apply_graphics_recommendation command are removed, so the banner only detects and explains. The user applies the change with the real toggles, matching the HiFi Wizard's advisory model. write_recommendation stays for the qbz --autoconfig-graphics CLI.
The Force DMA-BUF help text is now GPU-aware: the instability warning only shows on NVIDIA-only systems; non-NVIDIA and hybrid hosts are told it is safe to enable. On hybrid hosts the rendering GPU dropdown marks the integrated GPU as recommended.
cc
NVIDIA-only systems no longer get a hardware-acceleration recommendation that visibly stutters under WebKitGTK. The three NVIDIA-only branches (Wayland+GNOME, Wayland, X11) collapse into one CPU-rendering profile, since XWayland does not rescue the stutter either.
NVIDIA + iGPU hybrid now allows DMA-BUF: WebKit composites on the stable iGPU stack, so the instability that affects NVIDIA-only systems does not apply. The recommendation also tells the user to pick the iGPU explicitly in the rendering GPU selector instead of Auto.
cc
After the app-wide `user-select: none` on body, content the user
legitimately needs to copy stopped being selectable: HiFi Wizard
commands and paths, snap/flatpak/Pulse/PipeWire setup blocks in
Settings, graphics recovery commands, environment-variable hints, log
viewers, keyboard shortcut chips.
The HTML5 elements that wrap literal text — `pre`, `code`, `kbd`, `samp`
— are already the right escape hatch and every wizard/settings/logs
block in the codebase uses them. Adding a single rule alongside the
existing form-input exception restores selection everywhere it matters
without scattering `.selectable` classes through the views.
cc
The compact 220x220 cards in Discovery V2 truncate long album titles in
the metadata row. With the marquee scroll removed there was no way to
read the full title without opening the album detail.
Pipe the title and artist through a native `title` attribute on the
text-stack wrapper; the global SystemTooltip already mounted in the
layout intercepts that and renders the custom tooltip with adaptive
positioning. Shows the full "Title — Artist" string regardless of
truncation, no extra component, no per-card hover state.
cc
`/favorite/getNewReleases` includes albums whose stream availability is
still in the future, so they appear in the Release Watch grid even
though `get_album` 404s as soon as the user clicks them. Same shape for
regional restrictions and label takedowns surfaced via `streamable=false`.
Add `release_date_stream` and `streamable` to the Album struct (both
optional with serde defaults so existing call sites stay happy) and
filter the response inside `v2_get_release_watch` after the artist
blacklist pass: skip any album marked `streamable=false` or whose
`release_date_stream` is strictly greater than today (UTC). The total
count gets adjusted alongside so pagination math stays consistent.
cc
The TrackRowLite click handler was constructing a DisplayTrack with only
the fields shown on the row (id, title, artist, album, albumArt), masked
by an `as DisplayTrack` cast. duration, durationSeconds, hires, bitDepth,
samplingRate and isrc never reached the player, so the NowPlayingBar
showed no time and the seekbar was unusable. The legacy HomeView path
(home:continue_listening) also wired setPlaybackContext +
replacePlaybackQueue before the play call; the discovery-v2 rewrite
dropped both, which left a stale context label and a stale queue after
clicking a track from the section.
Backend already returns the full TrackDisplayMeta (duration_seconds,
hires, bit_depth, sampling_rate, isrc); the wire and the display types
just weren't carrying it. Extend RecoTrackDisplayMeta and
DiscoveryTrackCard with those fields, propagate them in
fetchHomeResolved, and rebuild the click handler to set the
home:continue_listening context and replace the queue with the full
section before emitting onTrackPlay with a complete DisplayTrack.
cc
Bundle token extraction requires the network. When the host is fully
offline at startup, the failure used to propagate out of CoreBridge::new
and leave the bridge as None, which made every queue/playback V2 command
panic at `bridge.get().expect("CoreBridge not initialized")`.
QbzCore::init() now treats bundle extraction as best-effort: on failure
the core still finishes initialization, the queue manager and player stay
usable for local/offline playback, and API methods return
CoreError::NotInitialized cleanly instead of crashing the runtime. Adds
is_api_initialized and try_init_api so callers can rebuild the API
client lazily once the network returns.
cc
Two-pronged improvement to the graphics safety net for users hitting
the NVIDIA + Wayland + WebKit GBM issue:
1) New opt-in toggle 'NVIDIA Wayland compatibility (experimental)' in
Settings > Graphics. When ON:
- Sets __NV_DISABLE_EXPLICIT_SYNC=1 to bypass libnvidia-egl-gbm's
buggy implicit-sync paths that produce "Failed to create GBM
buffer of size NxN: Invalid argument" on driver 550+.
- Lifts the defensive compositing-mode shutdown that QBZ applies
to NVIDIA-only Wayland sessions — the user is signaling the real
fix is now in place, so the safety net is no longer needed.
- Persisted in graphics_settings.db column nvidia_compat_mode (new,
default off). Migrated via ALTER TABLE so existing installs don't
pick up a behaviour change.
2) Reset-graphics CLI affordance, surfaced only when the toggle is on:
inline copyable input + Copy button rendering `qbz --reset-graphics`
(or `flatpak run com.blitzfc.qbz --reset-graphics` based on detected
install method). Works around the global select-disable elsewhere
in SettingsView via user-select: all + onclick={e.select()} + the
explicit button. The boot watchdog still auto-reverts on crash, but
this is the manual escape hatch when the user wants to recover
without waiting for the watchdog to fire.
3) Watchdog recovery now lands in full CPU mode after ANY revert.
The single-knob revert (hw_accel / dmabuf / preferred_gpu) still
narrows the cause for the UI banner, but the safest post-crash
state is full CPU mode (HW accel off + DMA-BUF off + preferred_gpu
reset to auto). A partial revert can still hit the next crash if
the failure was stack-wide (driver bug, missing extension); going
all-the-way-CPU guarantees the user always reaches a paintable
window after a recovery — they can selectively re-enable from the
Settings banner.
4) preferred_gpu='software' now implies hardware_acceleration=off at
env-var application time. Forcing Mesa llvmpipe while keeping
WebKit GPU compositing on would just round-trip software pixels
through a "GPU" path that has no GPU — nonsensical configuration
that the watchdog had to clean up after. Now it's prevented at
source.
i18n keys (en/es/de/fr/pt): settings.graphics.nvidiaCompatMode,
nvidiaCompatModeDesc, resetCmdTitle, resetCmdDesc;
actions.copiedToClipboard.
cc
GitHub Security advisory for file-type < 21.3.1: malformed ASF input
with a zero-size sub-header triggers an infinite loop in the ASF parser
(medium severity). Our exposure is transitive — node-vibrant → jimp →
file-type — and only reached when extracting palette colors from album
artwork. Trusted Qobuz CDN + user-local library limit the practical
attack surface to near zero, but the alert is real and easy to close.
The upstream fix is a 6-line position-advance guard in core.js
(commit 319abf87). Cleanly backports to our 16.5.4 with identical
semantics. Applied via patch-package so the change survives every
`npm install` and is visible in the repo.
Direct upgrade to 21.3.1 wasn't viable: file-type went ESM-only at
17.0.0, and our jimp 0.22.x chain still uses CommonJS `require()` —
an override would have broken vibrant at runtime. Patch-package is
the right tool when upstream's only fix lives behind a breaking
module-system change.
Added `postinstall: patch-package` to package.json so the patch
applies automatically on fresh installs.
cc
DiscordIpcClient::new became infallible in 1.0 — it now returns Self
directly instead of Result<Self>. Failures surface at connect() time
where the catch already lives. Removed the now-spurious `if let Ok()`
wrap around the constructor.
Closes#428 (applied manually because the bump needed a code change
that Dependabot can't author).
cc
Three bugs were stacking up to make session resume drop the saved offset
for streamed Qobuz tracks:
1. Position save was debounced, not throttled. Each playback tick reset
the 5s timer, so the save never fired while a track played
continuously — the persisted position froze at whatever value got
written during the first quiet window after track start (typically
~2s). Switch to throttle: fire the first call immediately, then once
per 5s window, with a tail call to flush the latest value when the
window ends.
2. togglePlay tried to apply the restore offset via a separate cmdSeek
right after cmdPlayTrack. For Qobuz streaming the audio thread's
Seek handler bails when current_audio_data is unset — streaming
uses current_streaming_source instead — so the seek was silently
dropped and the track started at 0:00. Pass start_position_secs
directly to v2_play_track. The streaming backend (legacy + V2
player) waits for enough buffer to cover the offset plus 8s
headroom, then eagerly decode-and-discards decoder output up to
the offset before engaging the audio engine — no race, no
first-pull underrun. Plex and local tracks keep the old play+seek
path; they load full audio_data so Seek works as-is.
3. The seekbar showed 0:00 from app launch until the user hit play,
even when a saved position was queued. setPendingSessionRestore
now pre-positions currentTime when positionSecs > 0 so the UI
reflects "paused at X" the moment the app opens.
Other plumbing:
- AudioCommand::PlayStreaming carries start_position_secs and
content_length (used to estimate bytes-per-second for the resume
buffer target).
- Player::play_streaming{,_dynamic} grow a start_position_secs
parameter; QConnect and api_server callers pass 0 (remote-driven
playback always starts fresh).
- cmdPlayTrack forwards startPositionSecs as a 5th optional arg.
cc
Solves the long-running performance complaints (especially Optimus
laptops where the dGPU silently hijacks WebKit). The end-to-end shape:
- graphics_detection enumerates physical GPUs from /sys/class/drm and
cross-references EGL vendor JSONs under /usr/share/glvnd/egl_vendor.d
to expose only stacks the running process can actually use. Works
inside Flatpak/Snap because /sys is still mounted read-only and the
sandbox-visible EGL vendor JSONs are the right thing to read.
- preferred_gpu column in graphics_settings.db (string: auto, integrated,
discrete, software, or a specific PCI slot id). Migrated with
ALTER TABLE ... DEFAULT 'auto'.
- env_vars_for_preferred_gpu returns Set/Unset actions applied at startup
before WebKit init. Integrated/AMD path explicitly unsets
__NV_PRIME_RENDER_OFFLOAD so KDE/Optimus sessions that pre-set it to 1
no longer override the EGL pin and yank rendering back onto NVIDIA.
- Auto resolver: on hybrid systems (iGPU + dGPU both usable), Auto pins
to the iGPU. Single-GPU and no-GPU systems remain no-op. The dropdown
surfaces what Auto resolved to ("Auto (Intel Graphics)") so the user
sees the decision without picking explicitly.
- boot_watchdog writes a pending.json before WebKit init listing the
risky knobs in effect. Frontend calls v2_mark_boot_succeeded after
first paint to clear it and promote to last-good.json. If the next
launch sees pending without a success ack, it blames matching fields,
reverts them in-DB, and increments per-knob failure counters; two
consecutive failures lock the knob until the user clears it.
- Settings > Graphics gets a Rendering GPU dropdown (between HW accel
and DMA-BUF) and a crash-recovery banner that lets the user re-enable
any auto-disabled knob. Restart-required affordance shared with the
other risky toggles.
- i18n keys for the dropdown, banner copy, and autoWithTarget label
across all five locales.
cc
180x180 card with a 114x114 artwork inset, palette extraction for the
background gradient and mix-blend-mode multiply on the PNG bezel so the
station artwork sits in front of a cohesive colored frame. RADIO label
uses padding-left compensation to align with the visible bezel edge,
not the raw container. Hover overlay blurs the artwork and surfaces a
play button — same pattern as the iOS reference shipped in c8cee326.
cc
autoconfig_graphics gains a hybrid Intel/AMD + NVIDIA branch that keeps
GSK renderer on Auto and DMA-BUF off so the iGPU handles compositing
without the dGPU getting dragged in. detect_gpu_name is exposed and
labels hybrid setups explicitly (e.g. "NVIDIA (driver 580) + Intel
(i915)"). RuntimeDiagnostics gets runtime_gpu_name and
runtime_desktop_environment so the Diagnostics panel can show what the
app sees at runtime, which is the first thing useful to triage
performance reports from log uploads.
cc
The Library Albums tab carried three different quality-badge designs
across three rendering paths (grid card, list row, album detail
header), each rendered as raw text — visually disconnected from the
QualityBadgeStatic that Mixtape and Collection lists already use, and
the Plex source icon was inconsistent (present on grid cards, missing
on list rows and the album detail header).
Three coordinated swaps so all surfaces speak the same visual
language:
- AlbumCardLibraryLite (Library grid + Artist Detail tile). The
custom .quality-pill text badge is replaced by
<QualityBadgeStatic iconOnly>, matching the Discovery V2 layout —
icon-only square sitting to the right of a two-line title/artist
stack. Adds format / bitDepth / samplingRate props so the badge
can render its real tooltip ("FLAC: 16-bit / 44.1 kHz") instead
of falling back to the parsed quality string.
- VirtualizedAlbumList list mode. The plain-text .quality-badge
span is replaced by <QualityBadgeStatic> full mode (the
Mixtape/Collection badge), plus an inline <SourceBadge> when
showSourceBadge is true so Plex / Qobuz Purchase / Qobuz Download
surfaces are recognizable in the row. resolveSourceBadge maps
the LocalAlbum.source string onto the narrow
LibrarySourceBadge enum the card prop expects.
- LocalLibraryView album detail header. The audio-specs strip now
ends with a <SourceBadge> tied to selectedAlbum.source. Pairs
naturally with the FLAC / 16-bit / 44.1 kHz pills already there
and finally lets the user spot at a glance whether the album
they're looking at came from a network Plex server or local
disk.
Call sites in LocalLibraryView (the renderMetadataAlbumCell snippet
and the Artist Detail tile loop) pass album.bit_depth and
album.sample_rate / 1000 alongside the existing quality string so
the badge tooltips read accurate numbers. VirtualizedAlbumList
forwards the same trio to its inner AlbumCard.
cc
The Plex aggregation CTE used MAX(codec)/MAX(bit_depth) directly,
which produced two bugs that surfaced in the Albums grid for many
Plex-only libraries:
- format = "Unknown". Plex frequently omits the stream-level codec
when its analyzer hasn't seen the file yet. MAX(NULL) collapsed
the row's format to NULL, which the frontend parser mapped to
AudioFormat::Unknown — the Library grid then showed UNKNOWN even
though the file's container was clearly "flac" / "mp3".
- bit_depth = NULL. Plex commonly omits bitDepth on older releases
entirely; the album row inherited the gap and the UI rendered
"--/44.1" next to an otherwise valid quality string.
Two fallbacks applied to the plex_aggregated CTE only — the local
CTE is untouched because local indexing always writes a non-null
format string and a real bit depth via Symphonia:
- format: COALESCE(MAX(codec), MAX(container)). The `container`
column is populated for the same media and typically carries the
same value ("flac", "mp3"), so it's a safe fallback when codec
is fully NULL across the album.
- bit_depth: COALESCE(MAX(bit_depth), CASE WHEN <lossless> AND
sample_rate <= 48000 THEN 16 ELSE NULL END). Lossless formats
at CD-range rates are virtually always 16-bit redbook, so we
default to 16 in that narrow window. Higher rates stay NULL so
we don't lie about 24 vs 32. The per-track view shows the real
value when the user clicks in (lazy hydration via
plex_get_track_metadata).
cc
The bottom-right song card (artwork + title + album + artist + quality
badge) was duplicated inline in nine visualizer panels — LineBed,
CometFlow, NeonFlow, EnergyBands, Oscilloscope, Lissajous,
SpectralRibbon, TransientPulse, and TunnelFlow. Each carried its own
HTML structure and ~90 lines of identical CSS for the song-card
chrome. Any visual tweak (badge swap, layout shift, album/artist
inline join, artwork sizing) had to be replayed nine times and was
easy to miss one — the recent LineBed-only inline-album·artist change
exposed exactly this trap.
Extracts the song card into src/lib/components/immersive/Immersive
SongCard.svelte, which owns its own absolute positioning, the
two-line text stack with the new "album · artist" subline, the
QualityBadgeStatic mount, the 72px artwork thumb, the explicit-badge
overlay, and the responsive breakpoint. Each consumer panel now drops
in `<ImmersiveSongCard {artwork} {trackTitle} {artist} {album}
{explicit} {quality} {bitDepth} {samplingRate} {format} />` instead
of carrying its own copy.
Net effect: 268 insertions across all ten files vs 948 deletions of
duplicated markup and CSS — and the next visual change touches one
file, not ten. SuggestionsPanel uses a totally different layout
(queue list) and is intentionally NOT migrated.
cc
JavaScript bitwise shift operates on 32-bit signed integers, so
`1 << 48` is `1 << (48 % 32)` = `1 << 16` = 65536. Every Qobuz track
id sits above that, so the existing `id >= EPHEMERAL_ID_FLOOR` checks
were classifying every Qobuz track as ephemeral and disabling the
favorite / playlist / mixtape / collection actions for them — most
visibly in the immersive player favorite button, but also silently
stripping Qobuz tracks from the persisted session queue.
Five places shared the same broken constant, all now use `2 ** 48`
(281474976710656) — the actual ephemeral id floor defined in
src-tauri/src/ephemeral_library/mod.rs:
- src/routes/+page.svelte (two metadataActionsDisabled checks for
the immersive player and now-playing bar, plus the session-save
EPHEMERAL_ID_FLOOR filter).
- src/lib/components/QueuePanel.svelte (isEphemeralQueueTrack guard
that hides favorite / playlist / track-info actions for queue
rows).
- src/lib/stores/playerStore.ts (isEphemeralTransition flag in the
gapless carve-out and CUE virtual-track position translation).
- src/lib/components/views/LocalLibraryView.svelte (close-folder
cleanup check — only ephemeral folders should wipe the queue +
now-playing slot when closed).
Each call site keeps a short comment naming the trap so future edits
don't reach for the same bitwise expression.
cc
CSS-only flex stretch + aspect-ratio collapsed instead of sizing the
thumb against the track-meta column — the previous two attempts
landed with no visible change (the thumb stayed near its old fixed
72px or fell back to image-intrinsic). aspect-ratio interacts poorly
with align-self: stretch when the item has no natural width to drive
the stretched dimension off.
Drops the CSS-only attempt. Binds clientHeight on .track-meta with
Svelte's bind:clientHeight, derives 98% of that into artworkSize, and
applies the result as inline width/height on the thumb. Reactive —
when the badge or text lines re-flow, the column height changes and
the thumb resizes with it. No fallback fixed size needed because the
{#if artwork && artworkSize > 0} guard avoids the first-paint flash
before the bind resolves.
cc
The previous attempt wrapped the thumb in a flex container with the
inner thumb at height: 98% — percentage height didn't resolve
because the wrapper's own height was content-determined, leaving the
inner thumb at near-zero and falling back to natural width. Visual
result: artwork stayed near the old fixed size, much smaller than
the column.
Drops the wrapper, sets align-self: stretch directly on the thumb so
it inherits the column height through the parent's align-items:
stretch chain, and uses aspect-ratio: 1 with auto width so the thumb
stays square. The 2% breathing room comes from margin-block: 1px,
which subtracts 2px from the stretched height in a way percentage
sizing couldn't reach.
cc
The bottom-info song card on LineBed pairs a four-row column (title +
album + artist + quality badge) with a square artwork thumb. With the
quality badge added the column grew to ~100px while the thumb stayed
at its old 72px fixed size, breaking the visual weight balance — the
column dominated the card and the thumb read as an afterthought.
Wraps the thumb in a flex container that stretches to the column
height, then sizes the thumb to 98% of that height with aspect-ratio
1 so it stays square. The 2% breathing room reads as a deliberate
gutter rather than a flush match, and the thumb sits vertically
centered against the column instead of pinned to either edge.
The fixed 72/56 dimensions and the mobile media-query override drop
out as a side effect — the thumb now scales with the actual content
height, so the breakpoint clamp isn't needed.
cc