Commit Graph
19 Commits
Author SHA1 Message Date
vicrodh 5458f3aa74 feat(mixtape): extract shared qbz-mixtape crate (MyQBZ backend, headless)
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
2026-06-09 02:16:20 -06:00
vicrodh 7a7e5f82c4 feat(slint): system media controls — MPRIS with GNOME app icon + SMTC/MediaRemote
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
2026-06-06 11:03:11 -06:00
vicrodh 2fe7008e23 refactor(plex): extract Plex integration into frontend-agnostic qbz-plex crate
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
2026-05-28 15:20:23 -06:00
vicrodh d6e9345f5a feat(offline-cache): scaffold qbz-offline-cache crate with the tauri-free core
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
2026-05-27 19:08:52 -06:00
vicrodh 4a9cbdeb90 feat(qbz-radio): agnostic smart-radio pool builder crate
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
2026-05-21 07:49:01 -06:00
vicrodh eddfaed4c5 chore: remove the qbzd crate from the workspace
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
2026-05-19 02:50:05 -06:00
vicrodh aa64f3fafb refactor(qbz-credentials): extract credential store into a crate
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
2026-05-18 13:42:17 -06:00
vicrodh f1748383a5 feat(qbz-slint): scaffold Slint POC crate
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
2026-05-18 11:57:49 -06:00
vicrodh 4f008cd219 refactor(qbz-app): scaffold app crate 2026-05-14 11:40:19 -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 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 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 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 373220c379 chore(qconnect): register workspace crates and tauri adapter dependency
-cx
2026-02-21 12:17:30 -06:00
vicrodh 63994c0a20 refactor(cast): extract qbz-cast crate
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
2026-02-15 14:51:06 -06:00
vicrodh d6e4b3349b feat(qbz-library): extract local library management to standalone crate
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.
2026-02-15 12:12:52 -06:00
vicrodh 384459d4d7 feat(qbz-cache): extract audio caching to standalone crate
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.
2026-02-15 11:27:38 -06:00
vicrodh bd30e7cf24 feat(qbz-integrations): create third-party integrations crate
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)
2026-02-15 10:36:53 -06:00
vicrodh 0911cf75df refactor(backend): Phase 0 - Initialize multi-crate workspace
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
2026-02-14 12:48:52 -06:00