Commit Graph
194 Commits
Author SHA1 Message Date
vicrodh 5b9f5a75c2 feat(ui): add explicit content badge across entire app
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.
2026-03-06 08:48:51 -06:00
vicrodh 5fae14d68d fix(audio): stop updating live gain during refinement passes
Only write to gain atomic on initial measurement. Refinements now
update the cache only, preventing audible volume changes within a
single track. Inter-track normalization still works via cached values.
2026-03-05 11:16:05 -06:00
vicrodh 87321a0ea6 fix(library): detect qobuz_purchase source on scan and clean stale downloads
insert_track() now checks downloaded_purchases table to set source.
get_downloaded_purchase_track_ids() verifies file existence and
auto-removes stale entries. Closes #115
2026-03-05 11:13:59 -06:00
vicrodh ec3ecb35b2 fix(flatpak): restore pipewire stream matching and tray icon access
cx
2026-03-01 20:18:17 -06:00
vicrodh 683f97a5d5 fix: deduplicate albums with quality subfolders in local library
The library scanner's album_root_dir() only knew to skip disc folders
(Disc 1, CD2, etc.) when resolving the album directory. Files stored
in quality subfolders like artist/album/FLAC 24-bit - 96 kHz/track.flac
caused each encoding variant to appear as a separate album card.

Add is_encoding_folder() to detect common audio format directory names
(FLAC, MP3, AAC, WAV, etc.) and make album_root_dir() skip past both
encoding and disc subdirectories, handling chained nesting too.

Fixes the workaround from #116.
2026-03-01 15:40:22 -06:00
vicrodh 8e104a3f3e fix(audio): replace deprecated CPAL name() usage with description()
cx
2026-03-01 10:44:54 -06:00
vicrodh c17c01b02b fix(security): remove unused qobuz user_id accessor
cx
2026-03-01 09:49:57 -06:00
vicrodh 133defbafa fix(security): redact sensitive identifiers and secrets from backend logs
cx
2026-03-01 09:24:42 -06:00
vicrodh 7086110569 feat: smart quality downgrade for unsupported sample rates
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.
2026-02-28 07:48:03 -06:00
vicrodh 3329d1d632 fix: suspend PipeWire before CPAL stream in exclusive mode
When transitioning from ALSA Direct to CPAL fallback (e.g. for rate
resampling), PipeWire auto-re-acquires the device after ALSA Direct
releases it. CPAL then can't open the device.

Suspend PipeWire sinks before creating CPAL stream in exclusive mode,
same pattern used in ALSA Direct DeviceBusy handler.
2026-02-28 07:48:03 -06:00
vicrodh b234828228 fix: pre-check hardware rates via /proc/asound before ALSA Direct
When ALSA Direct opens a PCM device and fails to set the rate, the
USB DAC doesn't release the device fast enough for CPAL to open it
afterwards. This caused "device busy" errors even with a delay.

New approach: read /proc/asound/cardN/stream0 to check supported
rates BEFORE opening the device. If the rate isn't supported, skip
ALSA Direct entirely and fall through to CPAL (which has rate
fallback logic). The device is never touched at the wrong rate.
2026-02-28 07:48:03 -06:00
vicrodh d667ace987 fix: add 100ms delay after ALSA Direct PCM release before CPAL fallback
When ALSA Direct opens a PCM device and fails to configure the rate,
the PCM handle is dropped but the kernel may not release the device
instantly. CPAL then fails with "Device busy" when trying to open it.

Add 100ms sleep after InvalidParams to let the kernel fully release
the device before CPAL attempts to open it. Also fix error messages
to show the effective (fallback) rate instead of the original rate.
2026-02-28 07:48:03 -06:00
vicrodh c99f0d342d fix: add sample rate fallback to ALSA backend CPAL path
When ALSA Direct can't handle a rate (e.g. 176.4kHz) and falls back
to CPAL, CPAL also fails because the hardware doesn't support it.

Port find_best_fallback_rate from PipeWire backend: queries supported
rates via CPAL, picks highest rate in same family (44.1k or 48k)
that's <= requested. 176.4kHz on a 96kHz-max DAC now falls back to
88.2kHz and rodio resamples, instead of failing entirely.
2026-02-28 07:48:03 -06:00
vicrodh 29343e8149 fix: fall back to CPAL when ALSA Direct doesn't support sample rate
When hardware doesn't natively support a sample rate (e.g. 176.4kHz),
return None from try_create_direct_stream instead of a hard error.
This lets the player fall through to CPAL/rodio which can resample.

