Add get_all_custom_artist_images() database method and
v2_library_get_all_custom_artist_images command to support
loading all custom artist image overrides at once for the
global resolution store.
- Auto-detect installed .desktop filename (com.blitzfc.qbz.desktop,
qbz.desktop, qbz-nix.desktop) instead of hardcoding
- Run update-desktop-database after register/deregister so xdg-open
and browsers pick up the MIME association immediately
- Fix v2_check_qobuzapp_handler to match all known desktop filenames
- Align StartupWMClass to com.blitzfc.qbz across all packaging formats
- Add StartupNotify=true to Arch/AUR/Snap desktop files
Fixes browser URI handler not working for .deb users (Discussion #44)
The reload_settings() call at bootstrap is now gated behind a new
sync_audio_on_startup setting (default OFF). Most users don't need
it; it exists for edge cases where Player::new() holds stale settings
(e.g., after Flatpak updates changing DB schema).
Adds toggle in Settings > Audio with i18n for all 4 locales.
reload_settings() was running before every v2_play_track call, which
is excessive and can overwrite correct in-memory audio settings with
stale DB values (especially in Flatpak where DB sync timing differs).
Moved to runtime_bootstrap Step 6, after session activation when
per-user stores are ready. v2_reinit_audio_device still reloads on
explicit device changes.
On first play after app start, the audio thread held stale settings from
Player::new() — if the user had configured PipeWire/ALSA backend, the
thread might not use the correct backend/device, defaulting to 48kHz.
Now v2_play_track reloads settings from the DB before every play_data()
call, matching what v2_reinit_audio_device already does. This is why
re-selecting the device fixed the issue: it called reload_settings first.
Also extracts convert_to_qbz_audio_settings() helper to deduplicate the
settings conversion between v2_play_track and v2_reinit_audio_device.
Fixes#69
Add /radio/artist and /radio/track Qobuz API endpoints alongside the
existing /radio/album. TrackMenu now shows a radio submenu with QBZ
Radio (RadioPoolBuilder) and Qobuz Radio options when both are
available. Artist header radio button opens a dropdown to choose
between the two engines.
Backend: get_radio_artist(), get_radio_track() in client.rs,
v2_create_qobuz_artist_radio and v2_create_qobuz_track_radio commands.
Frontend: TrackMenu radio submenu with mutual exclusion against
share/download submenus, ArtistDetailView dropdown, LabelView support.
i18n keys added to all four locale files.
The /radio/album endpoint returns partial track objects without the
performer field, causing all tracks to show as "Unknown Artist".
Extract track IDs from the response and fetch each via get_track()
to get complete data, matching the pattern used by artist/track radio.
Add /radio/album Qobuz API endpoint support:
- RadioResponse struct in models.rs with lenient deserialization
- get_radio_album() client method
- v2_create_album_radio command that calls the API directly (no
RadioPoolBuilder/RadioDb), filters blacklisted artists, populates
queue, and sets Radio playback context
- Register command in lib.rs invoke handler
Two fixes for session not persisting:
1. Changed v2_get_all_queue_tracks to return a struct instead of a
tuple — Tauri IPC serialization of tuples is unreliable.
2. Added proactive session saves instead of relying on async
beforeunload (which races with window close):
- Debounced full save on track change (2s after new track starts)
- Debounced full save on pause
- Reduced periodic save from 60s to 15s during playback
- Close handler remains as best-effort backup
The save was broken in two ways:
1. Only current_track + upcoming (capped at 20) were saved, dropping
all history tracks. Queue was reconstructed incomplete on restore.
2. Track metadata (hires, bit_depth, sample_rate, source, etc.) was
not being persisted, only the basic 6 fields.
Add v2_get_all_queue_tracks command that returns the full uncapped
track list with correct current_index. Use it in saveSessionBeforeClose
instead of the capped v2_get_queue_state. Map all metadata fields.
Add last_view, view_context_id, view_context_type to player_state
table via migration. Update PersistedSession struct, save/load/clear,
and v2_save_session_state with 3 new optional parameters.
- Deezer/Spotify/Tidal: fetch metadata directly from platform APIs
(bypasses Odesli ~300ms vs ~2-3s) with Odesli as fallback
- Smart Qobuz search with 3 progressive attempts:
1. Full "title artist" query
2. Cleaned title (strip parenthetical/bracket suffixes like Remastered)
3. Artist-only search with fuzzy title matching
- Fixes albums not found when platform titles differ (e.g. Iron Maiden
"Senjutsu (2021 Remaster)" vs Qobuz "Senjutsu")
The Odesli API can fail with connection errors on rapid successive
requests (connection pool staleness). Add a single retry with 500ms
delay to handle transient network failures gracefully.
Also improve song.link URL handling to hint track vs album detection.
Odesli's API returns entity IDs as integers for some platforms (e.g.
Bandcamp) while others return strings. The Entity.id field was typed as
String which caused serde deserialization to fail on numeric values.
Add a custom deserializer that accepts both strings and numbers.
Also show actual backend error in LinkResolverModal for easier debugging.
Odesli does NOT include Qobuz in its linksByPlatform response, so the
previous approach of extracting platforms["qobuz"] would always fail.
New strategy: use Odesli to identify the content (title/artist), then
search the Qobuz catalog via CoreBridge to find the matching album or
track. Falls back from track search to album search if needed.
Ctrl+L now accepts URLs from Spotify, Apple Music, Tidal, Deezer, and
song.link/album.link in addition to Qobuz. Non-Qobuz track/album URLs
are resolved to their Qobuz equivalent via the existing Odesli API
integration. Playlist URLs redirect to the Playlist Importer.
- Add MusicResource/MusicProvider enums and detect_music_resource()
- Add detect_resource() to each provider (spotify, apple, tidal, deezer)
- Add v2_resolve_music_link command with Odesli-based cross-resolution
- Upgrade LinkResolverModal with platform icon detection and playlist redirect
- Update i18n keys in all 4 locales (en, es, de, fr)
Library scan now automatically removes tracks whose files no longer
exist on disk, both for full scans and single-folder scans. Previously,
deleted files would remain in the library until the user manually ran
"Cleanup missing files" from Settings.
Fixes#93
DailyQ and WeeklyQ were incorrectly using 'home_list' context type.
LabelTop was defined in frontend but missing from Rust backend.
- Add LabelTop, DailyQ, WeeklyQ variants to Rust ContextType enum
- Add match arms in commands_v2.rs and display_info()
- Update frontend ContextType union and display strings
- DynamicSuggestView now uses 'daily_q' context type
- WeeklySuggestView now uses 'weekly_q' context type
Replace the flat album grid LabelView with a rich detail page featuring:
- Header with label image, name, description, and play button
- Popular Tracks section (5/10/all expand, same UX as ArtistDetailView)
- Releases horizontal scroll row with "See all" → LabelReleasesView
- Critics' Picks section from second releases container
- Playlists, Artists, and More Labels sections
Backend: add /label/page and /label/explore API endpoints with V2
commands (v2_get_label_page, v2_get_label_explore). Models use
serde_json::Value for flexible section parsing.
Old LabelView renamed to LabelReleasesView for the "See all" flow.
i18n keys added to all 4 locales (en, es, de, fr).
New V2 Tauri commands:
- v2_purchases_get_ids: fetch purchase IDs with optional type filter
- v2_purchases_get_by_type: fetch all albums or tracks with download status
- v2_dynamic_suggest: typed dynamic suggestions from Qobuz API
- v2_dynamic_suggest_raw: raw JSON variant for debugging
Registered all four in the Tauri invoke handler.
New API client methods:
- get_user_purchases_page_typed: fetch purchases filtered by type (album/track)
- get_user_purchases_ids_page_typed: fetch purchase IDs by type
- get_user_purchases_all_typed: paginated fetch for a single purchase type
- get_user_purchases_ids_page: generic purchase IDs page
- get_dynamic_suggest / get_dynamic_suggest_raw: Qobuz dynamic suggestions
New models: DynamicSuggestRequest/Response/Track, DynamicTrackToAnalyse,
PurchaseIdsResponse, lenient serde deserializers for robust API parsing.
ISRC/UPC paths now use a single Deezer API request instead of two
sequential requests (Deezer + Odesli). The song.link/album.link URL
is constructed directly from the Deezer ID (e.g. song.link/d/12345).
This cuts latency from ~2-5s (often timing out) to ~200ms.
The Odesli fallback is kept only for get_by_url (no ISRC/UPC).
Instead of passing a full Deezer URL to Odesli (which requires URL
resolution), use platform=deezer&type=song&id=XXX params directly.
This is faster since Odesli skips URL parsing.
Also fix album sharing (get_by_upc) — Odesli no longer accepts ?upc=
directly, same Deezer resolution approach applied.
Bump request timeout from 10s to 20s with 5s connect timeout.
Odesli no longer accepts ISRC as a direct query param or Spotify search
URLs. Replace with a two-step approach: resolve ISRC to a real Deezer
track URL via Deezer's free API, then pass that URL to Odesli.
Also fix error display in frontend — RuntimeError objects were shown as
[object Object] instead of extracting the details message.
Add experimental toggle to force PipeWire clock.force-quantum alongside
clock.force-rate for true bit-perfect playback. Quantum is matched to
sample rate (1024 for <=48kHz, 2048 for <=96kHz, etc). Both values
reset to 0 on stop and pause-suspend to avoid affecting other apps.
Requires DAC Passthrough to be enabled, PipeWire backend only.
If the Qobuz API returns an unexpected type for optional fields like
purchased_at, label, genre, quality metadata, etc., the field silently
defaults to None instead of crashing the entire purchases fetch.
cc
The Qobuz API returns purchased_at as a Unix timestamp integer, not a
string. Updated Rust structs, TypeScript types, and frontend date
formatting to handle numeric timestamps correctly.
Fixes#88
cc
The Tauri .desktop template was missing %u in the Exec line, preventing
the desktop environment from passing qobuzapp:// URIs as arguments when
launched from the browser. The tarball .desktop was also missing MimeType,
StartupWMClass, and StartupNotify fields.
Closes#44
cc
Add [[bin]] section to Cargo.toml so cargo produces target/release/qbz
directly. Update all packaging references (workflows, PKGBUILD, Flatpak
manifest, AUR desktop file) to use the new binary name. This fixes DEB
and RPM packages installing the binary as /usr/bin/qbz-nix instead of
/usr/bin/qbz.
Existing plex_cache_tracks rows had NULL album_key after the column
migration, causing plex_cache_get_album_tracks WHERE clause to return
zero results until a full re-sync. Now computes and fills album_key
for all NULL rows on DB open.
- Parse track_number (index) and disc_number (parentIndex) from Plex XML
- Add track_number, disc_number, album_key columns to plex_cache_tracks
- Fix plex_cache_get_album_tracks: add WHERE album_key clause (was
returning all tracks) and sort by disc/track number instead of title
- Update all SELECT/INSERT queries to include new columns
- Add inline track search to album detail view with search icon toggle
- Update PlexCachedTrack frontend interface and mapPlexTrack mapping
- Add i18n keys for search placeholder in all 4 locales
- downloaded_purchases table in library DB (permanent, per-user)
- V2 commands: mark_downloaded, remove_downloaded, get_downloaded_track_ids
- Frontend enriches purchase data with download status from backend
- Grid overlay for unavailable albums centered on artwork
- Quality filter expanded: All / Hi-Res / CD / Lossy (radio group)
- Album download status derived from track-level registry
- add Spectral Ribbon analyzer/render path and immersive panel
- tune ribbon density/interpolation and bars edge-bin rendering
- apply artist blacklist filtering to discover index home containers
- include supporting i18n/settings/player wiring changes
cx
Add a second LibraryDatabase connection to OfflineCacheState dedicated
to download post-processing writes. SQLite WAL mode natively handles
concurrent reads (UI via main connection) and writes (downloads via
this separate connection), eliminating app-level mutex contention that
caused progressive UI lag during bulk downloads.
Init after library.init_at() in both activate_session and
activate_offline_session. Teardown before main offline_cache teardown.
Add v2_cache_tracks_batch_for_offline command that inserts all tracks
in a single SQLite transaction and spawns download tasks in one IPC
call. Extract spawn_track_cache_download helper shared by both single
and batch commands.
Replace sequential for-of loops with batch calls in PlaylistDetailView
and +page.svelte (downloadAlbumById, handleDownloadAlbum,
handleReDownloadAlbum, reDownloadAlbumById). Single-track handlers
remain unchanged.
Reduces 75 IPC round-trips to 1 for bulk downloads.
Add a minimum 200ms interval between progress emissions per track,
on top of the existing 2% change filter. Always emits at 100%.
Limits events to max 5/sec/track, reducing IPC pressure during
concurrent downloads.
These two databases were missing PRAGMA journal_mode=WAL and
PRAGMA synchronous=NORMAL, which could cause UI lag from lock
contention during concurrent reads/writes.
Energy Bands + Oscilloscope:
- Constrain drawing area to 85% of screen height, vertically centered
Oscilloscope:
- Change from bottom-left info to centered artwork layout (like Bars)
Transient Pulse:
- Lower backend threshold 0.15 -> 0.04, cooldown 6 -> 3 frames
- Use raw (un-smoothed) bass-weighted RMS for sharper detection
- Add frontend-side supplementary beat detection from energy data
Album Reactive:
- Fix glow layer centering (was anchored to top-left, now inset: 0)
- Double scale effect (0.04 -> 0.08) and glow spread (40 -> 80)
- Fast attack / medium decay smoothing for visible reactivity
Backend:
- Emit viz:energy with 5-band RMS (sub-bass, bass, mids, presence, air)
- Emit viz:transient with attack detection (threshold + cooldown)
Frontend panels:
- EnergyBandsPanel: concentric glowing rings around centered artwork
- LissajousPanel: stereo X/Y phase plot with trail (watermark layout)
- TransientPulsePanel: expanding rings from center on beat detection
- AlbumReactivePanel: album art with breathing scale/glow from energy
All panels extract colors from album art, throttle at 30fps, and follow
the existing visualizer lifecycle (enable/disable backend on mount).
Focus tabs now: Coverflow, Static, Bars, Scope, Energy, X/Y, Pulse,
Reactive, Lyrics, Queue (keys 1-0).
Add viz:waveform event emission alongside existing viz:data FFT output.
De-interleaves L/R channels from the raw sample buffer and downsamples
to 256 points per channel (512 floats = 2048 bytes). Pre-allocates the
waveform buffer outside the hot loop to avoid per-frame allocations.