Add new no-dependency qbz-i18n crate to the workspace members.
Frontend-agnostic translation catalog per ADR-006 (reusable by
Slint/TUI/headless; no slint or tauri deps).
cc
- Booklet: new frontend-agnostic qbz-booklet crate (MuPDF render to PNG;
the Tauri Cargo stanza + device_rgb/alpha=false render workaround ported
verbatim). In-app reader modal (paginate/zoom/rotate/download) driven by
BookletState/BookletActions; header booklet button now opens it.
- "From the same artist" carousel (/artist/getReleasesGrid) and "Listening
suggestions" carousel (/album/suggest), reusing the Discover Carousel,
appended below the track list with their own artwork pipeline targets.
- External Links sidebar block (Last.fm / Discogs / MusicBrainz) built from
artist + title, opening in the system browser.
cc
Introduce the `qbz-theme` crate (ADR-006): pure Rust, no Slint/Tauri/heavy
deps, hand-rolled color + contrast math. It holds the scalable theming data
model the Slint migration consumes.
- `Rgba` + WCAG relative-luminance/contrast-ratio + an APCA Lc approximation
(secondary a11y gate for P3 tests).
- `ThemeColors`: the complete token contract (surfaces incl. bg-hover, text,
accent, danger/warning/success families, border-subtle/muted/strong,
focus-ring, favorite, card-shadow, 24-tier alpha superset with polarity
baked in).
- `ThemeId` enum (all 33 themes) with stable slugs, display names (proper-noun
data, not i18n keys), Core/Dark/Light/Accessibility categories, and a
luminance-derived light/dark flag. `default_theme_id()` = OLED.
- Registry materializes the 4 P1 themes (Dark/OLED/Tokyo Night/System-fallback)
from src/app.css, preserving the legacy translucent alias values so the
existing themes stay pixel-identical; derives the new token families.
- 22 unit tests (registry fully populated, default is OLED, legacy alpha
aliases unchanged, slug roundtrip).
cc
Slint 1.16's femtovg-wgpu renderer configures the wgpu surface via
get_default_config() and leaves the composite alpha mode at Auto, which
resolves to an opaque surface on KWin/Wayland — a borderless transparent
window then shows a black rectangular backing behind the rounded card.
WGPUConfiguration exposes no surface/alpha hook, so the only fix is a
renderer patch.
Vendor i-slint-renderer-femtovg-1.16.1 with a preferred_alpha_mode() that
picks a supported non-opaque mode (PreMultiplied / PostMultiplied /
Inherit, guarded by the surface's advertised alpha_modes, else Auto) and
wire it into set_surface(). Pulled in via [patch.crates-io]; Slint stays
pinned =1.16.1. The main window is unaffected (it paints opaque).
cx
Add-to-playlist (Slices 0-3):
- qbz-core: check_playlist_duplicates + pure compute_playlist_duplicates
(reuses qbz_models::PlaylistDuplicateResult) + unit tests.
- PlaylistDuplicateConfirmModal (Add all / Add new only / Cancel); the
duplicate check runs on the Qobuz->Qobuz picker branch only (local/Plex
add directly, mirroring Tauri); the track-id Vec lives Rust-side in a
stash, never in Slint.
- Inline create-and-add in the picker (online create_playlist, offline ->
local playlist per D8) + client-side filter (Slint 1.16 has no string
contains, routed through a Rust callback) + success toasts.
Playlist importer (1:1 Tauri port):
- New headless crate qbz-playlist-import (Spotify/Apple/Tidal/Deezer
scrapers, Qobuz matcher, importer, progress sink, shared http client;
copy-not-move from src-tauri, zero tauri deps; 40 unit tests green).
- PlaylistImportModal (two-step fetch-preview -> import with live progress
bar / log / summary) + controller + sidebar PLAYLISTS menu entry point.
Reco logging staged behind TODO(reco-v1) markers (golden rule: playlist
recommendations ship in Slint v1 once the reco engine is extracted to a
shared crate).
cc
S2 of the Lyrics epic: new shared crate crates/qbz-lyrics (headless,
frontend-agnostic per ADR-006), the whole fetch/parse/cache engine in one
testable home for Slint, Tauri and the TUI.
- Domain model: LyricsDoc/LyricsLine/Word (native Qobuz wsync word stamps
preserved) + the Tauri-wire-compatible LyricsPayload (camelCase serde);
LyricsProvider gains the qobuz variant and round-trips it.
- LRC parser ported from the Tauri TS semantics (gap markers as end-of-vocal,
8s MAX_SUNG cap, median last-line end) with the F5 defects fixed forward:
multi-stamp lines split per stamp, [offset:] honored. Emitter renders wsync
line bounds as LRC-with-gap-markers (Q5 persistence form, lossy at word
level only).
- Providers ported verbatim from src-tauri/src/lyrics/providers.rs: LRCLIB
search-first + scorer and lyrics.ovh, request shapes byte-identical.
- Cache: same per-user SQLite schema/path Tauri uses (WAL per ADR-002,
no TTL) plus the additive qobuz_wsync_json column (amended Q5) with an
idempotent in-place migration for Tauri-era DBs; readers prefer wsync.
Stats read entries AND size from the per-user path (F1 fix).
- Orchestrator LyricsService: cache probe with uniform plain-only soft miss
(Q4) -> Qobuz primary (wsync authoritative; plain held while LRCLIB is
probed for synced, the no-sync-regression rule) -> LRCLIB with one retry
on transport error -> lyrics.ovh; offline = cache-only with typed
NotAvailableOffline (F3); in-flight dedupe via shared futures (F6);
request-key echo on every response for the caller stale guard (F2).
- 55 unit tests: parser/emitter round-trips, scorer matrix, cache lifecycle
+ schema migration, chain priority/offline/dedupe with mocked providers.
cc
New crate crates/qbz-mixtape holding the Mixtapes+Collections backend, ported
1:1 from src-tauri/src/mixtape and re-typed against the real shared crates so it
runs headless (no tauri::State, no #[tauri::command] wrappers) per ADR-005/006:
- schema.rs / repo.rs / shuffle.rs copied verbatim (SQLite schema + migrations,
CRUD repository, DJ-mix similarity sampler). enqueue.rs re-typed to
qbz_qobuz::QobuzClient, qbz_library::LibraryDatabase and qbz_plex, dropping the
src-tauri crate::api / crate::library shims.
- ItemResolver is frontend-agnostic: ProdItemResolver takes a caller-supplied
sync local-resolution closure so &LibraryDatabase (!Sync) never crosses an
.await. resolve_local_item centralizes the full Local dispatch matrix
(Album / Track i64-parse / Playlist-unsupported) so frontends cannot drift.
- src-tauri left untouched: Tauri is removed at the end of the Slint port, so the
temporary duplication is intentional and zero-risk to the working Tauri build.
Adds qbz-mixtape to the workspace members + rand/strsim workspace deps.
Phase-2 Slice 1 of the MyQBZ Slint port. 57 crate tests pass.
cc
New frontend-agnostic crate qbz-media-controls (ADR-006): one MediaIntegration
trait, cfg-split backends. Linux uses mpris-server and publishes the MPRIS
org.mpris.MediaPlayer2.DesktopEntry property as com.blitzfc.qbz — the only way
GNOME Shell resolves the app icon for its media widget (souvlaki, the old
cross-platform crate, never set it, so GNOME showed no icon; KDE is lenient).
macOS/Windows keep souvlaki (MediaRemote/SMTC), where the icon comes from the
app bundle, not a DesktopEntry. The Linux server runs on a dedicated thread
with its own tokio runtime; state arrives over an async channel.
Wired into qbz-slint via src/media_controls.rs: started on shell entry,
metadata/playback pushed from the playback paths (mirroring the tray), and
inbound control events (media keys, the desktop media widget, Now Playing)
dispatched to the player through the existing QConnect-aware helpers. Both
Linux and macOS compile clean.
cc
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
Slice 0 of the offline-cache port (extraction per ADR-006). Creates the new
qbz-offline-cache workspace crate and moves the tauri-free leaf modules out of
src-tauri/src/offline_cache/ verbatim:
- types: the shared serde DTOs (was inline in offline_cache/mod.rs)
- db: the SQLite cached_tracks index (super:: -> crate::types)
- secret_vault, cmaf_store, path_validator: moved verbatim
- maintenance: super:: refs re-pointed to crate::
src-tauri is untouched and keeps its own copy, so the production build stays
green; the re-point + delete happens at the end of Slice 0. Crate compiles
in isolation and all 5 unit tests pass.
Remaining for Slice 0: playback pure-fn split, metadata/migration/purge
(re-point crate::api/crate::library -> qbz_qobuz/qbz_library), the downloader
+ CacheEvent/CacheEventSink abstraction, the OfflineCacheState struct, then
re-wire the Tauri commands and delete the old module.
cc
Ports the Tauri radio_engine into a standalone, frontend-agnostic
crate so any client (Slint, TUI, CLI) can build and play a smart
radio without Tauri.
- New crate qbz-radio (builder + db + engine + tests), copied
verbatim from src-tauri/radio_engine with the only change being
the dependency rewrite: crate::api::{QobuzClient, Track,
TracksContainer} -> qbz_qobuz::QobuzClient + qbz_models. Depends
only on qbz-qobuz + qbz-models + rusqlite + dirs. All four engine
tests pass.
- QbzCore::create_smart_artist_radio builds the session pool + pulls
up to 50 track ids + resolves them to full tracks. The radio DB's
rusqlite Connection is !Send, so the build + pull run on
spawn_blocking threads (the async builder driven via block_on),
mirroring the Tauri command.
- The artist "radio" action (Spotlight RADIO card + artist radio)
now uses the smart builder (playback::play_smart_artist_radio);
album radio (Radio Stations) keeps the Qobuz /radio/album
endpoint, so both radio systems are in use. play_artist_radio
(Qobuz /radio/artist) is retained as the simpler alternative.
cc
qbzd (the dormant TUI crate) pinned ratatui -> unicode-width =0.2.0,
which blocked upgrading Slint past 1.14. Dropped from the workspace
members; the directory stays on disk, just no longer built.
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
First increment of M1 of the Slint functional POC. Adds the qbz-slint
workspace member: Slint 1.x dependency, build.rs compiling ui/app.slint,
a minimal AppWindow, and the binary entry point.
Verifies the Slint toolchain builds in this environment before the
foundation layer (tokens, shell, theme, i18n, typed commands) lands.
Private slint-poc branch only (ADR-007).
cc
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
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
Extract casting functionality (Chromecast, AirPlay, DLNA) to standalone
qbz-cast crate. This crate is Tauri-agnostic and can be used by TUI/CLI
clients.
- Move device connection, discovery, threading, and media server to crate
- Keep only Tauri commands in src-tauri/src/cast/
- Thin re-exports in src-tauri for backwards compatibility
- Remove duplicate dependencies from src-tauri Cargo.toml
Create qbz-library crate with:
- LibraryScanner: directory scanning for audio files
- MetadataExtractor: audio metadata extraction via lofty
- LibraryDatabase: SQLite persistence with WAL mode
- CueParser: CUE sheet parsing for single-file albums
- Thumbnails: artwork extraction and thumbnail generation
- Tag sidecar: album metadata override files
src-tauri re-exports from qbz-library for backwards compatibility.
Enables TUI/headless clients to manage local libraries without Tauri.
Create qbz-cache crate with:
- AudioCache: L1 in-memory LRU cache (~300MB)
- PlaybackCache: L2 disk-based cache (~500MB)
- LRU eviction with spillover from L1 to L2
src-tauri now re-exports from qbz-cache for backwards compatibility.
This enables TUI/headless clients to use caching without Tauri.
New crate for Last.fm, ListenBrainz, and MusicBrainz integrations.
Designed to work WITHOUT Tauri for TUI/headless clients.
Modules:
- lastfm: Scrobbling and now-playing via Cloudflare Workers proxy
- listenbrainz: Direct API client with user token auth
- musicbrainz: Entity resolution with rate limiting
Features:
- `cache` (default): SQLite persistence for offline support
- ListenBrainz: Credential storage, offline listen queue
- MusicBrainz: Resolved entity cache with TTL
Architecture:
- Pure async Rust, no Tauri dependencies
- Shared error types via IntegrationError enum
- Rate limiting for MusicBrainz (1 req/sec direct, faster via proxy)
Create the foundational workspace structure for the multi-crate
architecture refactor. This phase establishes placeholder crates
that will be populated in subsequent phases.
Crates created:
- qbz-models: Shared types, events, and traits (Tier 0)
- qbz-audio: Audio backend system (Tier 1)
- qbz-player: Playback engine and queue (Tier 1)
- qbz-qobuz: Qobuz API client (Tier 1)
- qbz-core: Orchestrator (Tier 3)
All crates compile successfully with placeholder implementations.
The workspace uses shared dependencies via workspace.dependencies.
cc