Commit Graph
211 Commits
Author SHA1 Message Date
vicrodh ac17787264 feat(qbzd): unified event bus + SSE endpoint (Phase 3)
Playback event loop polls QbzCore player state with adaptive timing
(250ms playing, 1s paused, 5s idle) and broadcasts PlaybackSnapshot
events to the event bus.

New SSE endpoint at /api/events streams all DaemonEvents to clients.
Broadcast channel bounded to 256 events with lagged client handling.

cc
2026-04-09 03:38:06 -06:00
vicrodh 6ee123c2a4 feat(qbzd): add session lifecycle port (Phase 2)
New session.rs module ports activate_session from Tauri to headless:
- Creates per-user directories (same layout as desktop app)
- Persists last_user_id marker for session restore
- Emits RuntimeEvent::Ready via event bus
- Skips desktop-only stores (tray, visualizer, remote control)

Session activation wired into daemon startup after auto-login.
Audio settings reload deferred until qbz-player crate exposes
reload_settings (currently on legacy player only).

cc
2026-04-09 03:23:56 -06:00
vicrodh 2916dee42c feat(qbzd): wire QbzCore, Player, AudioCache, keyring (Phase 1)
DaemonCore now initializes the real QbzCore with DaemonAdapter,
creates Player with audio settings from shared database, and
attempts auto-login from saved OAuth token in system keyring.

Working features:
- QbzCore init (bundle token extraction from Qobuz web player)
- Player audio thread (ready for playback commands)
- AudioCache with auto-detected size from RAM
- Shared audio settings with desktop app (same SQLite DB)
- Keyring credential lookup (same service as desktop)
- HTTP endpoints: /api/ping, /api/info, /api/status
- Graceful shutdown (SIGTERM/SIGINT -> stop player -> exit)

cc
2026-04-08 21:29:28 -06:00
vicrodh b0ed467664 feat(qbzd): scaffold headless daemon binary (Phase 0)
New workspace member crates/qbzd/ with:
- CLI interface (clap): port, bind, data-dir, config, token, log-level
- Config system (TOML): server, audio, cache, data, mpris, qconnect, mdns
- Resource detection (sysinfo): auto-size cache from available RAM
- DaemonAdapter: FrontendAdapter impl routing CoreEvents to broadcast channel
- DaemonCore: state container replacing Tauri's app.state
- Minimal HTTP server (Axum): /api/ping + /api/info
- Graceful shutdown on SIGTERM/SIGINT

Zero Tauri dependency. Desktop app unaffected.

cc
2026-04-08 21:18:01 -06:00
vicrodh a30cfa7c71 Merge branch 'bugfix/internal/1.2.5-alsa-lifecycle' into pre-release 2026-04-08 10:29:37 -06:00
vicrodh 5aa0df1cbc feat(api): sign all remaining Qobuz API endpoints
Add request_ts + request_sig to every API call matching the mobile
app interceptor pattern. Introduces signed_get/signed_get_auth
helpers and a generic sign_request function.