Applied to both hw and plughw paths — InvalidParams on rate now
triggers graceful fallback instead of killing playback.
2026-02-28 07:48:03 -06:00
vicrodh fe3c5b0528 fix: suspend PipeWire sink before ALSA Direct to prevent DeviceBusy on first play
When switching from PipeWire to ALSA Direct, PipeWire holds the ALSA
device open causing EBUSY on the first snd_pcm_open. Now detects
DeviceBusy, suspends the PipeWire sink via pactl, waits 200ms for
release, then retries. Applied to both hw and plughw paths.
2026-02-28 07:48:03 -06:00
vicrodh cd4685820e fix: ALSA backend unavailable due to cpal 0.17 HostId::name() case change
cpal 0.15 returned "ALSA" from HostId::name(), cpal 0.17 returns "Alsa"
(now uses stringify! on the enum variant). Use eq_ignore_ascii_case()
for the comparison so it works regardless of cpal version.
2026-02-28 07:48:03 -06:00
vicrodh 7f6ac9bae6 fix: add sample rate fallback when DAC doesn't support requested rate
Query the DAC's supported sample rates from /proc/asound/cardN/stream0
(maps PipeWire sink to ALSA card via pactl). If the track's native rate
isn't supported (e.g., 176.4kHz), fall back to the nearest rate in the
same family (e.g., 88.2kHz). Rodio resamples from track rate to stream
rate automatically, so audio plays instead of producing silence.
2026-02-28 07:48:03 -06:00
vicrodh 5ef58f6121 fix: use with_buffer_size() after with_supported_config() to prevent underruns
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.
2026-02-28 07:48:03 -06:00
vicrodh b054f51410 fix: revert BufferSize::Fixed for PipeWire, use BufferSize::Default
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)
2026-02-28 07:48:03 -06:00
vicrodh 014becfb02 fix: disable PipeWire clock.force-quantum for rodio 0.22 compatibility
rodio 0.22's MixerDeviceSink has an internal mixer thread that cannot
synchronize with PipeWire's forced quantum scheduling. This caused
massive buffer underruns (600+) at sample rates >= 88.2kHz when Force
Bit Perfect was enabled.

clock.force-rate alone is sufficient for bit-perfect sample rate
switching. Logs confirm zero underruns at 88.2kHz and minimal underruns
at 192kHz (only during heavy I/O) when quantum is not forced.
2026-02-28 07:48:03 -06:00
vicrodh 3d8733181a chore: remove unused SampleRate imports
SampleRate is now a u32 type alias in cpal 0.17, so the import is no
longer needed anywhere.
2026-02-28 07:48:03 -06:00
vicrodh 6e8b8410c9 fix: set explicit 100ms buffer size to replace vendored cpal tuning
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.
2026-02-28 07:48:03 -06:00
vicrodh b5ec5de785 refactor: migrate all code to rodio 0.22 API
- 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
2026-02-28 07:48:03 -06:00
vicrodh faf8844584 chore: upgrade rodio 0.19 to 0.22, remove vendored rodio/cpal
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.
2026-02-28 07:48:03 -06:00
vicrodh 6a7c31220b feat: add Goody model to qbz-models for album booklets
Add Goody struct and goodies field to Album for Qobuz album
booklet/liner notes PDF support.
2026-02-28 07:48:02 -06:00
vicrodh ddedca1b5e chore: fix remaining Rust warnings
- Allow dead_code on TokenValidationResponse.code (deserialized but not read)
- Remove unused MusicProvider import in commands_v2.rs
- Restore #[allow(non_snake_case)] on v2_plex_cache_get_tracks
- Replace drop(tokens) with let _ = tokens (drop on reference is no-op)
2026-02-25 20:57:30 -06:00
vicrodh 56b4d467e3 fix(listenbrainz): fix error display, persistence, and build warning
- Remove unused ListenBrainzResponse struct (build warning)
- Fix error display showing [object Object] instead of actual message
- Persist credentials to legacy SQLite cache on connect/disconnect
  so they survive app restarts
2026-02-25 19:13:15 -06:00
vicrodh 4073b9fd35 feat: track download format in SQLite for per-quality status
Migrate downloaded_purchases to compound PK (track_id, format_id) so
the same track downloaded in MP3 and FLAC are tracked separately.
Album detail endpoint now returns downloaded_format_ids per track.
2026-02-24 15:07:58 -06:00
vicrodh 5dc83145bb feat(db): add custom_album_covers table and methods
Schema for user-uploaded custom album covers (keyed by album_id).
Database methods: set, get, remove, get_all.

