Commit Graph
215 Commits
Author SHA1 Message Date
vicrodh a67e3c9217 Merge PR #391 (Vudgekek): macOS CoreAudio Exclusive Mode
Adds CoreAudio Hog Mode integration on macOS so Exclusive Mode actually
takes the device exclusively (previously the macOS path used the
Linux-shaped logic without any kernel-level lock). Pins the CoreAudio
device id through Hog Mode acquisition, sample-rate switching, and
stream creation so System Default doesn't drift mid-session. Linux
behaviour is unchanged.

Review asks (all four resolved by Vudgekek + afonsojramos):

- set_coreaudio_hardware_volume falls back to software volume on Err
  rather than reporting success, so knob-only DACs keep a working
  volume slider via engine.set_volume().
- CoreAudioExclusiveGuard snapshots the device's hardware volume on
  acquire and restores it before releasing Hog Mode, so leaving
  Exclusive Mode returns the DAC to the user's pre-Exclusive level.
  Guard is constructed before the FFI call so partial-acquire failures
  still trigger Drop and release.
- Linux unused-variable warnings on stream_opt and exclusive_guard
  cleaned up via cfg_attr(allow(unused_variables)) on the macOS-only
  guard plumbing. cargo check is clean.
- Silent shared-mode regression on macOS when backend init fails:
  init_device now sets_current_device(None) and returns None on
  exclusive_mode==true so the user sees an error instead of
  unknowingly playing in shared mode.

Bonus: 5f10b4ba adds real CoreAudio device-rate detection for macOS
Shared Mode, +90 lines in backend.rs.

Conflict resolution: single textual collision in
crates/qbz-audio/src/lib.rs (both branches added a pub use line).
Resolved by keeping both, alphabetical order — coreaudio_direct
re-export from pr-391, device_reservation re-export from pre-release.
All other auto-merges verified line-by-line: AlsaDirectStream's
DeviceReservation::acquire flow (alsa_direct.rs:60-97), the macOS
exclusive_mode guard (player/mod.rs:1077-1085 in qbz-player and the
mirror in src-tauri:1024), and CoreAudioExclusiveGuard's volume
snapshot/restore (coreaudio_direct.rs:351,369,373,388) all coexist
correctly because each branch touched different functions.

cargo check clean across all four crates. 9 device_reservation +
3 reserve_dac + 214 src-tauri unit tests pass.

cc
2026-05-07 19:09:37 -06:00
vicrodh 4bc4213b97 feat(qbz-audio): add device-reservation module skeleton with parsers
Adds the new module that will host the org.freedesktop.ReserveDevice1
D-Bus client. This commit only ships the public surface, the parser
helpers (hw:N -> Audio<N> bus name, hw:CARD=name resolution) and unit
tests for them. acquire() currently returns a degraded guard; D-Bus
implementation lands in the next commit.

cc
2026-05-07 08:57:31 -06:00
Vudgekek e0151af6d9 feat(audio): add CoreAudio exclusive mode 2026-05-06 23:26:15 -05:00
vicrodh 54074a5580 feat(library): add metadata-grouped album queries
New album_grouping module exports the SQL CASE-WHEN expression for the
metadata group key shared by both new queries. The expression has three
priorities: (1) usable album tag => album|artist key, (2) fallback to
existing folder-based album_group_key, (3) orphan bucket
'__unknown_album__'.

get_albums_metadata_grouped runs the grouped query and returns
LocalAlbum rows with directory_path empty and source_folders populated
with the comma-separated list of contributing folders. Pinned 'Unknown
Album' to the bottom via ORDER BY.

get_album_tracks_metadata fetches all tracks belonging to a metadata
group, ordered by disc/track number.

Adds idx_local_tracks_album_lookup index on (album, album_artist,
artist) to keep the GROUP BY index-backed at 16k+ track libraries.

LocalAlbum gains an optional source_folders field (None for folder-
grouped rows; Some(comma-separated) for metadata-grouped rows). The
existing folder-grouped query continues to populate it as None so no
downstream consumer breaks.