Endpoints now signed:
- album/get, album/getFeatured
- track/get, track/getList
- artist/get, artist/getSimilarArtists, artist/page, artist/getReleasesGrid
- discover/index, discover/playlists, discover/* browse endpoints
- genre/list
- label/page, label/explore, label/get
- playlist/get, playlist/getUserPlaylists, playlist/create,
  playlist/delete, playlist/addTracks, playlist/deleteTracks,
  playlist/update, playlist/subscribe, playlist/unsubscribe
- playlist/getTags
- favorite/create, favorite/delete

Excluded (pre-auth, cannot sign): user/login, oauth/callback.
Already signed (unchanged): track/getFileUrl, favorite/getUserFavorites,
session/start, file/url, all search endpoints.

cc
2026-04-08 09:41:57 -06:00
vicrodh dbc97d6bd1 feat(search): add request signing to all search endpoints
Add request_ts + request_sig (MD5 signature) to catalog/search,
album/search, track/search, artist/search, and playlist/search
endpoints matching the mobile app authentication pattern.

Migrate v2_search_all from legacy AppState to CoreBridge, removing
the last direct API call that bypassed the V2 crate architecture.

cc
2026-04-08 08:34:32 -06:00
vicrodh c8a3a648f5 chore(alsa): lower device enumeration logs to debug, add 5th retry
Reduce log noise by demoting ALSA device enumeration from info to debug.
Add a 5th backoff retry at 800ms for both hw and plughw paths as a
safety net for any EBUSY cases that slip through the lifecycle fix.

cc
2026-04-07 11:53:23 -06:00
vicrodh 426a4d0dba fix(player): stop engine before dropping ALSA stream on format change
In the Play and PlayStreaming handlers, the old engine's writer thread
still holds an Arc<AlsaDirectStream> when the stream_opt is dropped and
a new snd_pcm_open() is attempted. This causes EBUSY on track switches
with sample rate changes. Reorder to: stop engine -> drop stream -> open
new stream, matching the Stop handler's correct lifecycle.

cc
2026-04-07 11:53:17 -06:00
vicrodh 74e3ba9519 chore(logging): reduce noise in default log level
- Device enumeration (48 CPAL devices): info → debug
- Playlist fetch completion: info → debug
- Playlist command invocation: info → debug
- zbus socket_reader/tracing: filtered to warn level

These remain visible with RUST_LOG=debug or verbose log capture.

cc
2026-04-07 08:39:18 -06:00
vicrodh 4505312280 fix(security): remove hardcoded crypto seed from cmaf crate
Move the CMAF app seed from a hardcoded constant in qbz-cmaf to a
parameter passed by callers. The seed now lives in qbz-qobuz::auth
where other app-level constants are defined. This resolves CodeQL
alerts for hard-coded cryptographic values.

Also truncate session_id in logs to avoid cleartext logging of
sensitive session identifiers.

cc
2026-04-07 07:04:33 -06:00
vicrodh 2c0f42b885 chore(deps): bump dirs 5 -> 6 across all crates
Zero API changes, only bumps internal deps (windows-sys, redox_users).
Eliminates duplicate dirs versions in Cargo.lock.

Closes #257

cc
2026-04-06 15:16:34 -06:00
vicrodh bf555ab143 feat(streaming): CMAF segmented download pipeline
Download tracks via Qobuz Akamai CDN with encrypted CMAF segments.
Derives session key (HKDF), unwraps content key (AES-CBC), decrypts
frames (AES-CTR), reassembles into standard FLAC. Falls back to legacy
track/getFileUrl if CMAF fails.

- Add qbz-cmaf dependency to src-tauri
- Make ensure_cmaf_session public on QobuzClient
- Add ensure_cmaf_session and get_file_url passthroughs to CoreBridge
- Add download_audio_cmaf and try_cmaf_download helpers
- Wire CMAF as primary download path in v2_play_track with legacy fallback

cc
2026-04-06 13:31:41 -06:00
vicrodh 0265cb996c feat(qobuz): add session/start and file/url CMAF endpoints
New CMAF session management with lazy renewal and file/url endpoint
returning segmented encrypted streaming URLs.

cc
2026-04-06 13:19:57 -06:00
vicrodh 8133931df5 feat(models): add SessionStartResponse and TrackFileUrl types
New response models for the CMAF streaming API endpoints
(session/start and file/url).

cc
2026-04-06 13:15:54 -06:00
vicrodh b3a9069712 feat(cmaf): add CMAF segment parser
Parse init segment (FLAC header + segment table) and audio segments
(frame entries with IVs for AES-CTR decryption).

cc
2026-04-06 13:12:55 -06:00
vicrodh 794dd9f24c feat(cmaf): add qbz-cmaf crate with crypto module
HKDF-SHA256 session key derivation, AES-128-CBC content key unwrap,
AES-128-CTR frame decryption, and MD5 request signing for Qobuz
CMAF segmented streaming.

cc
2026-04-06 13:10:00 -06:00
vicrodh 4439070032 fix(alsa): progressive backoff retry on device busy during track skip
When skipping tracks quickly, the previous PCM handle may not be
fully released by the kernel before the new track tries to open
the same ALSA device (snd_pcm_open fails with EBUSY).

Previous behavior: single retry after 200ms PipeWire suspend, then
immediate failure. This didn't cover our own PCM race condition.

New behavior: 4 retries with progressive backoff (50/100/200/400ms)
for both hw and plughw paths. PipeWire suspend still attempted once
upfront. Total worst-case wait: 750ms (vs 200ms before), but the
early retries at 50-100ms cover the common fast-skip case.

cc
2026-04-06 11:23:35 -06:00
vicrodh 09fcb5eb7b chore(deps): upgrade alsa crate 0.9 -> 0.10
Remove dead alsa dependency from src-tauri/Cargo.toml (code lives in
qbz-audio since V2 migration) and bump qbz-audio to alsa 0.10.
Eliminates duplicate alsa versions in Cargo.lock.

Breaking changes in 0.10 (Format/ChmapPosition reorder) do not affect
QBZ — we use catch-all matches and never rely on PartialOrd.

Closes #260

cc
2026-04-06 09:18:27 -06:00
vicrodh 1934d501ed feat(audio): make quality fallback opt-in via allow_quality_fallback setting
Off by default. When disabled, if the requested quality is unavailable
the download/stream fails instead of silently downgrading. Persisted
to audio_settings DB. Applied to offline cache downloads.

cc
2026-04-05 10:44:05 -06:00
vicrodh 83e43949b7 feat(audio): add skip_sink_switch option for JACK/DAW routing (#263)
When enabled, skips pactl set-default-sink on stream creation.
Preserves external audio routing (JACK, qjackctl, Reaper).
Mutually exclusive with DAC passthrough.

cc
2026-04-04 12:08:22 -06:00
Afonso Jorge Ramos 7a1890ad57 fix(audio): use released coreaudio-rs 0.14.1 instead of fork 2026-04-03 14:01:25 +02:00
vicrodh 3ac7073b9c feat(qobuz): migrate login_with_token to V2 crates
Add login_with_token to QobuzClient and QbzCore for restoring
sessions from saved OAuth tokens. Previously only existed in
the legacy client.

cc
2026-04-02 07:47:45 -06:00
Victor RH e56b0cbb6a Merge pull request #238 from afonsojramos/fix/macos-x86-build
fix(macos): enable x86_64 cross-compilation
2026-04-01 16:29:13 -06:00
Victor RH eed692a852 Merge pull request #236 from afonsojramos/feat/coreaudio-device-probing
feat(macos): add CoreAudio device probing and sample rate switching
2026-03-31 20:26:29 -06:00
Afonso Jorge Ramos 9823dfb680 fix(macos): enable x86_64 cross-compilation
- Upgrade rust_cast 0.18.1 → 0.21 (switches from openssl to rustls,
  removing the openssl-sys dependency that blocked x86_64 cross-compile)
- Split mupdf sys-lib features to Linux-only (on macOS there are no GTK
  symbol conflicts, and system libs don't support x86_64 cross-compile
  from Apple Silicon)
- Update release-macos workflow to build both aarch64 and x86_64 DMGs
  using a matrix strategy
2026-04-01 02:40:55 +02:00
vicrodh 1e019becfd fix: resolve correct artist and country for MusicBrainz metadata
Three fixes:
- resolve_artist now prefers exact name matches over highest score,
  fixing "The Warning" resolving to "Fates Warning" (score=100 but
  wrong name vs score=97 exact match)
- resolve_area_country now returns ISO country code alongside name,
  fixing flag display (was showing US flag for German artists)
- country_code updated in location metadata for correct flag rendering

cc
2026-03-31 17:23:00 -06:00
vicrodh 1953774d03 fix: resolve birth country from area hierarchy instead of MB country field
Add resolve_area_country() that walks the MB area hierarchy up to
the country level (e.g., Frankfurt -> Hessen -> Germany).

Use it in v2_musicbrainz_get_artist_metadata to get the real birth
country instead of MB's "country" field (which is where the artist
is active, not where they were born).

Hans Zimmer now shows "Frankfurt am Main, Germany" and clicking
the location shows German composers, not American ones.

cc
2026-03-31 17:17:01 -06:00
vicrodh 6acd843891 feat: add streaming buffer progress indicator to seekbar
Show a subtle buffer progress bar in the seekbar when streaming
tracks (before they are fully cached). The bar disappears once
the track is fully buffered.

Backend: expose buffer_progress from BufferedMediaSource through
SharedState -> PlaybackEvent -> playerStore.
Frontend: render .seekbar-buffer bar behind the playback fill.

cc
2026-03-31 16:02:07 -06:00
victorrhgap 016b0f6166 chore: remove unused required_i32_to_u64 function
cc
2026-03-31 11:09:32 -06:00
victorrhgap a99d558e37 fix: update lofty 0.18 API calls to lofty 0.23
- Imports reorganized: use lofty::prelude::*, lofty::tag::*, lofty::picture::*
- Picture::new_unchecked -> Picture::unchecked().pic_type().mime_type().build()
- set_year/remove_year -> set_date/remove_date with Timestamp
- ItemKey::Unknown("ISRC") -> ItemKey::Isrc
- save_to_path(path) -> save_to_path(path, WriteOptions::default())
- get_string/remove_key now take ItemKey by value
- Bump qbz-library crate lofty dep to 0.23

cc
2026-03-31 09:32:02 -06:00
Afonso Jorge Ramos 6bf397e0f3 feat(macos): auto-select SystemDefault backend on non-Linux
When backend_type is None (auto-detect), default to SystemDefault on
non-Linux platforms so macOS gets CoreAudio device probing and sample
rate switching instead of falling through to the legacy CPAL path.
2026-03-31 02:20:13 +02:00
Afonso Jorge Ramos 4c15a43200 feat(macos): enhance CpalDefaultBackend with device capabilities
On macOS, enumerate_devices() now populates supported_sample_rates,
max_sample_rate, device_bus, and is_hardware via CoreAudio probing.
create_output_stream() switches the device sample rate to match the
track before opening the CPAL stream.
2026-03-31 02:20:08 +02:00
Afonso Jorge Ramos c51f07fe65 feat(macos): add CoreAudio device probing and sample rate switching
Add coreaudio_direct module using coreaudio-rs safe wrappers for:
- Device capability probing (supported sample rates, transport type)
- Automatic sample rate switching per-track
- Device lookup by name for CoreAudio↔CPAL bridging

Uses a fork branch of coreaudio-rs that adds get_available_sample_rates
and get_device_transport_type helpers (PR RustAudio/coreaudio-rs#149).
2026-03-31 02:19:59 +02:00
victorrhgap 235bd08e2c fix(qconnect): tolerate missing queue_version major/minor fields
Qobuz changed their protocol to sometimes send QueueVersionRef
without major/minor populated. Our decoder treated these as
required fields, causing all cloud_type=6 messages to fail with
"missing required numeric field 'queue_version.major'".

Default to 0 when absent — matches the dev panel behavior.

cc
2026-03-24 21:14:44 -06:00
Afonso Jorge Ramos dad062d61b fix(audio): add docs and fix param names for non-Linux stubs 2026-03-20 01:13:55 +01:00
Afonso Jorge Ramos f854e09661 feat(audio): add SystemDefault variant to AudioBackendType 2026-03-20 01:13:29 +01:00
Afonso Jorge Ramos 5ed549d46b feat(macos): gate Linux-only audio backends with cfg
Add #[cfg(target_os = "linux")] to pipewire_backend and pulse_backend
modules. Add CpalDefaultBackend for non-Linux platforms using CPAL
default host (CoreAudio on macOS). Gate reset_pipewire_clock() calls.
2026-03-20 00:45:03 +01:00
vicrodh 1f1e729db8 fix: HiFi Wizard now detects real DAC sample rates from /proc/asound
v2_query_dac_capabilities was returning hardcoded sample rates
(44.1-192kHz including 176.4kHz) instead of querying the actual
hardware. This caused incorrect configuration for DACs that don't
support all rates (e.g. DacMagic Plus doesn't support 176.4kHz).

Now queries /proc/asound/cardN/stream0 via PipeWire sink mapping
or ALSA device ID to get the real discrete rates from the USB
descriptor. Falls back to defaults only if detection fails.

cc
2026-03-19 12:41:24 -06:00
vicrodh 1ad16338fa feat: add Qobuz playlist follow/unfollow (#172)
Discovered undocumented Qobuz API endpoints from bundle analysis:
- POST /playlist/subscribe { playlist_id }
- POST /playlist/unsubscribe { playlist_id }

This is the native "follow playlist" feature that syncs across all
Qobuz clients, separate from QBZ's existing "Copy to Library" (local
copy) and "Favorite" (local flag) features.

Added:
- subscribe_playlist/unsubscribe_playlist in both qbz-qobuz and legacy client
- v2_qobuz_subscribe_playlist / v2_qobuz_unsubscribe_playlist commands
- Bookmark toggle button in PlaylistDetailView (next to Copy button)
- i18n keys in all 5 locales

cc
2026-03-19 11:36:56 -06:00
vicrodh 5488839e97 fix: increase PipeWire rate switch delay and verify after stream creation (#180)
USB hubs/docks (e.g. Razer USB4 Dock) may need longer than 300ms for
sample rate switching. When PipeWire hasn't finished reconfiguring,
96K/192K tracks play at wrong speed (half/quarter) and sound distorted.

Changes:
- Increase initial clock.force-rate delay from 300ms to 500ms
- After stream creation, query pw-metadata clock.rate to verify the
  actual graph rate matches what was requested
- If mismatch detected, retry with additional 500ms delay and re-force
- Log warnings when rate can't be verified so users can report

cc
2026-03-19 10:57:21 -06:00
vicrodh 3b3dbc7ce1 fix(qconnect): trigger resync on reorder and remove server events
SrvrCtrlQueueTracksReordered and SrvrCtrlQueueTracksRemoved were not
triggering AskForQueueState, so the local queue never reflected
reorder/remove actions performed via QConnect. Same pattern as the
existing shuffle resync.

cc
2026-03-14 21:34:03 -06:00
vicrodh 52e3653dc3 fix(qconnect): complete repeat pending on loop ack
cx
2026-03-14 13:25:22 -06:00
vicrodh a59daf43dc fix(qconnect): resync queue after shuffle mode changes
cx
2026-03-13 23:13:16 -06:00
vicrodh cf2447f4b2 fix(qconnect): stop deriving remote shuffle locally
cx
2026-03-13 23:07:22 -06:00
vicrodh af7652c86b fix(qconnect): wait for authoritative shuffle queue
cx
2026-03-13 22:54:27 -06:00
vicrodh 1fc7a0a1a3 fix(qconnect): materialize remote shuffle atomically
cx
2026-03-13 20:43:56 -06:00
vicrodh 09c6d8f1f8 fix(qconnect): route shuffle and repeat through runtime
cx
2026-03-13 20:28:11 -06:00
vicrodh f18bd10aa4 fix(qconnect): complete active-renderer pending on session ack
cx
2026-03-13 18:50:04 -06:00
vicrodh de29f1e2e0 chore(qconnect): checkpoint current poc state
cx
2026-03-13 17:28:05 -06:00