Long sessions died when a single transient blip flipped the app to offline and
the queue then skipped every cached-but-not-downloaded track. Harden the
detector:
- Consecutive-failure hysteresis: declare offline only after 2 failed polls,
not a single blip.
- Replace the HEAD https://1.1.1.1 probe with a concurrent race of cheap
generate_204 endpoints, so a saturated link (long Hi-Res tracks) no longer
times the probe out.
- Positive liveness override: if a segment downloaded within the last 45s we
are online by definition, regardless of the probe.
cc
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
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
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
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
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
`/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
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
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
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 detection + decision matrix in autoconfig_graphics.rs already knew
the right combination for every (GPU vendor x display server x desktop)
case, but the only way to consume it was the qbz --autoconfig-graphics
CLI tool which most users never run. The recommendation sat invisible
while users hunted toggles by hand.
Lifts the same logic into the Settings UI as a persistent banner at
the top of the new Graphics tab:
- autoconfig_graphics: Environment and Recommendation structs become
public with Serialize, the three pieces (detect_environment,
compute_recommendation, write_recommendation) become pub so they
can be re-used. The existing apply_recommendation CLI helper now
delegates writes to the shared write_recommendation, keeping the
behaviour identical (and inverting the DMA-BUF mapping so that
rec.disable_dmabuf=false now writes force_dmabuf=true, matching
the 1.2.13 opt-in semantics).
- commands_v2/graphics_recommendation.rs (new): two thin Tauri
commands — v2_get_graphics_recommendation returns the environment
plus the computed recommendation so the frontend can compare it
to persisted settings; v2_apply_graphics_recommendation writes
the whole bundle in one call.
- Settings UI: the Graphics tab loads the recommendation on mount.
A $derived computes whether at least one recommended value
diverges from the four persisted settings (HW accel / force_x11 /
gsk_renderer / force_dmabuf with the inverted DMA-BUF axis).
When it does, a violet-accent banner appears above the profile
grid showing the detected environment, the rationale lines from
the matrix, and an Apply button. Persistent (no dismiss) — vanishes
the moment the persisted state matches the recommendation, no
second copy of state to keep in sync.
- i18n: 4 new keys per locale (title / detected / apply / applied)
across en/es/de/fr/pt. The rationale strings stay in English
because they're hardcoded in autoconfig_graphics.rs and reword
them per scenario would mean a structured i18n key per case —
not worth it for diagnostic copy that's read once and dismissed.
cc
The DMA-BUF renderer was on by default for AMD/Intel users with
specific NVIDIA carve-outs to disable it. The asymmetry was load-
bearing on a fragile feature: when DMA-BUF breaks on Linux the
failure mode is catastrophic (black window, missing textures, full
viewport corruption) rather than degraded, and the auto-detection
couldn't anticipate hybrid Intel+NVIDIA edge cases that surface only
on specific Mesa/NVIDIA driver pairings. The perf gain when it works
is marginal — ~5-10% memory bandwidth on heavy compositing, not
material for a music UI.
Flips the policy:
- DMA-BUF off by default for everyone.
- Users opt in via the existing Settings > Developer >
force_dmabuf toggle (already persisted; just inverts the
semantic from "force even though heuristics say off" to "turn
it on at all").
- QBZ_HARDWARE_ACCEL=1 explicit env override still bypasses
everything as before (power-user escape hatch).
- QBZ_DISABLE_DMABUF=1 still respected for explicit logging.
Removes the NVIDIA-Wayland / NVIDIA-X11 carve-outs because they're
strict subsets of "always off" now. Keeps the Wayland+NVIDIA-only
compositing disable — that's a separate axis (EGL protocol errors
corrupt the whole window, not just texture sharing) and still has
to fire on that specific stack.
User-visible startup log changes from "Using default WebKit
renderer (full hardware acceleration)" to "DMA-BUF renderer off by
default (opt-in via Settings > Developer)" for the common path.
cc
The PersistedQueueTrack struct already had streamable and parental_warning
fields but the load path hardcoded streamable=true and parental_warning
=false because the columns didn't exist in queue_tracks. source_item_id
_hint was missing entirely.
Adds the three columns via the same idempotent ALTER-TABLE pattern the
existing migrations use, persists them in save_session, and reads them in
load_session. PersistedQueueTrack gains source_item_id_hint as an
Option<String> with serde default so saves from older builds deserialize
cleanly.
User-visible effect: a restored queue now keeps explicit-content badges,
unstreamable visual state, and Mixtape/Collection source-item ids
matching the live queue instead of resetting to defaults at every
launch. Backwards-compatible: columns get safe defaults for older rows
(streamable=1, parental_warning=0, hint=NULL) so users upgrading from
1.2.12 don't see a regression.
cc
The playback-event polling loop now drains
SharedState::take_stream_error_message and emits a Tauri
audio:init-failed event with the user-readable cause. Drain semantics
guarantee the toast fires exactly once per error instead of every
tick of the polling loop.
Plex cache lives in a separate SQLite database
(`<data_dir>/qbz/plex_cache.db`), which is why `v2_library_get_albums_page`
previously only saw local_tracks and the LocalLibraryView chunked
path had to fall back to the legacy virtualizer when Plex was on.
That fallback meant Plex-dominant users (which is the common case)
got none of the chunked-store / recycling-pool perf work.
Now consolidated server-side via SQL `ATTACH DATABASE` + `UNION ALL`:
Backend (qbz-library):
- `get_albums_metadata_page` accepts `plex_cache_path: Option<&Path>`.
When the file exists it `ATTACH`es the plex DB to the connection,
runs a CTE that aggregates `plex_cache.plex_cache_tracks` by
`album_key` into the same column shape as local_aggregated, and
UNIONs both before the filter/sort/pagination step. DETACHes at
the end so subsequent queries don't see stale attachment.
Plex group_key prefixed with `'plex:'` so it doesn't collide with
local group keys; `'plex'` literal in the `source` column drives
the frontend's SourceBadge mapping.
- Schema differences smoothed via `CAST` in the plex arm:
`duration_ms / 1000 → total_duration` (matches local seconds),
`sampling_rate_hz → sample_rate` (INTEGER → REAL).
- `count_albums_metadata_for_page` mirrors the same UNION so total
is consistent across empty-page edge cases.
Backend (Tauri command):
- `v2_library_get_albums_page` accepts an `include_plex` bool.
When true, resolves the plex cache path the same way
`plex/mod.rs::open_plex_cache_db` does and passes it through.
Frontend:
- Chunked store's `LocalAlbumsQueryParams` carries an `includePlex`
flag, included in the `paramsEqual` check so toggling Plex
visibility invalidates the chunk cache.
- LocalLibraryView drops the `!isPlexLibraryEnabled()` guard from
both the `$effect` that calls `setParams` and the render
condition that picks chunked vs legacy. The new path serves
Plex-only, local-only, and mixed libraries the same way.
cc
Phase A of the Plex-style port: the Albums grid no longer keeps the
full collection (1200+ rows on the user's library) in reactive Svelte
state. Instead, it pulls a single chunk at a time from a new Tauri
command, with the recycling-grid pool firing `requestIndex` for
indices whose data hasn't arrived yet.
Why this matters: Phase B (recycling pool against an in-memory array)
removed mount/unmount churn during scroll but left two costs in
place — every reactive tick still ran the `$derived` filter/sort
pipeline over all 1200 records, and the records themselves still
sat in Svelte's reactivity graph. Under software compositing the
combined cost was still the dominant scroll-time spike.
Server-side pagination eliminates both: sort + filter run in SQL,
only the visible chunk's records are in memory, and the per-tick
$derived work disappears.
Backend:
- `qbz-library/src/models.rs` — new `AlbumsMetadataPage` struct
(`{ albums, total }`) returned by the paginated query.
- `qbz-library/src/database.rs` — `get_albums_metadata_page`:
reuses the existing track-aggregation CTE from
`get_albums_metadata_grouped`, adds a HAVING-style search filter
over the aggregated title/artist (mirroring the legacy
`matchesAlbumSearchFast` semantics), validated allowlist-based
`ORDER BY` for artist/title/year × asc/desc, and
`LIMIT ?/OFFSET ?` on the filtered set. Total comes from
`COUNT(*) OVER ()` in the same query for one round-trip; a
fallback `count_albums_metadata_for_page` covers the empty-page
case where the window function has no row to attach to.
- `src-tauri/src/commands_v2/library.rs` — new
`v2_library_get_albums_page` Tauri command, registered in
`lib.rs`. `include_qobuz_downloads` is read from
`DownloadSettingsStore` to match the legacy
`v2_library_get_albums_metadata` behavior.
Frontend:
- `src/lib/discovery-v2/localAlbumsChunkedStore.svelte.ts` — new
class-backed store with `version` + `total` as `$state`, sparse
`Map<chunkIdx, LocalAlbumLite[]>`, in-flight `Set<chunkIdx>`
dedup, and a generation counter that drops stale responses if
the user switches filters mid-flight. `getAlbum(globalIdx)` is
sync (null for missing chunks); `requestIndex(globalIdx)` fires
the fetch.
- `src/lib/components/views/LocalLibraryView.svelte` — when the
Albums tab is in grid mode AND grouping is off AND no quality
filters are active (the common case), render through
`AlbumGridPool` bound to the chunked store. Other paths
(grouped, filtered, list mode) keep the legacy
`VirtualizedAlbumList` so we don't lose features we haven't
server-side yet. A `$effect` mirrors search/sort/source state
into `setParams` whenever they change; the store no-ops on
unchanged params.
Plex consolidation (other source listed in the audit) deferred to a
follow-up: `plex_cache.db` is a separate SQLite file, so unifying
needs `ATTACH DATABASE` + `UNION ALL` from the same aggregation,
which is a clean delta once the local-only path is verified.
cc
User finding (2026-05-12): under SW compositing, toggling the
"Match system window chrome" setting OFF turned an unusable modal
experience into instant popups. The transparent main window forces
GTK to repaint the client-side decoration chrome (rounded corners +
subtle edge + window shadow) on every frame. Under GPU compositing
the cost is imperceptible; under SW compositing it becomes the
dominant frame-budget cost — modals, the splash spinner, and any
other animation visibly drop frames because each tick the WebKit
surface has to be recomposited against the CSD chrome.
Auto-gate the transparent window on hardware acceleration so users
in CPU rendering mode (whether by setting or by env var) don't have
to discover this themselves. While we work toward Slint, this is the
single biggest win available for the SW path.
Implementation:
- `should_use_main_window_transparency()` in src-tauri/src/lib.rs
now reads QBZ_HARDWARE_ACCEL env var (mirroring main.rs) and
falls back to graphics_settings.db.hardware_acceleration. When
HW accel is off, returns false regardless of the user's
match_system_window_chrome preference. The window is built
opaque; the frontend's `windowTransparent` reads false from the
Rust side; the `.match-chrome-transparent` CSS class never
activates. Round corners disappear, opaque rectangle remains.
- The user's chrome preference is preserved in the DB. When HW
accel is re-enabled (next restart with QBZ_HARDWARE_ACCEL unset
or set to 1), the preference reactivates automatically.
- SettingsView shows a `<small class="setting-note">` under the
chrome toggle when `isHardwareAccelEnabled()` is false,
explaining the override. Toggle itself stays interactive so the
user can still set their preference for the next HW-accel-on
session.
- New i18n key `settings.appearance.matchSystemChromeCpuOverride`
in all 5 locales.
cc
The earlier default of `hardware_acceleration = 0` was set "for maximum
compatibility" but in practice it shipped a severely degraded experience
to every user with a working GPU. Under WebKitGTK software compositing
(what hardware_acceleration=0 forces via WEBKIT_DISABLE_COMPOSITING_MODE
in main.rs:477) mounting any new component takes multiple seconds on
4K / tiled windows — confirmed during the 2026-05-12 perf session where
the same modal that took 2-3s to open under SW comp opened instantly
with HW accel on.
The mitigation logic in `main.rs:482-496` already handles known-broken
configs (NVIDIA-only Wayland forces compositing off; AMD/Intel/hybrid
configs run with compositing enabled). With HW accel = ON as the new
default, that mitigation logic actually runs — previously the nuclear
"off everything" branch at `main.rs:468` was the de-facto path for
every user who hadn't explicitly opted in.
Schema change is non-destructive: `INSERT OR IGNORE` only seeds the
row on first run, so existing users who already have a row keep their
preference. Only fresh installs (and the test environment after
`DELETE FROM graphics_settings`) pick up the new default.
cc
Hotfix release. Reverts titlebar-mode-selector and PR #406 (broken
window chrome on Linux); preserves PR #407 (sticky home header) and
adds PR #408 (macOS native icon and tray variants by @Vudgekek).
cc