Commit Graph
461 Commits
Author SHA1 Message Date
vicrodh f86bc826b8 test(radio): add spacing and long-run stability tests
cx
2026-01-20 15:07:32 -06:00
vicrodh baade9949c test(radio): add deterministic selection tests
cx
2026-01-20 15:07:09 -06:00
vicrodh b8d78a9003 feat(radio): expand pool with playlists and similar artists
cx
2026-01-20 15:06:39 -06:00
vicrodh 029e4a07df feat(radio): add pool builder and artist tracks endpoint
cx
2026-01-20 15:05:52 -06:00
vicrodh da1384fbcc feat(radio): add zero-weight selection fallback
cx
2026-01-20 15:05:02 -06:00
vicrodh eab57dc4cd feat(radio): add deterministic next-track selection
cx
2026-01-20 15:04:38 -06:00
vicrodh fab21cbf8e feat(radio): add pool/history counters
cx
2026-01-20 15:04:12 -06:00
vicrodh 334dc4f1b5 feat(radio): add radio DB schema and sessions
cx
2026-01-20 15:03:52 -06:00
vicrodh c749b7d932 chore: Update tauri-plugin-dialog to 2.6.0
Fixes version mismatch warning between Rust crate (2.5.0) and
NPM package (@tauri-apps/plugin-dialog 2.6.0)
2026-01-20 14:34:18 -06:00
vicrodh b0d70b390a Security: Remove email from credential logs and add live smoke test
credentials/mod.rs:
- Remove email from log messages (privacy/security best practice)
- Logs still indicate success without exposing PII

tests/qobuz_live_smoke.rs:
- Add ignored live smoke test for Qobuz API
- Requires explicit opt-in via QBZ_RUN_QOBUZ_LIVE_TESTS=1
- Uses local credentials from keyring (no hardcoded secrets)
- Basic test: login + get_favorites endpoint
- Safe for repository (no credentials exposed)
2026-01-20 14:07:52 -06:00
vicrodh c1d32b4181 Rewrite offline playlist system to use file paths instead of IDs
Problem: Pending playlists created offline used auto-increment track IDs,
which change on library re-scans, causing FOREIGN KEY errors and data loss.

Solution: Store file_path instead of IDs for local tracks. Paths are stable
identifiers that only change if files are moved/renamed.

Backend changes:
- Add local_track_paths TEXT column to pending_playlist_sync table
- Update PendingPlaylist struct to include local_track_paths field
- Modify create/add functions to accept paths instead of IDs
- Add get_track_by_path command to resolve paths to current IDs
- Keep local_track_ids for backward compatibility

Frontend changes:
- Update PendingPlaylist interface with localTrackPaths field
- Fetch file paths from track IDs before creating pending playlists
- Implement path-to-ID resolution in sync function
- Gracefully handle missing files (log warning and skip)
- Backward compatible with old ID-based pending playlists

Testing:
- Simulated playlist creation with 3 local tracks (IDs 197-199)
- Simulated re-scan by changing IDs to 1500-1502
- Verified path resolution finds tracks with new IDs correctly
- Verified missing files are handled gracefully

This makes the offline playlist system robust and production-ready.
2026-01-20 13:30:10 -06:00
vicrodh 59cf94e7ca Support adding tracks to pending playlists in offline mode
Backend:
- Add add_tracks_to_pending_playlist method to OfflineStore
- Reads current track lists, appends new tracks, updates DB
- Add corresponding Tauri command and register in lib.rs

Frontend:
- Detect pending playlists by negative ID in handleAddToPlaylist
- Call add_tracks_to_pending_playlist for pending playlists
- Properly separate Qobuz and local tracks before adding
- Fixes error trying to call playlist_add_local_track with negative ID
2026-01-20 10:11:07 -06:00
vicrodh ea03553718 Add local track support to pending playlist sync
- Add local_track_ids column to pending_playlist_sync table
- Update PendingPlaylist struct to include localTrackIds field
- Modify create_pending_playlist to accept and store local track IDs
- Update get_pending_playlists to read local track IDs
- Allows creating playlists with both Qobuz and local tracks in offline mode
2026-01-20 08:23:52 -06:00
vicrodh 2270d80139 Add source/qobuz_track_id to LocalTrack model and implement filtering for Artists/Tracks
- Added source and qobuz_track_id fields to LocalTrack model for proper filtering
- Updated all database row mapping functions to include new fields
- Implemented network folder and download filtering for library_get_artists command
- Implemented network folder and download filtering for library_search (Tracks) command
- Made tab counters (Albums, Artists, Tracks) reactive to filtered content
- Counters now update based on current filtered data instead of total stats