cc
2026-02-24 09:58:27 -06:00
vicrodh 21358aa876 fix(oauth): use 'code' param for /oauth/callback API (Qobuz uses code_autorisation in redirect URL but 'code' in the API) 2026-02-23 15:24:40 -06:00
vicrodh 22060d1d35 fix(oauth): correct parameter names (code_autorisation, not code)
Qobuz redirects to play.qobuz.com/discover?code_autorisation=XXX
not ?code=XXX. Both the navigation intercept and the /oauth/callback
API call were using the wrong parameter name, so the code was never
captured and the exchange request was malformed.
2026-02-23 15:18:52 -06:00
vicrodh 9d6be623d4 feat(core): add set_session to QbzCore and login_with_session to CoreBridge for OAuth 2026-02-23 14:45:24 -06:00
vicrodh 5ed96c1d2d feat(qobuz): add private_key extraction and login_with_oauth_code to qbz-qobuz client 2026-02-23 14:45:20 -06:00
vicrodh 9db1e5ca6b perf(cache): increase L1 to 400MB and L2 to 800MB
L1 (memory): 300MB -> 400MB (+100MB)
L2 (disk):   500MB -> 800MB (+300MB)
2026-02-23 08:55:33 -06:00
vicrodh 7c6e07908f feat(backend): add bulk fetch for custom artist images
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.
2026-02-23 08:55:25 -06:00
vicrodh 633c755d3b fix: skip CoreBridge actions for echo SET_STATE commands
The server echoes every state report as a SET_STATE with only next_track
(playing_state=null, current_track=null). These echoes were triggering
align_corebridge_queue_cursor which uses the accumulated renderer state
current_track (from a different context, e.g. iOS). When the track
wasn't found in the local queue, it replaced the entire queue with a
single track, destroying the user's loaded queue.

Now echo SET_STATE commands are detected early and only update the
internal renderer state (for next_track tracking). CoreBridge actions
(align cursor, load track, play/pause) and state reports are skipped
entirely for these echoes.
2026-02-22 16:58:45 -06:00
vicrodh f6506cd4ea fix: break infinite feedback loop in renderer state reports
The server echoes every RNDR_SRVR_STATE_UPDATED as a SET_STATE command
with only next_track updated (playing_state=null, current_track=null).
send_renderer_reports was replying to these echo SET_STATE commands
with another state report, creating an infinite loop that generated
47k log lines (8.2MB) in seconds and completely blocked playback.

Fix: only send state reports for substantive SET_STATE commands that
carry playing_state or current_track changes. Echo commands (next_track
only) are silently consumed.

Also demote diagnostic logging from info to debug to reduce terminal
noise during normal operation.
2026-02-22 15:58:56 -06:00
vicrodh 40a2fa618e fix: omit queue_item_ids from renderer state reports to fix seekbar sync
The server rejects renderer state reports with "Current track not found
in queue nor autoplay" after QBZ-initiated queue loads, because it assigns
non-standard queue_item_ids (first track = track_id, rest = 1..N).

Omit current_queue_item_id and next_queue_item_id from all outbound
renderer reports. The server already knows the current track from
SET_STATE commands and adds the correct queue_item_id when forwarding
to controllers.

Also add info-level diagnostic logging to both report paths (periodic
frontend reports and command-response reports) and the protobuf encoder
for verifying wire content.
2026-02-22 15:18:35 -06:00
vicrodh 9444269fd6 fix(qconnect): fix seekbar position sync with remote controllers
Root cause: server pause/resume commands send None for position and
tracks, which the renderer state blindly overwrote. Then renderer
reports sent null position to the server, making controllers show the
track as finished.

Three-part fix:
1. renderer.rs: only overwrite position/tracks when server provides
   a non-None value (retain last known values through pause/resume)
2. app.rs + qconnect_service.rs: update renderer state position from
   frontend periodic reports so the internal state always has the
   real playback position
3. +page.svelte: increase position report frequency from 5s to 2s
   for smoother seekbar updates on controllers
2026-02-22 12:57:50 -06:00
vicrodh 65f0f25203 fix(qconnect): fix queue error decode and add context_uuid to queue commands
ErrorMessage.code was defined as int32 but server sends string codes
(e.g. 'ERROR_QUEUE_INSERT_TRACKS'). Changed to string type and added
logging for queue error responses.

