- Add DAC short name to pulse config filename for multi-DAC support
- Add getCreatedConfigPaths() for dynamic file path generation
- Show configured DAC name/description in done step
- Add configuredFor translation to all locales (EN/ES/FR/DE)
Step 5 (PipeWire config):
- Add sample rate checkboxes (44.1, 48, 88.2, 96, 176.4, 192 kHz)
- Pre-fill from DAC query if available, otherwise empty for manual selection
- Show "detected" badge for rates found by query
- Dynamic command updates based on selection
Step 7 (WirePlumber config):
- Use DAC short name in filename: 99-qbz-dac-{name}.conf
- Extract name from node.name or description
- Include DAC description in config file comment
- Use selected sample rates instead of hardcoded values
This enables proper multi-DAC separation - each DAC gets its own config file.
- Welcome step: checkbox to confirm understanding of system changes
- Backup step: checkbox to confirm backup was created
- Start/Next buttons disabled until checkboxes are checked
- Add translations for checkboxes (EN/ES/FR/DE)
- Clear default input value (use empty string)
- Add ALSA hardware capabilities query via:
- /proc/asound/cardX/stream0 for USB devices
- aplay --dump-hw-params for detailed hw params
- Parse rate ranges [min max] and expand to common rates
- Now shows actual DAC capabilities, not just current config
- Remove redundant subtitle text from DAC wizard welcome (all languages)
- Fix collapsed sidebar settings menu to expand upward instead of down
- Menu now opens to the right and grows upward, avoiding player overlap
- Add complete French (FR) translations for dacWizard section
- Add complete German (DE) translations for dacWizard section
- Add qobuz-player (TUI) to bit-perfect app selector in all 4 languages
- Replace single settings button with dropdown menu in UserCard
- Menu opens upward in expanded mode, to the right in collapsed mode
- Use openUrl from Tauri plugin for external links (bug report)
- Make dropdown compact (similar to context menu) and wider
- Pass onKeybindingsClick prop through Sidebar to +page.svelte
- Support collapsed prop in UserCard for sidebar collapsed mode
- Create KeyboardShortcutsModal.svelte with categorized shortcuts display
- Add ? shortcut to open modal (ui.showShortcuts action)
- Integrate modal in +page.svelte with Settings navigation
- Use i18n for all UI text
- Add animations and accessibility attributes
$state and $derived at module level cause "Cannot access uninitialized
variable" error during SSR. Changed to lazy initialization with getter
functions.
Keep only start/result summary at INFO for user issue reports.
Move all intermediate steps (MBID resolution, config, engine steps) to DEBUG.
Also moved to debug:
- playlist_get_settings, playlist_get_stats
- playlist_get_local_tracks_with_position
- All [SuggestionsEngine] step logs
Convert hw:X,0 to front:CARD=name,DEV=0 format when saving device selection.
This makes device IDs stable across reboots and USB reconnections since they
use the card name (e.g., C20) instead of the card number which can change.
- Add normalize_device_id_to_stable() to convert hw:X,0 to front:CARD=name,DEV=0
- Add resolve_stable_to_current_hw() for reverse lookup when needed
- Apply normalization automatically in set_audio_output_device command
- Export functions from audio module for use across codebase
- Clear stale device IDs from DB when they don't exist in enumeration
- Add warning when device lookup fails in handleBackendDeviceChange
- Add logging to set_audio_output_device command for debugging
ALSA card numbers (hw:X,0) can change between boots when USB devices
are connected/disconnected. This fix prevents using invalid device IDs
that cause playback failures.
cc
- Add fr.json with complete French translations
- Add de.json with complete German translations
- Register new locales in i18n/index.ts
- Add missing keys (playlist, metadata, genreFilter sections)
- All 869 translation keys present in both files
- Translated modal title and form labels
- Translated search buttons and results text
- Translated table headers (track, title, disc)
- Translated persistence options (sidecar vs direct)
- Translated progress states (writing tags, saving)
- Added placeholder translations for year and genre examples
Using $t() store subscriptions inside $derived() was causing
vite-plugin-svelte CSS extraction to fail, resulting in PostCSS
receiving the entire file content instead of just the style block.
Changed networkFsOptions to use labelKey pattern with $t() called
directly in the template instead.
- Update play menu items (Play now, Play next, Add to queue)
- Update navigation items (Go to artist, Go to album)
- Translate View more buttons
- Add noPlaylistsFor translation key
cc
- Update Last.fm API key and shared secret labels
- Update Last.fm authorize button text
- Update ListenBrainz labels and token hint
- Add translations for connecting/opening states
- Add apiKey, sharedSecret, userToken translation keys
cc
- Update Dropdown.svelte with search placeholder translation
- Update DeviceDropdown.svelte with searchDevices placeholder
- Update GenreFilterPopup.svelte with searchGenres placeholder
- Update PlaylistModal.svelte with name and description labels
- Add playlist.name and playlist.descriptionOptional keys
cc
- Add translations for loading/error states
- Translate empty states for albums, artists, tracks views
- Translate header titles and subtitle
- Update button titles (Scan, Settings, Play All, Shuffle)
- Translate offline mode notice
- Translate filter panel labels
- Add editAlbumMetadataTitle key for tooltips
cc
- Add t import and use translations for all empty state messages
- Translate loading states (favorites, playlists, albums)
- Translate section headers (Discography, EPs & Singles, Live Albums)
- Translate button titles (Play All, Shuffle, More, Search, Clear)
- Add hint translations for liking items to favorites
- Update getSortLabel to use sort.* translation keys
- Update getTabLabel to use favorites.tabLabels.* keys
cc
Document i18n requirements for contributors:
- No hardcoded strings policy
- svelte-i18n interpolation format ({ values: {} })
- Translation key naming conventions
- Checklist for PRs with UI text
svelte-i18n requires { values: { key: value } } format for variable
interpolation, not { key: value } directly. Fixed across all toast
and greeting calls.
- Update LocalLibraryTagEditorModal to use toast translations
- Update PlaylistImportModal to use toast translations
- Update PlaylistModal to use toast translations
- Update LocalLibraryView to use toast translations
- Update SettingsView to use toast translations for error messages
- Add time-based greeting translations (goodMorning, goodAfternoon,
goodEvening, goodNight) to homeSettings section
- Update homeSettingsStore to export getGreetingInfo for i18n support
- Update HomeView.svelte to use translated greetings
- Add comprehensive toast translations (~60 keys) for all user-facing
notifications including queue, playback, offline, favorites, etc.
- Update +page.svelte to use $t() for all showToast calls
- Complete Spanish translations for all new keys
Add second level of protection by filtering blacklisted artists
in Rust queue commands: set_queue, add_to_queue, add_to_queue_next,
and add_tracks_to_queue. This ensures blacklisted tracks are never
added to the queue regardless of frontend implementation.
- TrackRow: Add isBlacklisted prop with grayout styling and ban icon
- VirtualizedTrackList: Check blacklist status for each track
- PlaylistDetailView: Gray out and restrict actions for blacklisted tracks
- AlbumDetailView: Apply album-level blacklist check to all tracks
- HomeView: Gray out blacklisted tracks in Continue Listening section
- ArtistDetailView: Banner shown when viewing blacklisted artist page
Blacklisted tracks show reduced opacity, ban icon instead of track
number, and restricted menu actions (navigation only, no playback).
- Show success/error toasts when adding/removing artists
- Show feedback when enabling/disabling blacklist
- Show confirmation toast when clearing all artists
- Replace direct navigateTo import with onBlacklistManagerClick callback
- Pass navigation callback from +page.svelte to SettingsView
- Follows same pattern as other view navigation (playlist-manager, etc)
- Create BlacklistManagerView component with list of blocked artists
- Add enable/disable toggle, search filter, and clear all function
- Integrate navigation to blacklist manager from Settings
- Add Content Filtering section in Settings with artist count
- Add i18n translations for English and Spanish
- Wire up view routing in +page.svelte
- most_popular now iterates through all items to find first non-blacklisted
- Added album filtering to search_all by album.artist.id
- Added BlacklistState to search_albums with filtering
- Fixes edge case where most_popular was empty when first item was blacklisted
Phase 3 (minimal) - UI for testing:
- Add artistBlacklistStore.ts with O(1) local checks
- Initialize store on app startup
- Add blacklist toggle button to ArtistDetailView
- Button shows red when artist is blacklisted
cc
Phase 2 (part 1) - Read-only integration:
- Filter blacklisted artists from search_tracks, search_artists, search_all
- Filter blacklisted artists from get_similar_artists
- Filter most_popular item if blacklisted
- Add blacklist filtering to all radio commands (artist/track/infinite/refill)
- Use output-layer filtering for radio (engine internals unchanged)
cc
Phase 1 of artist blacklist implementation:
- Add artist_blacklist module with HashSet-based service for O(1) lookups
- Add SQLite persistence with WAL mode
- Add global enable/disable feature flag
- Add Tauri commands for CRUD operations
- Register BlacklistState in app state
cc
Add semaphore to limit concurrent prefetch operations to 1 at a time.
This helps prevent potential race conditions in native audio libraries
(CPAL/PipeWire/ALSA) that can cause malloc corruption.
Also:
- Reduce QOBUZ_PREFETCH_COUNT from 3 to 2
- Reduce PREFETCH_LOOKAHEAD from 15 to 10
- Add 50ms delay before cache insertion
Addresses #38 - random crashes during prefetch
Implement 9/10 check strategy:
- 9 out of 10 connectivity checks go to 1.1.1.1 (Cloudflare DNS)
- 1 out of 10 checks go to Qobuz to verify service availability
This prevents false offline detections when the app is making many
concurrent API calls (e.g., loading genre hierarchies), which could
trigger Qobuz rate limiting and cause timeout-based false positives.
Also reduced timeout from 15s to 10s and retry wait from 2s to 1s
since 1.1.1.1 responds much faster than Qobuz.
Only show expand carets in the advanced view for genres that actually
have children loaded, or where children haven't been loaded yet.
Applies to all levels (parent, child, grandchild).
- Add silent parameter to loadChildren to skip notification
- loadAllDescendants now loads children and grandchildren in parallel
- Only one notification after all loads complete (no triple reload)
- Lazy load children only when tree node is expanded (faster startup)
- Fix popup positioning to grow left when toggling advanced view
- Eagerly load all descendants when selecting a parent for filtering
- This ensures basic mode also filters by Genre > Children > Grandchildren
- Show loading spinner while fetching children
- Parent genres shown as headers with checkbox
- Children displayed in grid below each parent
- Clicking parent toggles all its children
- Partial selection shows indeterminate state on parent
- Popup expands 150px taller when in advanced view
- Default view shows only parent genres (less cluttered like Qobuz)
- When selecting a parent, filtering includes all its sub-genres
- Added "Show all sub-genres" toggle for granular control
- Store now tracks parent/child relationships for proper filtering
- getFilterGenreNames() returns selected + children for filtering
Fetch parent genres first, then fetch sub-genres for each parent in
parallel. Combine into a flat list sorted alphabetically. This fixes
the issue where albums with sub-genres (like Alternative & Indie,
Opera, Concertos) were filtered out even when all genres were selected.
Add FavoritePlaylistCard component with hover overlay containing:
- Heart button (remove from favorites)
- Play button (start playlist playback)
- More menu (play next, add to queue, share, remove)
Card click navigates to playlist page. Updates FavoritesView to use
the new component with all action handlers wired through +page.svelte.
- Add 'control' variant for Favorites style matching other dropdowns
- Add 'align' prop: 'left' (Home) or 'right' (Favorites)
- FavoritesView uses control variant with right alignment
- Control variant shows "Genre: X" with chevron icon
- Add GenreFilterButton to Favorites > Albums toolbar
- Filter albums by genre name (multi-select supported)
- Use genreFilterVersion for reactivity tracking
Note: Artists tab doesn't have genre filter since artist
objects don't contain genre info directly.
- Allow multiple genre selection (checkbox behavior)
- Add getSelectedGenreNames() for multiple name retrieval
- Merge results from multiple API calls for featured albums
- Clear button now closes the popup
- Button shows "N genres" when multiple selected
- Add getSelectedGenreName() to genre filter store
- Add filterAlbumsByGenre() helper in HomeView
- Apply genre filter to Recently Played and Favorite Albums
- Fetch 3x more items when filtering to ensure enough results
- Reduce card height from aspect-ratio to fixed 36px
- Use neutral bg-tertiary background instead of colored gradients
- Horizontal layout with name and check circle
- Selected state uses accent-primary
- Better spacing to prevent overlap
Use onMount instead of $effect to load genres once.
Add initialized flag to prevent onFilterChange from firing
during initial subscription setup, which was causing
loadHome -> re-render -> loadGenres -> notify -> loadHome loop.
Remove genre_id from base schema and let migration add it.
This ensures existing databases don't fail when trying to create
index on non-existent column during initial schema execution.
- Add GenreFilterButton to home header
- Pass genre_id to fetchFeaturedAlbums API calls
- Reload home content when genre filter changes
- Featured sections now filter by selected genre
- Add reco_backfill_genres command to populate genre_id for existing events
- Add reco_needs_genre_backfill command to check if backfill is needed
- Fetches album details from Qobuz API and updates events in batches
- Includes rate limiting to avoid API throttling
- Add genre_id column to reco_events table
- Add migration to add column to existing databases
- Update RecoEventInput and RecoEventRecord structs
- Update insert_event and get_events_since to handle genre_id
- Add get_album_ids_without_genre() for backfill
- Add update_genre_for_album() for backfill
- Add optional genre_id parameter to client method
- Add optional genre_id parameter to Tauri command
- Qobuz API filters featured albums by genre when provided
- Add playPlaylistById to set queue and play first track
- Add queuePlaylistNextById to add playlist tracks next
- Add queuePlaylistLaterById to add playlist tracks later
- Add copyPlaylistToLibraryById using subscribe_playlist
- Add sharePlaylistQobuzLinkById to copy link to clipboard
- Connect all handlers to SearchView component
- Repositioned toggle to appear right after Streaming Quality dropdown
- Updated description to clarify it overrides quality setting when device
doesn't support the selected sample rate
Frontend changes for quality limiting feature:
- Add limitQualityToDevice state and handler
- Add AudioSettings interface fields for limit_quality_to_device and device_max_sample_rate
- Update handleBackendDeviceChange to send max_sample_rate when device changes
- Update handleOutputDeviceChange to find max_sample_rate from backend devices
- Add Settings UI toggle for "Limit Quality to Device"
This completes the quality limiting feature that prevents requesting tracks
that exceed the device's sample rate capabilities, ensuring bit-perfect playback.
- Add limit_quality_to_device setting (enabled by default)
- Add device_max_sample_rate field to store selected device's max rate
- Implement limit_quality_for_device() to restrict quality based on device
- Apply quality limiting in play_track and prefetch_track commands
- Add set_audio_device_max_sample_rate Tauri command
This ensures bit-perfect playback by not requesting tracks that
exceed the device's sample rate capabilities. When enabled:
- Devices <= 48kHz: limited to Lossless (44.1kHz)
- Devices <= 96kHz: limited to HiRes (up to 96kHz)
- Devices > 96kHz: allow UltraHiRes (up to 192kHz)