Fixes offline mode filtering for Artists and Tracks tabs. When offline or when
downloads are hidden, Artists/Tracks now properly filter out inaccessible content,
matching the behavior already implemented for Albums.
2026-01-20 01:26:27 -06:00
vicrodh 6387fd598e Fix download repair to handle scanner-damaged tracks
Problem: Repair was failing for 42 tracks because it only searched
by qobuz_track_id, which was lost when scanner replaced downloads.

Root cause: Scanner's INSERT OR REPLACE doesn't include qobuz_track_id
in its column list, so it gets set to NULL during replacement. Previous
repair logic couldn't find tracks without qobuz_track_id.

Solution: Multi-strategy repair approach:
1. Search by qobuz_track_id first (intact downloads)
2. If not found, search by file_path (scanner-damaged downloads)
3. When found by path, restore BOTH qobuz_track_id AND source

New database functions:
- repair_qobuz_download_by_path(): Restores both ID and source
- track_exists_by_path(): Check existence by file path

Updated backfill logic:
- Try match by ID (for intact downloads with lost source)
- Try match by path (for scanner-damaged downloads)
- Insert as new only if not found by either method

This should successfully repair all 42 damaged downloads by matching
them via file_path from download_cache.

Files changed:
- src-tauri/src/library/database.rs
- src-tauri/src/library/commands.rs
2026-01-20 01:05:32 -06:00
vicrodh ac77a31b06 Add repair functionality for damaged Qobuz downloads
Problem: Qobuz downloads that were already scanned lost their
source = 'qobuz_download' marker. The previous fix only prevents
future damage, but doesn't repair existing tracks.

Solution:
- Added repair_qobuz_download_source() to restore source marker
- Modified library_backfill_downloads to repair existing tracks:
  - When a track exists, try to repair its source marker
  - Report shows: added, repaired, and skipped counts
  - This fixes downloads that lost their marker during scans

User can now run the backfill command to:
1. Restore source markers for damaged downloads
2. Make "Show in Local Library" setting work correctly
3. Separate Qobuz downloads from scanned music again

Files changed:
- src-tauri/src/library/database.rs
- src-tauri/src/library/commands.rs
2026-01-20 00:49:57 -06:00
vicrodh 6073cb96c1 Fix Qobuz downloads behavior and network filtering in offline mode
Problem 1: "Show in Local Library" setting was being ignored
- Qobuz downloads appeared in library even when setting was disabled
- Root cause: Scanner was overwriting Qobuz downloads with INSERT OR REPLACE
- This lost the 'source = qobuz_download' marker needed for filtering

Problem 2: Network music visible in offline mode
- Music from network folders remained visible when offline
- User expected network content to be completely hidden offline
- Only local disk music should appear

Solution 1: Protect Qobuz downloads from scanner
- Added is_qobuz_download_by_path() to check if file is already a download
- Modified insert_track() to skip files with source = 'qobuz_download'
- Now downloads and scanned music coexist as separate "joins" in DB

Solution 2: Filter network folders when offline
- Added get_network_folder_paths() to get list of network folder paths
- Modified library_get_albums to accept excludeNetworkFolders parameter
- When offline, filter out albums whose tracks are in network folders
- Frontend passes excludeNetworkFolders: isOffline to backend
- Added reactive effect to reload library when offline state changes

This implements the "join between tables" approach - Qobuz downloads
don't interfere with scanned music, and network content is properly
hidden when offline without being deleted.

Files changed:
- src-tauri/src/library/database.rs
- src-tauri/src/library/commands.rs
- src/lib/components/views/LocalLibraryView.svelte
2026-01-20 00:42:47 -06:00
vicrodh b450accb56 Fix LocalLibrary infinite loading in offline mode
Problem: When app is in offline mode, LocalLibrary gets stuck in
infinite loading screen and becomes unusable.

Root cause: loadFolders() was calling checkFolderAccessibility()
for each network folder. The backend command library_check_folder_accessible
uses blocking std::fs::read_dir which can hang for 20-30 seconds per
unreachable network path.