Added context_uuid (random UUID v4) to queue insert and add commands -
the server likely requires this to associate tracks with a context.
2026-02-22 02:45:04 -06:00
vicrodh a9121ef427 fix(qconnect): correct track_ids wire type to fixed32 packed in outbound queue commands
track_ids in QueueLoadTracks, QueueInsertTracks, QueueAddTracks, and
AutoplayLoadTracks was int32 packed=false but the server expects
fixed32 (packed by default). This silently broke all outbound queue
manipulation commands.
2026-02-21 23:12:09 -06:00
vicrodh 9e8dfd91e5 fix(qconnect): correct wire types for track_id (fixed32) and shuffle_seed (fixed32)
Server sends track_id as fixed32 (wire type 5) and shuffle_seed as
fixed32, but our structs declared them as int32 (wire type 0/varint),
causing decode failures on QueueState, TracksLoaded, and
RendererSetState messages.
2026-02-21 17:29:47 -06:00
vicrodh 4c7be7597d fix(qconnect): remove wrong inner envelope, decode QConnectBatch directly
Root cause: inner_payload from CloudPayload.payload IS a QConnectBatch
(QConnectMessages) with repeated QConnectMessage entries in field 3.

The QCloudInnerEnvelope struct incorrectly treated field 3 as opaque
bytes, keeping only the last repeated entry and losing all messages.
Additionally, QConnectMessages.messages_time was declared as uint64
(wire type 0/varint) but the server sends fixed64 (wire type 1),
causing a wire type mismatch that would fail decoding.

Changes:
- Remove QCloudInnerEnvelope struct entirely
- Pass inner_payload directly to decode_queue_server_events/decode_renderer_server_commands
- Fix messages_time from uint64 to fixed64 to match server wire format

Verified against qonductor proto definitions (QConnectBatch) used in
working POC v1 and v2 implementations.
2026-02-21 17:15:34 -06:00
vicrodh 975fac55f7 diag(qconnect): add decode-layer logging to find envelope unwrap issue
Log whether QCloudInnerEnvelope decode succeeds/fails, message byte
count, and whether queue/renderer decoders succeed or error.
2026-02-21 16:27:01 -06:00
vicrodh 309da97b8e fix(qconnect): unwrap inner QCloud envelope and defer renderer join
Root cause: server wraps QConnectMessage inside an inner envelope with
fields 1 (correlation_id), 2 (sequence), 3 (actual message bytes).
We were passing the envelope directly to QConnectMessage::decode(),
which silently skipped the unknown wrapper fields and found nothing.

Also restructure bootstrap: controller join first (works without
session_uuid), renderer join deferred until SESSION_STATE (type 81)
arrives with the session_uuid the server requires.
2026-02-21 16:12:37 -06:00
vicrodh 8a9e08ad91 feat(qconnect): add session management protobuf types and outbound reporting
Add missing SRVR_CTRL session management message types (81-87, 97-101)
that were completely absent from the protobuf definitions. Without these,
prost silently discarded all session topology messages from the server.

- Add 12 new proto structs for session/renderer/volume/quality events
- Add decoder branches and map functions for all new message types
- Add SessionManagementEvent variant to bypass queue reducer
- Add v2_qconnect_report_playback_state and v2_qconnect_report_volume
- Add comprehensive logging for inbound message diagnostics
2026-02-21 15:35:55 -06:00
vicrodh b160773e50 fix(qconnect): add device capabilities, initial state, and channel subscriptions
- Add DeviceCapabilitiesMessage to protobuf (tag 7 on DeviceInfo) with
  volume_remote_control=ALLOWED, min/max audio quality
- Extend JoinSessionMessage with reason, initial_state, is_active fields
  matching Android renderer proto (tags 3-5)
- Bootstrap sends renderer JoinSession with is_active=true and initial
  RendererState (STOPPED) so other devices see QBZ immediately
- Send initial StateUpdated, VolumeChanged, MaxAudioQualityChanged
  reports after join for full renderer presence
- Default subscribe channels to 0x01,0x02,0x03 instead of empty
- Request queue state from server after bootstrap

cc
2026-02-21 15:11:25 -06:00
vicrodh 9a0bdc33cb fix(qconnect): register renderer role during connect bootstrap
cx
2026-02-21 14:22:09 -06:00
vicrodh a85f7ca324 feat(qconnect-tauri): add admission middleware commands and blocked events
cx
2026-02-21 13:38:17 -06:00
vicrodh 4152d740c9 feat(qconnect-tauri): add controller command API and snapshots
cx
2026-02-21 13:10:42 -06:00