Hidden filter (album_settings.hidden) does not apply to metadata-
grouped rows in this iteration; revisiting requires extending
album_settings to support metadata keys.

cc
2026-05-06 17:10:51 -06:00
vicrodh 134808c880 feat(audio): route libasound errors through log::debug
libasound's default error handler prints directly to stderr. During
device enumeration probes, PCMs that wrap dmix or surround routes emit
benign errors ("unable to open slave", "no matching channel map") that
cluttered the app log without indicating a real problem.

Install a process-wide snd_lib_error_set_handler shim from
AudioBackendFactory::create_backend (idempotent via std::sync::Once)
that forwards the file/line/function/err/fmt tuple to log::debug. The
messages stay available with RUST_LOG=debug and disappear during
normal runs.

Adds alsa-sys 0.3 as a direct Linux-only dependency on qbz-audio
(previously transitive via alsa/cpal). The handler is declared
non-variadic and transmuted into libasound's variadic function pointer;
ignoring the unused varargs is ABI-compatible on every platform we
support.

cc
2026-04-24 13:40:09 -06:00
vicrodh 14b72b0582 chore(deps): bump rand from 0.8.5 to 0.8.6
Fixes GHSA-cq8v-f236-94qc (low: rand unsound with custom logger using
rand::rng()). Transitively bumped across both src-tauri and crates
workspaces via cargo update.

cc
2026-04-24 12:05:04 -06:00
vicrodh d871d924aa chore(deps): bump rustls-webpki from 0.103.12 to 0.103.13 in /crates
cc
2026-04-24 11:38:52 -06:00
vicrodh 1743c9e5ec feat(secrets): add qbz-secrets crate for device-bound secret wrapping
Foundation for the offline-cache hardening story. The offline cache
needs to persist per-track AES content keys to disk, but storing them
plaintext would make copying the SQLite DB enough to decrypt the
cached audio on another machine.

qbz-secrets provides a uniform wrap/unwrap API backed by:

1. **OS keyring** (preferred): libsecret/gnome-keyring on Linux,
   Keychain on macOS, DPAPI on Windows. A 256-bit master AES key
   lives inside the OS secure store; all wraps go through it. This
   is the gold-standard device binding used by commercial music
   apps (Spotify, Apple Music, Tidal) for the same class of data.

2. **HKDF fallback**: when the OS keyring is not reachable (headless
   setups — Raspberry Pi, Docker, the future qbzd daemon on a
   server), the master key is derived on demand via HKDF-SHA256
   over service_name + machine_id + a persistent per-install UUID.
   Weaker than keyring (reversible by anyone with filesystem access
   to both sources) but still device-bound against cross-machine
   copy attacks, and lets daemon/headless variants work at all.

Every wrap uses AES-256-GCM with a fresh random 12-byte nonce. The
on-disk envelope carries a version byte + backend marker + nonce +
ciphertext+tag, so (a) we detect tampering via GCM, (b) we refuse to
silently unwrap a blob produced by a backend we can't reach, and (c)
we have a forward-compat path for future key rotation.

4 unit tests cover: small-secret round-trip, 16-byte content-key
round-trip (the exact CMAF use case), GCM tamper detection, and
nonce-randomness (two wraps of the same input differ).

This crate intentionally has zero QBZ dependencies — it's a generic
primitive. Next commits wire it into the offline cache schema + the
DB migration.

cc
2026-04-17 00:53:01 -06:00
vicrodh 7b2b58a5d0 chore(deps): bump rand 0.9.2 -> 0.9.3 to fix GHSA-cq8v-f236-94qc (0.9.x line)
The 0.10.x advisory was fixed in v1.2.7 by bumping 0.10.0 -> 0.10.1.
The same advisory also applies to 0.9.x with a separate patched
version (0.9.3). Applying the bump to both lockfiles so the alerts
for the transitive 0.9.x deps close too.