Solution:
Frontend (LocalLibraryView.svelte):
- Skip network folder accessibility checks when offline
- Assume network folders are inaccessible in offline mode
- Only check accessibility when online

Backend (library/commands.rs):
- Add 2-second timeout to library_check_folder_accessible
- Use tokio::task::spawn_blocking to avoid blocking async runtime
- Log timeout warnings for unreachable network paths

This allows LocalLibrary to load instantly when offline, showing
only local disk content as intended.

Files changed:
- src/lib/components/views/LocalLibraryView.svelte
- src-tauri/src/library/commands.rs
2026-01-20 00:27:28 -06:00
vicrodh 0d147a41b0 revert: restore previous offline detection behavior
cx
2026-01-20 00:08:55 -06:00
vicrodh a43ec7b7fb feat(offline): harden connectivity checks
cx
2026-01-19 22:53:47 -06:00
vicrodh ebca7c0546 Add Download functionality to all Qobuz track context menus
- Created open_track_folder Rust command to open folder containing downloaded track
- Added frontend wrappers: openTrackFolder and reDownloadTrack in downloadState.ts
- Updated all Qobuz views to support Download submenu:
  - HomeView: Added track download props and handlers
  - AlbumDetailView: Added onTrackOpenFolder and onTrackReDownload
  - SearchView: Added track download actions to both TrackMenu usages
  - FavoritesView: Added download submenu to both track list variants
- Downloaded tracks now show submenu with 'Open containing folder' and 'Re-download' options
- Completes Phase 2 Task 2.1: Context Menu Homogenization (Download)
2026-01-19 21:15:06 -06:00
vicrodh ee8978819c Revert "Speed up ALAC playback by using streaming decoder"
This reverts commit 20eac2f578.
2026-01-19 20:45:50 -06:00
vicrodh 20eac2f578 Speed up ALAC playback by using streaming decoder
Removed special case that forced ALAC to use symphonia fallback which
decoded the entire file (~30MB) before playback, causing 6+ second delays.

Now ALAC uses rodio's Decoder first (supports ALAC via symphonia-all
feature) which streams instead of decoding everything upfront.

Fallback chain is now:
1. Rodio Decoder (streaming) - instant
2. MP4 fallback (streaming) - instant
3. Symphonia (full decode) - slow, last resort

This brings ALAC playback start time from ~6 seconds to instant,
matching FLAC and other formats.
2026-01-19 20:41:57 -06:00
vicrodh cdaf02bf3a Fix ALAC playback by defaulting to stereo when channel info missing
ALAC files (and some other formats) don't include channel information in
the initial codec params, causing playback to fail with error:
'No channel info in codec params'

Changed to use .unwrap_or(2) instead of .ok_or_else() to default to
stereo (2 channels) which is the most common case and matches the
behavior in metadata extraction.

Fixes playback of ALAC (.m4a) files in Local Library.
2026-01-19 20:38:38 -06:00
vicrodh dec6eab109 Add explicit f64 type annotations for all sample_rate reads
Fixed missing type annotations in row.get() calls for sample_rate.
Without explicit type annotation, Rust was inferring the wrong type
causing 'Invalid column type Real' errors when reading from database.

Added ::<_, f64> type annotation to all three locations:
- row_to_track (line 1168)
- get_playlist_local_tracks (line 1772)
- get_playlist_local_tracks_with_qobuz (line 1821)

This is the critical fix for the database migration error.
2026-01-19 20:23:33 -06:00
vicrodh 88d4bc8471 Fix sample_rate type mismatches for LocalAlbum and inserts
Changed LocalAlbum.sample_rate from u32 to f64 to match database REAL type.
Fixed database queries to read sample_rate as f64 instead of u32.
Removed incorrect i64 casts when inserting sample_rate values.

This fixes the 'Invalid column type Real' error that prevented the app
from loading after the database migration.
2026-01-19 20:14:09 -06:00
vicrodh 4080cfbfc8 Add cleanup of temp table in sample rate migration
Drop any existing local_tracks_new table before starting migration
to handle cases where a previous migration attempt failed mid-execution.

This prevents 'table already exists' errors when retrying the migration.
2026-01-19 20:07:32 -06:00
vicrodh ba8a0d3fd7 Fix sample rate migration to handle NULL file_size_bytes values
Use COALESCE in migration SELECT to safely handle NULL file_size_bytes
values that may exist in older database versions. This prevents the
'NOT NULL constraint failed' error when migrating from INTEGER to REAL
sample_rate type.

