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
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
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
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
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
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
- 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
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
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
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
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
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
- 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
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
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
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
- 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
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.
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.
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).
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
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.
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
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
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
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