Replace the non-functional '...' button with a dropdown menu
containing Fullscreen, Maximize/Restore, Minimize, and Exit
Immersive options. Menu uses the same glassmorphism style as the
player bar for visual consistency.
On open, restore the preferred view from localStorage. When set to
'remember', save the current view on every view/tab switch via header
or keyboard. When set to a specific view, always open to that view.
Add dropdown in Settings > Appearance to choose which view immersive
mode opens with. Options: Remember last used (default), 6 focus views,
and 4 split-mode combinations. Persisted via localStorage.
Change Source<Item = i16> to Source<Item = f32> throughout.
Remove i16-to-f32 normalization since f32 samples are already in
[-1.0, 1.0] range. Update tests to use f32 sample data.
PlaybackEngine::append() now accepts Source<Item = f32>.
ALSA Direct thread uses f32 buffer and calls write_f32().
New write_f32() method in AlsaDirectStream converts f32 samples to
hardware format with full 24-bit precision:
- S24_3LE: f32 -> 24-bit packed 3 bytes (SMSL/Topping DACs)
- S32LE: f32 -> i32 full range
- S24LE: f32 -> 24-bit in i32 container
- S16LE: f32 -> i16
- FloatLE: direct write
Old write(&[i16]) kept for safety during migration.
Update AudioSpecs, decode_with_symphonia(), decode_with_fallback(),
wrap_source closure, and all Source boxing casts to use f32 samples.
This ensures 24-bit audio data flows through the player without
truncation to 16-bit.
Change SampleBuffer<i16> to SampleBuffer<f32> in the vendored rodio
decoder. This preserves 24-bit audio data (f32 has 24 bits of
significand) instead of truncating to 16-bit at decode time.
Affects: symphonia.rs (SampleBuffer, Iterator::Item)
mod.rs (Decoder, LoopedDecoder, DecoderImpl Iterator::Item)
Subscribe to favoritesStore in ArtistDetailView to display filled
heart icon when a track is already favorited. The button now toggles
(add/remove) instead of only adding, with proper loading state.
When user presses STOP then PLAY, playback now resumes the current
queue track instead of doing nothing. Added onResumeFromStop callback
in playerStore that queries the backend queue state and re-plays the
current track. The audio cache ensures instant playback within the
same session.
When activeView is 'album' but selectedAlbum is null, show an error
message with a back button instead of a blank page. Also add detailed
console logging in handleAlbumClick to help diagnose the root cause
for users who can reproduce.
Lyrics sidebar was staying open behind the immersive player, causing
duplicate CPU usage for synced lyrics rendering. Now hideLyricsSidebar
is called alongside closeAllMenus when opening fullscreen or focus mode.
Mute now saves the current volume before setting to 0, and unmute
restores it. Pre-mute volume persists in localStorage across sessions.
Centralized toggleMute in playerStore so all UI components (NowPlayingBar,
immersive VolumeSlider) behave consistently. Also moved session volume
persistence into setVolume to avoid duplication.
Dropdowns and popups with z-index 10000 were appearing over the
immersive player (z-index 100). Raise immersive to 10001 and call
closeAll() on the floating menu store when entering immersive mode.
The device capability detection was unreliable, causing incorrect
downgrades to 16b/44.1kHz on some PipeWire+DAC setups. Disconnect
the quality limiting logic in play_track and prefetch_track, hide
the toggle from settings UI, and default to off. Code preserved
for future fix.
1. Artist name normalization fallback:
- If "Mikael Åkerfeldt" finds no results, also try "Mikael Akerfeldt"
- Helps with artists whose Qobuz name lacks accents
2. Increased track search limits:
- Playlist artists: search up to 30 tracks (was 9)
- API search: fetch 100-500 results to find enough matches
- Needed because search results mix artist's tracks with appearances
This should fix:
- Missing Opeth tracks (all 9 found were already in playlist)
- Mikael Åkerfeldt not found (Qobuz has "Mikael Akerfeldt")
Ensures genre names are consistent (English) for all users regardless
of their Qobuz registration country. This affects the genre filter
in Home and Favorites views.
Qobuz returns localized genre names based on user's registered address,
not app language. This caused genre filtering to fail when user sees
"América Latina" instead of "Latin".
Solution: Pass lang=en explicitly when fetching artist albums for genre
verification. This ensures consistent English genre names regardless of
user's registration country.
Also simplified the incompatible genres list since we only need English now.
1. Add localized genre names to blocklist:
- Spanish: "América latina", "música latina", "infantil", etc.
- French: "variété française", "comptines", etc.
- German: "Kindermusik", etc.
2. Upgrade step 4a logging to INFO level to debug missing Opeth tracks
- Shows which playlist artists are being searched
- Shows how many tracks found vs skipped (already in playlist)
Pass client reference to search_artist_tracks_by_qobuz_id instead of
acquiring a new lock, which caused deadlock when step 4c already held
the qobuz_client lock.
Two improvements:
1. Accent normalization in artist validation
- Now "Mikael Åkerfeldt" matches "Mikael Akerfeldt" in Qobuz
- Uses existing normalize_name() function for comparison
2. Qobuz similar artists as fallback (step 4c)
- When pool has <20 tracks after MusicBrainz relationships
- Fetches similar artists from Qobuz API for each playlist artist
- These artists are guaranteed to exist in Qobuz
- Still applies genre compatibility filter
This ensures playlists with few MusicBrainz relationships (like solo
artists) still get meaningful suggestions from Qobuz's own similarity
data.
Add more genres and keywords to filter:
- Electronic/Dance: trance, techno, house, EDM, dubstep, etc.
- Spoken word: audiobook, hörbuch, podcast, meditation, ASMR
- Country: country, bluegrass, americana
- New age: healing, spa, yoga, mindfulness
- Additional title keywords: "gelesen von", "read by", "trance mix"
This prevents suggestions like:
- Tony Parsons (trance DJ) in Iron Maiden playlist
- Tony Parsons (German audiobook narrator) in metal playlist
When validating if an artist exists in Qobuz, now also checks their
album genres against a blocklist. This prevents false matches like:
- "Martin Lopez" (bachata singer) matching when we want the drummer
- "Martin Mendez" (merengue) matching when we want the bassist
Incompatible genres: bachata, merengue, reggaeton, salsa, cumbia,
k-pop, j-pop, gospel, children's music, etc.
The validation fetches up to 5 albums and checks both genre metadata
and album titles for incompatible keywords.
Add pre-validation step that checks if an artist has a dedicated Qobuz
page before searching for their tracks. This prevents false matches for:
- Session musicians who don't have their own catalog (e.g., Martin Lopez
the drummer returning tracks from unrelated Martin Lopez artists)
- Names that exist in MusicBrainz but not in Qobuz's artist database
The validation searches Qobuz artists for exact name match with
albums_count > 0. Also matches tracks by Qobuz artist ID when possible
(more precise than name matching).
Also fix outdated test assertions for config defaults.
- For 2-word names: require EXACT same words (no extra words allowed)
- 'Martin Lopez' vs 'Tomas Martin Lopez' → REJECT (different word count)
- 'George Harrison' vs 'Harrison, George' → ACCEPT (same words, different order)
- Add normalize_name() to handle accents (Martín -> Martin)
- Require ALL words of shorter name to match longer name
- Remove permissive 'contains' checks that allowed partial matches
- For 2-3 word names, require 66%+ match ratio
- Prevents 'Martín Méndez' from matching 'Tomas Martin Lopez'
- Increase pool sizes (INITIAL_POOL 18->30, EXPANDED_POOL 60->100, MAX_POOL 120->200)
- Add auto-refresh when available tracks fall below threshold (MIN_AVAILABLE_THRESHOLD=12)
- Add artist identity validation to filter incompatible genres (prevents merengue in metal playlists)
- Log when artists are skipped due to incompatible disambiguation
- Increase backend defaults (max_artists 20->30, tracks_per_artist 5->6, max_pool_size 100->150)
- Hide button only shows when content filtering is enabled
- Use custom blind-eye.svg icon
- Remove border for cleaner look
- Banner also conditional on filtering being enabled
cc
- Replaced prominent blacklist button with smaller tertiary button
- Button now opens dropdown with descriptive help text:
"Hide this artist - Don't show in searches, playlists, discover, etc."
- Updated terminology from "blacklist" to "hide/show"
- Updated banner and toast messages for consistency
- Uses EyeOff icon instead of Ban for softer visual
cc
- PlaylistModal: convert $derived with get(t) to regular function
- LocalLibraryTagEditorModal: rename 't' to 'track' in map to avoid
shadowing the t store
cc
vite-plugin-svelte CSS extraction fails when $t() is used inside $derived().
This causes "[postcss] Unknown word <script" errors that appear to be cache
issues but are actually a known Svelte 5 / vite-plugin-svelte incompatibility.
Files fixed:
- DownloadButton.svelte: use titleKey pattern with function resolver
- DACSetupWizard.svelte: use labelKey pattern resolved in WizardStepper
- RemoteControlSetupGuide.svelte: use labelKey pattern
- WizardStepper.svelte: support optional labelKey with $t() in template
Also added download.* translation keys to en.json and es.json.
cc
- Add backend endpoint for /discover/playlists API
- Add PlaylistTagFilter dropdown component for filtering by genre
- Add QobuzPlaylistCard with CSS blur background effect (color-thief style)
- Add HorizontalScrollRow header snippet for custom headers with scroll controls
- Add session cache store for caching API responses
- Add i18n keys for playlist filter and discover section
cc
Old database schemas created before locale support was added
caused "no such column: locale" errors when fetching artists.
The migration detects this and drops the old table so it gets
recreated with the correct schema.
Fixes Top Artists section not appearing in Home view.
Security improvements based on code review:
1. Credential Encryption (Critical)
- Replace XOR obfuscation with AES-256-GCM encryption
- Derive key from machine ID (tied to specific machine)
- Add migration support for legacy XOR format
- Automatic re-encryption on first load of legacy credentials
2. HTTPS Default (Medium)
- Change default `secure` setting to `true` for new installs
- Existing users keep their current setting
3. Configurable CORS Origins (High)
- Replace hardcoded domain whitelist with SQLite-backed storage
- Default origins: vicrodh.github.io, control.qbz.lol
- Add commands: get/add/remove origins, restore defaults
- Origins can be customized for third-party clients
- Server restarts automatically when origins change
New dependencies:
- aes-gcm: AES-256-GCM authenticated encryption
- sha2: SHA-256 for key derivation
- Move Remote Control section to end of Settings (after Updates)
- Make section collapsible, closed by default
- Keep Experimental badge visible when collapsed
- Update nav sidebar order to reflect new position
- Remove unused .section-title-row CSS selectors
Add /api/events endpoint using Server-Sent Events (SSE) as a more
efficient alternative to WebSocket for pushing playback state updates
to PWA clients. SSE provides automatic reconnection and is simpler
to implement on the client side.
- Add tokio-stream dependency with sync feature for BroadcastStream
- Implement sse_handler with 15s keep-alive ping
- Keep /api/ws for backwards compatibility
- Add shuffle/repeat fields to PlaybackEvent struct
- Emit queue:state event when shuffle/repeat changes via API
- Add queue event listener in desktop frontend
- Include shuffle/repeat in WebSocket broadcasts
- PWA updates shuffle/repeat from WebSocket messages
This enables bidirectional sync: changes from PWA update desktop UI,
and changes from desktop update PWA via WebSocket.
Update the local volume state when receiving playback events from the
backend. This ensures the volume slider reflects changes made via
remote control or other external sources.
The playback event listener was only started when currentTrack existed,
creating a chicken-and-egg problem: external playback (from PWA) couldn't
set currentTrack because the listener wasn't active to receive the event.
Now the listener is always active, allowing remote control to update the
player state even when nothing is playing locally.
Call notifyListeners() right after setting currentTrack when track
changes from remote control. Also update isPlaying and currentTime
from the event to ensure player UI reflects correct state.
Added debug logging for queue track mismatches.
When a track change is detected from an external source (e.g., PWA
remote control), also sync the full queue state. This fixes the issue
where desktop UI showed empty queue after PWA started playback.
Add emit_playback_update() to notify both desktop UI (Tauri event) and
PWA (WebSocket) when playback state changes from remote commands.
Previously, desktop UI would only update on the next polling cycle (up
to 5 seconds), making the remote control feel unresponsive.