cc
2026-04-16 12:08:14 -06:00
vicrodh c3a270fd18 chore(deps): bump rand 0.10.0 -> 0.10.1 to fix GHSA-cq8v-f236-94qc
Advisory: rand is unsound with a custom logger using rand::rng().
Affects 0.10.x only; 0.10.1 fixes the issue. The older rand versions
still present in the lockfiles (0.7.3, 0.8.5, 0.9.2) are transitive
dependencies that do not use the rand::rng() API surface introduced
in 0.10 and are not exploitable via this advisory.

The lru 0.12.5 advisory (GHSA-rhfx-m35p-ff5j) is a transitive dep of
ratatui 0.29.0 (used by qbzd). Can't be bumped without upstream
ratatui dropping the old lru; tracked separately.

cc
2026-04-16 10:16:38 -06:00
dependabot[bot] 56b7b24c90 chore(deps): bump rustls-webpki from 0.103.9 to 0.103.12 in /crates
Bumps [rustls-webpki](https://github.com/rustls/webpki) from 0.103.9 to 0.103.12.
- [Release notes](https://github.com/rustls/webpki/releases)
- [Commits](https://github.com/rustls/webpki/compare/v/0.103.9...v/0.103.12)

---
updated-dependencies:
- dependency-name: rustls-webpki
  dependency-version: 0.103.12
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-15 21:52:32 +00:00
vicrodh ebeb86d03d feat(release-watch): dedicated tabbed view mirroring Qobuz mobile
Ports the Qobuz mobile 'Radar de Novedades' / Release Watch screen 1:1
(see rr0/i.java): a standalone view with three lazy-loaded tabs —
Artists, Labels, Awards — each hitting /favorite/getNewReleases with
the matching type parameter (limit 150 per tab, matching the mobile
cap).

Home's Release Watch rail (which still shows the Artists feed by
default, matching the mobile's default tab) now has a 'See all' link
that routes to the new view.

Adds view 'discover-release-watch' to the navigation type, wires the
route in +page.svelte, and adds tab/empty-state i18n keys in all five
locales.

cc
2026-04-14 10:52:20 -06:00
vicrodh ca4480239b feat(qbzd): guard all stores/services for remote mode override
When controlling a qbzd daemon, QBZ Desktop becomes a pure remote
control. 119 invoke() call sites across 16 files now check
skipIfRemote() before executing local operations.

Local-only (skipped entirely): castStore, offlineStore,
offlineCacheState, updatesStore, titleBarStore, qobuzLegalStore,
sessionService.

Daemon-routable (guarded, daemon API routing in follow-up):
favoritesStore, albumFavoritesStore, artistFavoritesStore,
artistBlacklistStore, playlistFoldersStore, playbackPreferencesStore,
playbackContextStore.

playbackService: scrobbling, MPRIS metadata, and notifications
all skipped (daemon handles its own).

cc
2026-04-09 14:20:09 -06:00
vicrodh c3ce3c6667 fix(ci): drop openssl dependency, use rustls-only for all crates
reqwest default features included native-tls which pulled in
openssl-sys. This broke zigbuild cross-compilation because zig's
C compiler doesn't support the glibc version suffix for openssl
header expansion.

Fix: default-features = false on reqwest in workspace and
qbz-integrations (which had its own reqwest dep with defaults).
Now the entire daemon tree uses rustls-tls exclusively.

cc
2026-04-09 06:48:37 -06:00
vicrodh 8cee1c60be ci(qbzd): fix workflow paths, add cache, track Cargo.lock
Fixes:
- Rust cache via Swatinem/rust-cache (workspaces: crates -> target)
- Track crates/Cargo.lock for reproducible builds
- Bundle service files in release tarball
- Add permissions: contents: write for release job
- generate_release_notes for auto changelog

Local release build verified: 30 MB (23 MB stripped), x86_64.

cc
2026-04-09 06:22:17 -06:00