Move the Plex LAN client, hand-rolled XML metadata parsing, and the
SQLite plex_cache.db logic out of src-tauri/src/plex/mod.rs into a new
qbz-plex core crate so it can be shared by every frontend (Tauri, Slint,
TUI) per ADR-006. Plex previously lived entirely in src-tauri (~1715
lines), which blocked the Slint Local Library port from shipping Plex.
The extraction is mechanical: the module was already almost entirely
plain Rust (reqwest + rusqlite + serde + hand-rolled XML). The only
Tauri coupling was a dead plex_play_track command bound to AppState
(unreferenced; the live path is commands_v2::v2_plex_play_track, which
calls plex_resolve_track_media + AppState.player itself). That dead fn
and the vestigial #[tauri::command] attributes are dropped.
src-tauri/src/plex/mod.rs becomes a `pub use qbz_plex::*;` re-export so
every existing crate::plex::* call site compiles unchanged. Full Tauri
workspace cargo check stays green; qbz-plex tests pass.
cc
Completes Slice 0. src-tauri now depends on the qbz-offline-cache crate; the
old src-tauri/src/offline_cache/ implementation files are deleted. What remains
in that module is a thin shim: a re-export of the crate plus the Tauri glue.
- offline_cache/mod.rs: pub use qbz_offline_cache::* + tauri_cache_sink (maps
CacheEvent -> the exact legacy offline:caching_*/offline:unlock_* app.emit
shapes, so the Svelte frontend is unchanged) + core_client (resolves the core
qbz_qobuz::QobuzClient from the CoreBridge for the download pipeline).
- Cache/redownload/migration commands: drop the legacy crate::api::QobuzClient
(state.client) + the CoreBridge double-plumb; pass one core client + an
app.emit-backed CacheEventSink. Unused `state: State<AppState>` params dropped.
- Playback + library cached-resolution call sites: pass &tauri_cache_sink(..)
to load_cmaf_bundle_with_ui_events instead of &app_handle.
- session_lifecycle: apply_persisted_limit now reads the persisted value here;
purge takes &library.db directly.
- migration: qobuz_client is the Option-wrapped core client (consistent with the
downloader); None mid-loop is recorded as a per-track failure.
Full src-tauri cargo check passes; behaviour-preserving for the Svelte frontend.
Backend extraction (ADR-006) done — next is Slice 1 (shared tier-walk in core).
cc
qbzd was a premature daemon experiment that never shipped (gated off
behind QBZD_ENABLED, no user access). Its ratatui 0.29 dependency pinned
unicode-width =0.2.0, which conflicts with slint's annotate-snippets in a
shared workspace. Remove it entirely; it will be reimplemented from
scratch later on the clean, UI-agnostic shell.
- Delete crates/qbzd daemon crate + workspace member (drops ratatui,
crossterm and the other qbzd-only deps from the lock).
- Delete src-tauri qbzd_discovery (mDNS) + its commands/state wiring in
lib.rs; drop the direct mdns-sd dep (Chromecast keeps its own via
qbz-cast).
- Delete remoteApi/remoteEvents/playbackTargetStore; neutralize
commandRouter to local-only dispatch (isRemote/skipIfRemote are now
always-false no-ops so the ~100 guard call-sites keep compiling).
- Strip qbzd branches from playbackService and the remote banner from
NowPlayingBar; excise qbzd from CastPicker (Chromecast/DLNA kept).
- Remove qbzd i18n keys (5 locales), README daemon node, gitignore note.
cargo check (crates + src-tauri) and svelte-check all green.
cc
- version: Cargo.toml, tauri.conf.json, package.json (+ Cargo.lock, package-lock.json)
- metainfo.xml: add 1.2.14 release entry
- About modal codename -> "Just cleaning"
- docs/release-1.2.14: TAG_DETAILS.md + CHANGELOG_FULL.md
Snap and Nix flake are intentionally left at 1.2.13: they pin a source
tag, and this is a no-tag, build-only cycle (no distribution).
cc
cpal 0.17.3 requires alsa 0.11, which in turn pulls alsa-sys 0.4. The
`links = "alsa"` constraint permits only one alsa-sys in the dependency
graph, so qbz-audio's direct alsa stack is raised in lockstep:
alsa 0.10 -> 0.11 and alsa-sys 0.3 -> 0.4. Both workspace lockfiles
(crates/ and src-tauri/) are updated to cpal 0.17.3 / alsa 0.11.0 /
alsa-sys 0.4.0.
No source changes required: alsa_direct.rs uses only the stable
high-level alsa API (pcm, HwParams, Format, Direction, ValueOr, mixer),
and the snd_lib_error_set_handler FFI in alsa_error_handler.rs is
unchanged in alsa-sys 0.4. Both workspaces pass cargo check.
cc
Adds qbz-integrations as a dependency of qbz-core and embeds a
MusicBrainzClient on the QbzCore struct (initialized in new()). The
client's own enabled flag governs whether requests fire; calls return
the integration-layer ServiceUnavailable error when disabled.
New methods on QbzCore<A>:
musicbrainz_is_enabled()
musicbrainz_set_enabled(enabled)
musicbrainz_resolve_artist(name) -> Option<ResolvedArtist>
musicbrainz_get_artist_metadata(mbid) -> ArtistMetadata
musicbrainz_get_artist_relationships(mbid) -> ArtistRelationships
The metadata method calls location::extract_metadata on the full artist
response and then resolves the real country from the begin_area
hierarchy when a city-level location was found (MB's "country" field is
where the artist is currently active, not where they were born/formed).
The relationships method splits relations into members / past_members /
groups / collaborators by relation_type and direction, mirroring
v2_musicbrainz_get_artist_relationships exactly.
No cache wiring yet — every call hits the network. A cache wrapper can
be plugged in later via a setter without changing the method
signatures.
cc
Pure-logic move of two helper modules out of src-tauri/src/musicbrainz/
into the qbz-integrations crate, where the MusicBrainzClient and DTOs
already live:
src-tauri/src/musicbrainz/location_discovery.rs
-> crates/qbz-integrations/src/musicbrainz/location.rs
src-tauri/src/musicbrainz/genre_normalization.rs
-> crates/qbz-integrations/src/musicbrainz/genre.rs
The functions exposed are unchanged: extract_metadata,
compute_affinity_score, build_scene_cache_key, format_life_span_date
(from location); normalize_genre, is_broad_genre, extract_affinity_seeds,
genre_summary (from genre). Imports in src-tauri/src/commands_v2/
integrations.rs are repointed to the new home; the legacy file
declarations are removed from src-tauri/src/musicbrainz/mod.rs. No
behavior change.
Unblocks the Slint side from reaching the same helpers via qbz-core
without depending on src-tauri.
cc
The qbz-cache and qbz-cmaf entries on qbz-player landed with the CMAF
playback work yesterday but the lockfile sync was missed. No code
change, lockfile only.
cc
Extracts two self-contained, framework-agnostic modules out of
src-tauri so they can be reused by other clients without duplicating
logic:
- ImageCacheService / ImageCacheStats move into the qbz-cache crate
(LRU disk cache, MD5-keyed, SQLite index).
- The credential store (keyring + AES-256-GCM encrypted file for Qobuz
credentials and the OAuth token) moves into a new qbz-credentials
crate.
src-tauri/image_cache.rs and src-tauri/credentials/mod.rs become thin
re-exports, keeping their Tauri managed-state wrappers. The Tauri app
is behavior-identical; both modules were already self-contained (no
Tauri and no crate:: references).
cc
Moves the credentials module (keyring + AES-256-GCM encrypted file
store for Qobuz credentials and the OAuth token) out of src-tauri into
a new qbz-credentials crate. src-tauri/credentials re-exports it, so the
Tauri app keeps identical behavior while the Slint shell can share the
exact same implementation, encrypted file, and keyring entries.
The module was already fully self-contained (no Tauri, no crate::
references); only the dependency declarations move with it.
cc
Cover art now goes through the QBZ image cache instead of being
downloaded directly every time, matching the Tauri behavior.
- Moves ImageCacheService / ImageCacheStats into the qbz-cache crate
(LRU disk cache, MD5-keyed, SQLite index). src-tauri/image_cache.rs
re-exports it and keeps the Tauri managed-state wrapper, so the Tauri
app stays behavior-identical and both shells share one implementation
and one on-disk cache (~/.cache/qbz/images).
- qbz-slint artwork.rs checks the cache first, downloads and stores on
a miss, and trims the cache to 200MB on startup.
cc
Adds a framework-agnostic composition root for non-Tauri UI shells
(Slint, TUI, headless) as Task 1 of the Slint POC readiness audit
(session-20-slint-poc-readiness-audit).
AppRuntime<A> owns an Arc<QbzCore<A>> plus the runtime state machine,
built with the same recipe as the Tauri CoreBridge but with no Tauri
dependency. Scope is composition and accessors only; per-user store
ownership and session activation are deferred to Task 2.
qbz-app now depends on qbz-core, qbz-player, qbz-audio, and qbz-models.
The Tauri app does not consume this module; it is purely additive.
cc
Version bumped across Cargo.toml, tauri.conf.json, package.json, snapcraft.yaml (version + source-tag), flake.nix, and the lockfiles. metainfo.xml gains a 1.2.13 release entry, the About modal codename becomes "400+ stars", and the README contributors list adds @Vudgekek and @DoubleGate.
cc
DiscordIpcClient::new became infallible in 1.0 — it now returns Self
directly instead of Result<Self>. Failures surface at connect() time
where the catch already lives. Removed the now-spurious `if let Ok()`
wrap around the constructor.
Closes#428 (applied manually because the bump needed a code change
that Dependabot can't author).
cc
Hotfix release. Reverts titlebar-mode-selector and PR #406 (broken
window chrome on Linux); preserves PR #407 (sticky home header) and
adds PR #408 (macOS native icon and tray variants by @Vudgekek).
cc
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
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
Bumped only for src-tauri's qconnect-discovery path. qbz-cast (chromecast/
DLNA) stays on 0.11 because mdns-sd 0.19 changed the address API
(HashSet<IpAddr> → HashSet<ScopedIp>) and adapting qbz-cast is real
work outside dependabot scope. Workspace now carries both versions
in the graph; both compile and operate independently.
cc