The migration now uses COALESCE(file_size_bytes, 0) to ensure all
rows have a valid file size value, defaulting to 0 for missing data.

Fixes critical migration failure that prevented app startup for users
with existing databases.
2026-01-19 19:59:01 -06:00
vicrodh 1e7475e308 Fix sample rate decimal precision for bit-perfect playback (CRITICAL)
Problem:
- sample_rate stored as INTEGER in DB and u32 in Rust
- Fractional rates (44.1, 88.2, 176.4 kHz) stored as 44100, 88200, 176400 Hz
- Integer division (44100 / 1000 = 44) lost decimals
- DAC badge showed "resampling 44 → 44.1 kHz" false positive
- Breaks bit-perfect guarantee - user requirement: "no es negociable"

Root Cause:
- DB schema: sample_rate INTEGER (44100 Hz)
- Frontend: Math.round(44100 / 1000) = 44 kHz (lost .1)
- Badge: Compared 44 kHz vs 44.1 kHz → false resampling warning

Solution:
1. Changed LocalTrack.sample_rate from u32 to f64 (models.rs)
2. Changed AudioProperties.sample_rate from u32 to f64 (models.rs)
3. Changed DB schema from INTEGER to REAL (database.rs)
4. Added migration to convert existing data (database.rs)
5. Updated metadata extraction to cast to f64 (metadata.rs)
6. Fixed frontend to use float division without rounding (+page.svelte)

Migration:
- Checks if sample_rate is INTEGER
- Creates temp table with REAL, copies data, drops old, renames
- Preserves all data and indexes
- Runs automatically on app start

Result:
- 44100 Hz stored as 44100.0
- Division: 44100.0 / 1000.0 = 44.1 kHz (exact)
- Badge: 44.1 kHz = 44.1 kHz → no resampling (correct)
- Bit-perfect guarantee restored

Files modified:
- src-tauri/src/library/models.rs - Changed sample_rate to f64 in structs
- src-tauri/src/library/metadata.rs - Cast sample_rate to f64
- src-tauri/src/library/database.rs - Changed schema to REAL + migration
- src/routes/+page.svelte - Fixed division to preserve decimals
2026-01-19 19:49:34 -06:00
vicrodh 61fa05da93 Fix critical shuffle queue bug, Local Library context, and sample rate crash
1. QUEUE SHUFFLE FIX (CRITICAL):
   - When shuffle enabled and set_queue called, starting track was placed randomly in shuffle_order
   - This caused queue UI to show only tracks AFTER that random position
   - Example: 11 tracks, start at index 5, but index 5 was at position 7 in shuffle → only 3 tracks visible
   - FIX: When shuffle enabled, swap starting track to position 0 in shuffle_order
   - Now all tracks appear in queue UI as expected

2. LOCAL LIBRARY CONTEXT FIX:
   - handleLocalTrackPlay was clearing context immediately after LocalLibraryView set it
   - FIX: Removed clearPlaybackContext() call - LocalLibraryView handles context correctly
   - Context icon should now display for Local Library playback

3. SAMPLE RATE CRASH FIX:
   - show_track_notification expected u32 but received f64 (44.1, 88.2, etc.)
   - Error: "invalid type: floating point 44.1, expected u32"
   - FIX: Changed sample_rate parameter from u32 to f64 in notification command and format_quality
   - Bonus: Format shows "44.1" for fractional rates, "48" for whole numbers

Files modified:
- src-tauri/src/queue/mod.rs - Fixed set_queue to move start_index to front of shuffle_order
- src/routes/+page.svelte - Removed clearPlaybackContext from handleLocalTrackPlay
- src-tauri/src/commands/notification.rs - Changed sample_rate to f64 with proper formatting
2026-01-19 19:13:07 -06:00
vicrodh ed7984a029 fix(context): fix playback context tracking bugs
Multiple critical fixes for context tracking:

1. Add 'local_library' to backend context type parser
   - Was rejecting 'local_library' causing "Invalid context type" error
   - Now properly recognized in set_playback_context command

2. Fix shuffle to not always start with first track
   - Removed logic that moved current track to front of shuffle
   - Now finds current track position in shuffled order instead
   - Shuffle is truly random, not biased to original order

