- Detect Flatpak sandbox at startup and log it
- In Flatpak on Wayland, unset DISPLAY to prevent WebKitGTK from
falling back to XWayland internally (causes CPU-GPU texture
round-trips that degrade CSS blur performance)
- Per-GPU Wayland paths: only disable compositing and DMA-BUF for
NVIDIA on Wayland. AMD/Intel get full GPU acceleration on native
Wayland (compositing enabled, DMA-BUF enabled)
- Change GitHub Releases Flatpak manifest from --socket=x11 to
--socket=fallback-x11 to match Flathub and prevent XWayland
on Wayland sessions
Remove Spotify API credential setup from README since playlist import
now uses embed scraping. Update resolve_via_odesli_and_search doc
comment to reflect the new Spotify data path.
Spotify client_credentials API access ends 2026-03-06. Remove all
API token/proxy code and the paginated fetch_playlist_with_token
function. Playlist imports now go straight to embed page scraping
(limited to ~50 tracks, no ISRC/album data).
Link resolver for Spotify tracks/albums also switched from API to
embed page scraping via new fetch_embed_metadata function.
Remove Spotify credentials from .env.example since they are no
longer needed.
insert_qobuz_cached_track_direct was hardcoding track_number to 0,
causing all imported tracks to show wrong numbers. Now accepts
track_number and disc_number params, and both callers (backfill
downloads and offline cache sync) extract metadata from the audio
file via MetadataExtractor before inserting.
Also fix LocalLibraryView to use || instead of ?? for track_number
fallback, so 0 correctly falls back to index-based numbering.
Closes#147
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.
ALSA Direct playback thread now supports gapless via mpsc channel:
when current source ends, it checks for a queued next source and
transitions seamlessly without draining the PCM buffer.
Format mismatch still blocks gapless (bit-perfect has priority).
Gapless now defaults to enabled for all backends.
v2_set_audio_gapless_enabled, v2_set_audio_normalization_enabled, and
v2_set_audio_normalization_target now call player.reload_settings()
after writing to SQLite, so changes take effect immediately without
requiring an app restart.
Embeds quality in album folder name instead of subfolder, so each
quality variant appears as a separate album in the local library.
Adds download error toast and fixes format labels to bracket notation.
Closes#116
Normalize notification artwork to square PNG (max 512x512, <=4 MiB) before sending Icon::Bytes via ashpd portal, so album art renders reliably without legacy fallback.\n\ncx
The XDG portal notification spec's icon field doesn't support inline
album artwork the way freedesktop Notifications does with image-path.
Dual-path approach:
- Native (non-Flatpak): use org.freedesktop.Notifications D-Bus
interface directly with image-path hint for album artwork
- Flatpak (sandboxed): use ashpd portal (Flathub requirement)
Uses ashpd::is_sandboxed() for detection and ashpd's re-exported
zbus for the direct D-Bus call, avoiding extra dependencies.
The XDG portal was not displaying album artwork when passed as a
file:// URI (showed KDE default icon instead). Send artwork as
Icon::Bytes so the portal receives the image data directly.
Replace direct D-Bus notifications (notify-rust) with XDG Desktop
Portal notifications (ashpd) for Flathub compliance. Portal
notifications use the app's .desktop file identity and don't require
--talk-name=org.freedesktop.Notifications permission.
- Replace notify-rust dep with ashpd (linux-only, tokio+notification)
- Use NotificationProxy for portal-based notifications
- Use stable ID "track-now-playing" to replace instead of stacking
- Convert artwork path to file:// URI via ashpd::Uri
- Graceful fallback when portal is unavailable
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.
v2_purchases_get_all and v2_purchases_get_by_type only set the
downloaded boolean but never populated downloaded_format_ids, so
check marks disappeared after app restart. Now both use
get_downloaded_purchase_formats() and build the format map, matching
the behavior already present in v2_purchases_get_album.
Fixes#119
Detects GPU (NVIDIA/AMD/Intel), display server (Wayland/X11), desktop
environment, and VM status. Computes optimal configuration based on a
decision matrix and writes it to the graphics settings DB.
Usage: qbz --autoconfig-graphics
Part of #127.
Add gsk_renderer field to graphics settings DB with UI dropdown
(Auto/GL/NGL/Vulkan/Cairo). Value is read at startup and set as
GSK_RENDERER env var. Env var override takes precedence. Also
included in --reset-graphics and reset now restores hw_accel to true.
Part of #127.
Add is_amd_gpu() and is_intel_gpu() detection functions alongside
existing is_nvidia_gpu(). Update GraphicsStartupStatus to expose
has_amd and has_intel to frontend. Log detected GPUs at startup.
Part of #127.
The hardware_acceleration toggle in settings was dead code — stored in
DB but ignored at startup (main.rs only checked QBZ_HARDWARE_ACCEL env
var). Now the DB value is read as the default, with env var override.
Fixes part of #127.
The ContextType enum and v2_set_playback_context match were missing
fav_q and top_q variants, causing an "Invalid context type" error that
blocked all playback from FavQ and TopQ views. Also wrap
setPlaybackContext calls in try/catch so context errors don't prevent
track playback.
Fixes#140
The "free(): corrupted unsorted chunks" crash was caused by secondary
windows (miniplayer) being closed twice during shutdown:
1. CloseRequested handler closes secondary windows before main closes
2. RunEvent::Exit handler tried to close them again
Calling close() on a window already mid-teardown causes WebKit's
EGL/GPU thread to hit a double-free during TLS cleanup, corrupting
the heap. This was visible even without audio playing.
Fix: use an AtomicBool flag (WINDOWS_CLOSED_BY_USER) set in
CloseRequested, checked in RunEvent::Exit. Secondary windows are
only closed in Exit if CloseRequested didn't already handle them
(e.g., SIGTERM from tauri dev hot reload).
Two fixes for issue #139 (window opens larger than screen):
1. Screen resolution clamping at startup: detect monitor size via
xdpyinfo/xrandr and clamp saved window dimensions to 95% of screen.
Prevents windows from opening larger than the display even if the DB
has valid-but-too-large values.
2. Physical-to-logical pixel conversion in resize handler: the Resized
event gives physical pixels but inner_size() expects logical pixels.
With scale factor > 1.0, the window would grow each restart because
physical values were saved and restored as logical. Now divides by
scale_factor before persisting.
Closes#139
The smart quality downgrade was only in the network fetch path, but
tracks served from cache (memory L1, disk L2, offline) bypassed it
entirely. Now all cache paths probe the FLAC header for sample rate
and skip the cache if the hardware doesn't support it, falling through
to the network path which re-requests at Hi-Res quality.
Also applies quality downgrade to prefetch so upcoming tracks are
fetched at compatible quality instead of caching incompatible data.
When ALSA backend detects hardware doesn't support a track's sample rate
(e.g. 176400Hz on a DAC that supports up to 192000Hz but not 176.4kHz),
automatically re-request the track from Qobuz at Hi-Res quality instead
of Ultra Hi-Res. This avoids resampling and maintains bit-perfect playback
by getting a version at a rate the hardware natively supports.
Adds device_supports_sample_rate() and get_device_supported_rates()
public API to qbz-audio for hardware capability queries via /proc/asound.
Previous commit removed the miniplayer close from CloseRequested,
but that kept the app alive because Tauri won't exit while windows
remain open. Now closes secondary windows with close() (not
destroy()) in CloseRequested before the main window closes.
Also hides the miniplayer when close-to-tray activates.
The miniplayer window was being destroyed twice during shutdown:
once in CloseRequested and again in RunEvent::Exit. This caused
WebKit's TLS destructors to collide during process teardown,
resulting in SIGABRT in WebKitWebProcess.
Changes:
- Remove miniplayer destroy from CloseRequested (was premature)
- Guard global on_window_event to only handle main window
- In RunEvent::Exit, close all secondary windows (miniplayer, oauth)
with close() instead of destroy() for graceful WebKit cleanup
- Add 100ms delay after closing secondary windows to let WebKit
tear down EGL/GPU resources before process exit
Extends PR #136 (save-time validation) with load-time validation.
If the DB contains corrupt sizes (e.g. 9084748x62267212), get_settings()
now returns safe defaults instead of passing garbage to GDK/Cairo.
Also extracts validation into a shared helper and adds NaN/Inf checks.
v2_get_hardware_audio_status was reading sample_rate/bit_depth from the
legacy player state, which is never updated since playback runs through
the V2 player (CoreBridge). Now reads from V2 player first, falls back
to legacy only if CoreBridge isn't initialized yet.
The OAuth login path in runtime_bootstrap was missing the audio settings
sync code that exists in the email/password path. Users logging in via
OAuth had backend_type remain None, causing legacy CPAL fallback and
requiring manual settings reconfiguration on every startup.
rodio 0.22's with_supported_config() resets buffer_size to Default via
..Default::default(), so our BufferSize settings in StreamConfig were
never reaching CPAL. MixerDeviceSink has zero internal buffering, making
CPAL's buffer the ONLY buffer between the mixer and audio hardware.
Chain .with_buffer_size(Fixed(sample_rate/10)) AFTER with_supported_config()
to actually pass ~100ms buffer to CPAL, matching old vendored cpal behavior.
PipeWire manages its own buffer scheduling via quantum. Forcing a fixed
buffer size (sample_rate / 10 = 19200 frames at 192kHz) conflicts with
PipeWire's internal scheduling and causes buffer underruns that produce
audible noise.
BufferSize::Fixed is kept only for:
- Exclusive mode (512 frames, low latency)
- ALSA backend (direct ALSA, not through PipeWire)
Replace BufferSize::Default with BufferSize::Fixed(sample_rate / 10)
for non-exclusive mode. This preserves the 100ms period tuning that was
previously applied in the vendored cpal ALSA backend, reducing CPU
wakeups during playback.
- OutputStream/OutputStreamHandle -> DeviceSinkBuilder/MixerDeviceSink
- Sink -> Player (as RodioPlayer)
- current_frame_len -> current_span_len in all Source impls
- Source::channels() returns NonZero<u16>, sample_rate() returns NonZero<u32>
- SamplesBuffer no longer generic, constructor takes NonZero args
- SampleRate is now u32 type alias (no tuple struct)
- Decoder::new_mp4 no longer takes Mp4Type hint
- Applied across both crates/ workspace and src-tauri/ legacy code
Remove vendored rodio and cpal from src-tauri/vendor/ since rodio 0.22
now includes upstream f32 decoder support (our vendored modification).
Update all Cargo.toml files to use rodio 0.22 from crates.io and remove
the [patch.crates-io] section.
Handle RunEvent::Exit (fires on SIGTERM from tauri dev hot reload)
to stop audio playback and visualizer before process teardown. The
previous CloseRequested handler only fires on user-initiated close,
not on SIGTERM, leaving ALSA/CPAL C threads active during cleanup
which causes "free(): corrupted unsorted chunks" heap corruption.
- Switch from .run(context) to .build(context).run(callback)
- Stop visualizer and both legacy/V2 players on Exit event
- Destroy miniplayer window on Exit event
Replace browser-side pdfjs-dist canvas rendering with server-side
MuPDF rendering for high-quality PDF booklet display. MuPDF renders
pages to PNG at configurable DPI in Rust, producing much sharper
results than the JavaScript canvas approach which had persistent
JPEG 2000 WASM decoder issues.
- Add mupdf crate with sys-lib features (avoid GTK symbol conflicts)
- Create pdf_viewer.rs with v2_booklet_open/render_page/close commands
- Download PDF to temp file, render pages on demand via MuPDF
- Display rendered PNGs as img tags instead of canvas
- DPI scales with zoom and devicePixelRatio for crisp output
- Support rotation via MuPDF transformation matrix
PDF loading was failing because pdfjs-dist cannot fetch from
static.qobuz.com due to CORS restrictions in the WebView.
Added v2_fetch_url_bytes command that fetches URL bytes through
Rust's reqwest (no CORS), then passes the binary data to pdfjs.
Also added rotation support (R key or toolbar button) for
vertical booklets.