3. Add missing await to all setPlaybackContext calls
   - PlaylistDetailView: await context creation
   - FavoritesView: make setFavoritesContext async + await calls
   - ArtistDetailView: await context creation
   - HomeView: await context creation
   - SearchView: await + use home_list type (search type doesn't exist)

   This fixes intermittent context tracking (race condition)

Fixes: shuffle starting with first song, Local Library context error,
intermittent playlist/favorites/artist/home/search context tracking

cc
2026-01-19 18:31:18 -06:00
vicrodh f2c3e8aedd feat(local-library): implement playback context tracking
Add proper context tracking for Local Library playback so the stack
icon shows correctly when playing from Local Library.

Changes:
- Add 'local_library' to ContextType enum (frontend and backend)
- Add 'Local Library' display label in context info
- Import setPlaybackContext in LocalLibraryView
- Call setPlaybackContext when playing tracks from Local Library
  - For album view: context is the album with album title
  - For tracks view: context is "Local Tracks"
- Track IDs and position are properly set for context navigation

Fixes issue where Local Library playback showed stale Qobuz context.

cc
2026-01-19 18:17:04 -06:00
vicrodh 0bbf6435b4 fix(queue): implement proper Fisher-Yates shuffle algorithm
Previous implementation had a critical bug in the shuffle algorithm
that used the same seed value for all iterations, resulting in
weak/predictable shuffling.

Changes:
- Add rand crate dependency for proper PRNG
- Fix Fisher-Yates to use unique random value per iteration
- Use StdRng with timestamp-based seed for reproducible randomness
- Each shuffle iteration now gets a new random number from the RNG

This fixes shuffle working inconsistently across different contexts
(albums, playlists, continuous play).

cc
2026-01-19 18:17:04 -06:00
vicrodh 7b596a25c2 Fix PipeWire detection and App ID migration in Flatpak
Two critical fixes for Flatpak sandbox environment:

1. PipeWire detection now uses pactl instead of pw-cli
   - pw-cli is not available in GNOME Flatpak runtime
   - pactl works for both PipeWire and PulseAudio (compatibility layer)
   - Fixes missing PipeWire backend in Settings

2. App ID migration now works in Flatpak sandboxes
   - Detects Flatpak environment and uses absolute paths
   - Accesses old sandbox (com.blitzkriegfc.qbz) via HOME directory
   - Added read-only permission to old App ID in manifest
   - Properly migrates config/data/cache from old to new sandbox
2026-01-19 16:10:31 -06:00
vicrodh a0467fb10a Add Flatpak detection and automatic data migration
Features:
- Flatpak runtime detection (/.flatpak-info check)
- Automatic migration of user data from old App ID to new ID
  - Config: ~/.config/com.blitzkriegfc.qbz → ~/.config/qbz
  - Data: ~/.local/share/com.blitzkriegfc.qbz → ~/.local/share/qbz
  - Cache: ~/.cache/com.blitzkriegfc.qbz → ~/.cache/qbz
- Migration runs on startup before any other initialization
- Old directories are preserved (manual cleanup required)
- Tauri commands for frontend integration:
  - is_running_in_flatpak()
  - get_flatpak_help_text()

Migration is non-destructive and idempotent.
2026-01-19 15:14:14 -06:00
vicrodh d2f61974d8 Rename App ID: com.blitzkriegfc.qbz → com.blitzfc.qbz
Changes:
- Renamed Flatpak manifest, desktop file, and AppStream metainfo
- Updated App ID in all metadata files
- Fixed MPRIS name: qbz_nix → qbz
- Added NAS support permissions: /mnt, /media, /run/media
- Updated CI workflow to use new filenames
- Updated tauri.conf.json bundle identifier

BREAKING CHANGE: Users will need to migrate data from old App ID directories
Data migration will be implemented in next commit
2026-01-19 15:12:21 -06:00
vicrodh e2afc25175 Merge feature/ux-improvements-180226: UX improvements and playback context
Major UX improvements:
- Playback context tracking from albums/playlists/artists/favorites/home/search
- Stack icon showing context origin with clickable navigation
- Autoplay behavior setting with persistence
- Context menu actions (Play track only, Play from here)
- Track row pause on hover across all views
- Volume indicators while dragging (all 4 players)
- Favorites customization (custom icon, background, sticky nav, search)
- Theme expansion (Nord, Dracula, Tokyo Night, Catppuccin, Rosé Pine, Breeze, Adwaita)
- Layout fix: NowPlayingBar moved outside app-body flex row

Database migrations (safe for existing users 1.1.5+):
- playback_preferences table (autoplay + show_context_icon)
- favorites_preferences table (custom_icon_path, preset, icon_background, tab_order)
- Favorites icon now stored in cache dir with auto-migration

Audio backend: ✅ UNTOUCHED (bit-perfect playback preserved)

Known gaps (to fix in next branch):
- Track focus on context navigation not implemented
- Volume indicator in FocusMode/MiniPlayer not confirmed tested

# Conflicts:
#	src/lib/components/AlbumCard.svelte
2026-01-19 15:04:12 -06:00
vicrodh b172b55bcf fix(favorites): migrate custom icon storage to cache
cx
2026-01-19 14:51:18 -06:00
vicrodh 6166b7c206 Reduce console spam: change hardware status log from info to debug
The 1s polling of get_hardware_audio_status was logging every second:
"Hardware audio status: 192000Hz, format: Some("S32_LE")"

This is useful for debugging but too noisy for normal usage.

Change:
- log::info! → log::debug! (line 52)

Result:
- Console stays clean during playback
- Log still available with RUST_LOG=debug for debugging
- Polling continues working, just silently
2026-01-19 13:20:49 -06:00
vicrodh 8947bcf9c3 Skip drain on track skip for instant track changes
This fixes the 2-5s delay when rapidly changing tracks. The drain() was
blocking for hundreds of milliseconds waiting for the PCM buffer to empty,
even when the user was skipping tracks.

Root cause (playback_engine.rs:136):
- drain() was called ALWAYS after playback loop ended
- drain() blocks until PCM buffer is completely empty (~125-500ms)
- When user skips tracks, we don't need elegant buffer drain
- This caused 'need to wait 2-5s' or 'double click next' behavior

Solution:
- Added 'natural_end' flag to track WHY playback stopped
- natural_end=true: Song finished normally → DO drain (elegant end)
- natural_end=false: User skipped/paused → SKIP drain (instant response)

Benefits:
- Track changes now instant (no 2-5s wait)
- Songs that finish naturally still drain properly (no clicks/pops)
- Maintains audio quality while improving responsiveness

Testing:
- Rapid track skipping should be instant
- Songs that play to end should have smooth transition
- No audio artifacts (clicks/pops) in either case
2026-01-19 11:40:19 -06:00
vicrodh 0a361712ce Increase stream initialization delay from 50ms to 150ms
This addresses the 'slow playback' issue on first large sample rate transition
(e.g., 88.2kHz → 44.1kHz). The hardware needs extra time to stabilize when
changing sample rates significantly, especially on cold start.

Root cause:
- First transition 88.2→44.1 kHz: song started slowly
- Subsequent identical transitions: clean
- Affects both ALSA Direct and PipeWire DAC Passthrough
- Hardware 'warm-up' time needed for large rate changes

Fix:
- Increased delay from 50ms to 150ms (still imperceptible to user)
- Gives DAC hardware more time to stabilize clock after rate change
- Extra time especially critical for downward transitions (88.2→44.1)

Testing needed:
- Verify first 88.2→44.1 transition is now clean
- Ensure 150ms delay doesn't create noticeable gap
- Check all other transitions still work
2026-01-19 11:39:01 -06:00
vicrodh 97c9473790 Fix DAC badge showing 'Default' instead of actual device
The badge was always showing 'Default' regardless of the selected device because
thread_state.set_current_device() was only being called in the legacy CPAL path,
not in the backend system path (PipeWire DAC, ALSA Direct).

Root cause:
- Lines 801-805: Backend system creates stream but never sets current_device
- Lines 847, 883: Only legacy CPAL path was setting device name from CPAL device.name()
- UI badge calls get_audio_output_status() which returns player.state.current_device()
- current_device was None/outdated when using backend system

Fix:
- After successful stream creation (line 897), read output_device from settings
- Set thread_state.current_device with the device name from settings
- Fallback to 'Default' string if output_device is None
- Now badge shows: 'hw:4,0', 'DacMagic Plus Analog Stereo', etc.

Testing:
- ALSA Direct: Should show 'hw:4,0' or configured device
- PipeWire DAC: Should show 'DacMagic Plus Analog Stereo' or selected sink
- System Default: Should show 'Default'
2026-01-19 11:29:11 -06:00
vicrodh 78c4898edf fix(favorites): persist custom icon and sync modal state
cx
2026-01-19 11:11:17 -06:00
vicrodh f88cee7674 Fix sample rate change sync gap (all backends)
This is an integral fix that resolves the multi-second gap when changing
sample rates, without breaking bit-perfect modes (DAC Passthrough, ALSA Direct).

Root causes identified:
1. Missing ALSA Direct format change detection - stream wasn't recreated
2. Unnecessary CPAL device enumeration before backend system - added latency
3. Decoder started before stream fully initialized - seekbar advanced without audio

Changes made:
1. Added ALSA Direct to format change detection (line 773-775)
   - Stream now recreates on sample rate change for ALSA Direct
   - Maintains bit-perfect playback like DAC Passthrough

2. Restructured device initialization to skip CPAL enumeration (lines 798-893)
   - Backend system (PipeWire DAC, ALSA Direct) checked FIRST
   - CPAL enumeration only used as fallback for legacy path
   - Eliminates "Device 'hw:4,0' not found" warning for ALSA Direct

3. Added 50ms delay after stream creation (line 904-906)
   - Ensures stream is fully initialized before decoder starts
   - Prevents seekbar advancing without audio output
   - Small enough not to be noticeable, large enough to prevent gap

Testing needed:
- ALSA Direct: 192→96→192→44.1→48→88.2 kHz transitions
- PipeWire + DAC Passthrough: Same sample rate transitions
- System Default: Verify gapless still works
- Verify bit-perfect modes still work (no resampling)
2026-01-19 09:10:08 -06:00
vicrodh 45e6df49d5 Add UI for ALSA hardware volume control
- Add alsa_hardware_volume to AudioSettings interface
- Add Tauri command set_audio_alsa_hardware_volume
- Add checkbox in Settings (visible only for ALSA Direct + Hw plugin)
- Checkbox disabled by default for maximum compatibility
- Clear tooltip explaining experimental nature and risks
2026-01-19 04:08:44 -06:00
vicrodh ce72addd90 Fix database migration: remove alsa_hardware_volume from INSERT
User's existing database doesn't have the column yet, so INSERT fails.
Migration adds column after INSERT runs. Solution: only insert old columns.
2026-01-19 04:01:29 -06:00
vicrodh 61688bd1ea Integrate hardware volume control in PlaybackEngine
- Add hardware_volume parameter to PlaybackEngine::new_alsa_direct()
- Update all three call sites in player.rs to pass alsa_hardware_volume setting
- Implement conditional volume control: hardware mixer or DAC/amplifier
- Add device_id() getter to AlsaDirectStream for mixer access
- Hardware volume disabled by default for maximum compatibility
2026-01-19 03:59:28 -06:00
vicrodh c87b41894d Implement ALSA hardware mixer volume control
Added set_hardware_volume() to AlsaDirectStream:
- Tries common mixer control names (Master, PCM, Speaker, Headphone, Digital)
- Returns error if DAC doesn't support mixer (doesn't break playback)
- Stores device_id in struct for mixer access

This enables bit-perfect volume control via DAC hardware when supported.
Falls back gracefully if not supported.
2026-01-19 03:53:41 -06:00
vicrodh 31798432cb Fix database migration for alsa_hardware_volume column
PROBLEM: App crashed on startup with existing databases:
'table audio_settings has no column named alsa_hardware_volume'

CAUSE: ALTER TABLE with 'NOT NULL' constraint fails on SQLite migrations.

FIX:
- Removed NOT NULL from ALTER TABLE statement
- Handle NULL values with unwrap_or(0) in get_settings()
- Defaults to false (disabled) for old databases

Now works with both new and existing databases.
2026-01-19 03:52:01 -06:00
vicrodh 7177f248c9 Add alsa_hardware_volume setting (disabled by default)
Preparation for hybrid volume control (B+C option):
- New field: alsa_hardware_volume: bool (default: false)
- Database schema updated with migration support
- Setter/getter methods added

When enabled, will attempt to control DAC volume via ALSA mixer.
When disabled (default), in-app volume control is disabled for maximum
compatibility (user controls volume on DAC/amplifier).
2026-01-19 03:50:02 -06:00