Commit Graph
27 Commits
Author SHA1 Message Date
vicrodh 74e1610433 feat(orbit): add shared library foundations and gated host tools
Extract the HTTP host and catalog layer into qbz-control, preserve the
existing daemon API, and keep private session credentials out of SSE.

Scope library stores and scan workers to their host. Share bounded source
search and show Local results alongside Qobuz in the Qt search page.

Add the launch-gated Qt Orbit laboratory and persistent remote-context
banner. Let qbzd run --orbit own and administer its file library through
instance-scoped API and CLI operations. Full remote UI control remains
pending.

Validate library/control/daemon suites, Qt search and QML regressions,
two isolated daemon processes, Qt release build and final cargo check.

cx
2026-09-12 13:02:29 -06:00
vicrodh d3cf7ae53d chore(qconnect): wire LAN runtime dependencies
cx
2026-08-30 22:22:31 -06:00
vicrodh 6d8c951a38 chore(deps): rusqlite 0.40 (u64 casts; a latent two-statement execute fixed)
rusqlite 0.40 removed ToSql/FromSql for u64 (SQLite integers are i64):
every u64 column now reads/writes through an explicit i64 cast. It also
rejects a multi-statement execute() instead of silently running only the
first statement — which is what catalog.rs's editions/logical_albums
prune had been doing all along; it is an execute_batch now, so the
logical_albums prune finally runs.

cc
2026-08-29 08:36:21 -06:00
vicrodh f06ffc7141 fix(qbzd): gate the instance lock per OS
lock.rs imported std::os::unix and called libc::flock unconditionally, so
a workspace test run could not compile on Windows.

The Windows arm locks ONE byte at offset 1 GiB rather than the whole file.
Byte-range locks are mandatory there, not advisory like flock: measured on
this box, a whole-file lock from offset 0 makes the losing instance's
read_to_string fail with ERROR_LOCK_VIOLATION, so AlreadyRunning would
always carry None - the pid is the only reason that variant exists. With
the lock byte past any content the pid still reads back, the same handle
conflict is still detected, and the lock still releases when the File
drops. Reserving a region past EOF is what SQLite's Windows VFS does.

Only ERROR_LOCK_VIOLATION means contention; a filesystem that does not
implement byte-range locks answers ERROR_INVALID_FUNCTION and must surface
as Io, not as a daemon that is not running. The unix arm keeps its
existing any-failure-means-busy behaviour unchanged.

cc
2026-08-26 23:07:02 -06:00
vicrodh 69ed612077 refactor(frontend): retire Slint application
cx
2026-08-26 11:10:18 -06:00
vicrodh bdbdf4e0a1 feat(qbzd): publish MPRIS media controls (KDE/GNOME widget + media keys)
Add a daemon MPRIS service (CONSOLE ext) via qbz-media-controls (mpris-server on
Linux, incl. the DesktopEntry that lets GNOME/KDE resolve the app icon). A KDE
Plasma media widget, a plasmoid, or hardware media keys now drive the daemon
with NO custom client code — the marquee third-party-integration surface.

Two halves in crate::mpris:
- OUTBOUND: a CoreEvent-bus subscriber pushes now-playing metadata, play/pause,
  live position, and volume into the OS controls (seeded once from live state).
- INBOUND: the qbz-media-controls callback maps MediaEvent (Play/Pause/Toggle/
  Next/Previous/Stop/Seek/SetPosition/SetVolume) onto core transport commands;
  seek/volume honor the DSD-direct guard; Next/Previous fire the full advance
  ritual off the D-Bus thread; Raise/Quit are ignored (headless).

The inbound callback holds only a Weak<AppRuntime> (upgraded per event) and the
updater task upgrades a Weak once to seed then drops it, so the integration never
pins the runtime — the #521 audio-release-before-drop(booted) ordering is
unaffected. Spawned next to the scrobbler in run(); torn down (updater abort +
handle drop) in shutdown. On by default where a session bus exists; QBZD_MPRIS in
{0,false,off,no} disables it, and spawn returns None gracefully on a headless
box with no D-Bus. 280 tests green.

cc
2026-07-19 22:24:41 -06:00
vicrodh 17de908225 feat(qbzd): scrobbling auth foundation — Last.fm / ListenBrainz (CONSOLE)
The connect flows, same methodology as `qbzd login`:
- `scrobble login lastfm`: LastFmClient::get_token prints an authorize URL, the
  user approves on last.fm, get_session exchanges it for a session key.
- `scrobble login listenbrainz --token <T>`: ListenBrainzClient::set_token
  validates a pasted user token (like `login --token`).
- `scrobble status` · `scrobble enable|disable <provider>`.

Credentials land in the CANONICAL scrobbler store
(qbz_app::settings::scrobblers::ScrobblerSettingsStore — SQLite at the daemon
data root), the SAME store the desktop uses and the one the settings bundle
export/import already carries, so scrobbler creds travel with `settings export`.
Secrets registered with qbz_log; a running daemon is nudged to reload.

- Cargo.toml: qbz-integrations path dep (already in-tree via qbz-core).
- cli/scrobble.rs: the verbs. main.rs: Scrobble subcommand group.

Verified: cargo test -p qbzd --bin qbzd -> 276 passed, 0 failed.

NOTE: lands CREDENTIALS + auth only. Scrobble-on-play (a daemon task on the
CoreEvent bus: update_now_playing on TrackStarted, scrobble past
scrobble_delay_secs, ListenBrainz submit + offline queue) is the next slice —
it wires the playback driver. The setup-TUI Scrobbler screen is also pending.

cc
2026-07-19 19:11:43 -06:00
vicrodh 7fc292de27 feat(qbzd): add playlist recommendations — POST /api/reco/playlist (CONSOLE)
Enables QBZ's Suggested-Songs engine headlessly and Slint-free. The
artist-vector store is a CACHE the engine builds on demand from MusicBrainz +
Qobuz — no listening history needed, so the personalized (history-based)
recommendations stay out of scope (they would need play-recording plumbing).

- Cargo.toml: qbz-reco path dep (already in-tree via qbz-core; direct dep only
  exposes the ArtistVectorStore name).
- daemon.rs: open ArtistVectorStore::open_at(daemon-data-root) after core init
  and set_artist_vectors — mirrors qbz/src/auth.rs, session-independent,
  best-effort (suggestions still work un-cached if the open fails).
- api/reco.rs: POST /api/reco/playlist {playlist_id} (server seeds the engine
  with the playlist's distinct artists + excludes its tracks) | {artists,
  exclude} | {limit}. generate_playlist_suggestions; auth-gated. Documents the
  single-thread cost (MusicBrainz-resolve is rate-limited).
- cli/reco.rs: `qbzd reco playlist <ID> [--limit --ids --json]`; --ids emits
  suggested track_ids (pipe into `queue add -` / `playlist add`).
- api/mod.rs: P1_ROUTES 23->24; route + test. main.rs: Reco subcommand group.

Verified: cargo test -p qbzd --bin qbzd -> 274 passed, 0 failed.

cc
2026-07-19 16:56:49 -06:00
vicrodh a1fbbafc79 feat(qbzd): ratatui setup TUI, six screens with explicit-save semantics
Implements owner requirement 2 (03-setup-tui.md): `qbzd setup`, a six-screen
raspi-config-grade configurator over the daemon's real stores — zero new
persistence. ratatui 0.30 (IV5: unifies on the lock's unicode-width 0.2.2;
0.29 forbidden). A synchronous event loop on a blocking thread; disk/HTTP only
at screen entry, `r`, save and the immediate actions, on workers with a spinner
(no I/O on keystrokes). Non-tty exits 2 with the verbatim copy; alt-screen +
panic-hook restore.

Screens: Account (T5 login engine; inline masked token paste, suspend-and-run
browser login, logout confirm), Audio (the §3.2.3 constraint matrix + 7
cascades verified 1:1 vs the desktop code, device picker re-deriving the
§3.2.2 grouping/BP-badge rules, DSD guard), Playback (ask->fallback rule,
infinite-radio read-only), QConnect (startup/name/volume_mode, live preview),
Network (whole-file qbzd.toml rewrite preserving every key), Import/Export
(pure renderer of the T12 bundle plan; interactive device re-pick; default-off
auth gate). Saves reuse T11 write_one + reload/status classification (§4.3);
dirty marker + Save/Discard/Stay. All user-facing strings centralized.

cc
2026-07-14 20:42:42 -06:00
vicrodh 196f167873 feat(qbzd): copy QConnect glue with TODO(converge) markers and daemon trait impls
Real-volume join report and reconnect credential re-resolution are daemon-copy
fixes; desktop call sites untouched (OD2).

cc
2026-07-14 17:47:33 -06:00
vicrodh 66c9acfa86 feat(qbzd): login via one-shot nonce-bound listener, paste and token paths
Port the desktop system-browser OAuth (crates/qbz/src/auth.rs) into the daemon
CLI with three daemon-specific changes: the one-shot listener binds an ephemeral
port (never the control-API port, D6), a CSPRNG state nonce binds the callback,
and the command only validates + persists the token into the daemon root then
best-effort nudges a running daemon. Adds validate_token / login_browser /
login_paste / login_with_token_arg / nudge_reload plus logout; no email+password
surface anywhere. cli/copy.rs carries the verbatim 02 §2.2 timeout, success and
logout strings.

cc
2026-07-14 15:13:07 -06:00
vicrodh 02c2dba68d feat(qbzd): scaffold slint-free qbzd crate with clap verb tree and CI gate
IV6 build peak: 795 MB max RSS (summed across the cargo-capped build's
process tree via /proc VmRSS polling; /usr/bin/time -v is not installed
on this box). aws-lc-rs in graph: 0 (single rustls provider: ring
v0.17.14 throughout).

cc
2026-07-14 12:53:09 -06:00
vicrodh 47db92ff92 chore: remove qbzd headless daemon and its client integration
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
2026-05-25 15:08:14 -06:00
vicrodh 64a42fd156 fix(qbzd): use SetState.next_track for auto-advance + periodic state reports
When iOS controls via QConnect without sending a queue, the local
QbzCore queue is empty so auto-advance fails. Now we extract next_track
from each SetState command, store it in DaemonCore, and the orchestrator
uses it as a fallback when the local queue is empty.

Also adds a periodic state reporter (every 2s) so iOS knows the
renderer's actual state (playing/paused/stopped + position). Without
this iOS thinks the daemon is still playing after a track ends.

Bump version to 0.1.9.

cc
2026-04-10 00:42:02 -06:00
vicrodh 4112cc77bd fix(qbzd): materialize QConnect remote queue for auto-advance
When QConnect controls the daemon, the local QbzCore queue is empty so
the orchestrator's auto-advance fails. Now on QueueUpdated, fetch all
tracks via get_tracks_batch and populate the local queue, mirroring
desktop's materialize_remote_queue. Bump version to 0.1.8.

cc
2026-04-09 23:22:20 -06:00
vicrodh fc83ad6cc8 chore(qbzd): bump version to 0.1.7
cc
2026-04-09 22:49:48 -06:00
vicrodh 82ea7517c8 chore(qbzd): own version 0.1.6, decoupled from workspace
cc
2026-04-09 14:45:20 -06:00
vicrodh ca9ef961cb fix(qbzd): full QConnect protocol + createToken Content-Length fix
QConnect now implements the full registration protocol:
1. Connect WebSocket
2. Bootstrap: CtrlSrvrJoinSession + AskForQueueState
3. Event loop listening for SESSION_STATE
4. Deferred renderer join with device_info
5. State/volume/quality reports

Fix 411 Length Required on qws/createToken (missing Content-Length: 0).
Fix renderer command handler to execute play/pause/stop/seek/volume.

cc
2026-04-09 12:00:18 -06:00
vicrodh eade143342 feat(qbzd): TUI setup wizard with ratatui (Phase 1.5)
Interactive menu-based wizard accessible via 'qbzd setup':
- Audio Backend & Device: lists backends, devices, saves selection
- Cache & Resources: instructions for qbzd.toml config
- Integrations: ListenBrainz token setup instructions
- QConnect: config instructions

Full ratatui TUI with keyboard navigation (up/down/enter/q).
Individual sections runnable via 'qbzd setup --section audio'.

Audio section is fully functional: enumerates backends and devices,
saves selection to audio_settings.db.

cc
2026-04-09 05:44:54 -06:00
vicrodh e5539132da feat(qbzd): QConnect headless integration
HeadlessQconnectSink routes QConnect events to the daemon event bus.
Fetches QWS JWT credentials from Qobuz API, connects via WebSocket.

The daemon now registers as a Qobuz Connect renderer — controllable
from the Qobuz mobile app and web player. QConnect starts
automatically after login if enabled in config.

cc
2026-04-09 05:39:34 -06:00
vicrodh e0301b6a29 feat(qbzd): MPRIS playback updates + status subcommand
Playback loop now updates MPRIS state (playing/paused/position)
on every emit cycle so media controls always reflect current state.

qbzd status: queries running daemon via HTTP and prints status.
Shows logged_in state, port, and daemon info.

cc
2026-04-09 05:26:37 -06:00
vicrodh f9d6e551e2 feat(qbzd): LAN CORS, headless MPRIS, security model
LAN-only CORS: allows any origin on local network for qbz-control PWA.

MPRIS via souvlaki (D-Bus, no window): play/pause/toggle/next/prev/stop
media keys work out of the box on any Linux desktop or headless system
with D-Bus. Metadata and playback state updates available via helpers.

Security model finalized:
- LAN: open access, no auth needed (private IP check)
- WAN: completely blocked (403 for non-private IPs)
- Remote: QConnect only (cloud, Qobuz-managed auth)

cc
2026-04-09 05:04:09 -06:00
vicrodh aacdeabe07 feat(qbzd): login subcommand, library scan, service files (68 endpoints)
qbzd login: interactive OAuth via system browser. Opens browser,
captures callback, exchanges code, saves token to keyring. Same
flow as desktop app. Daemon auto-logins on next start.

Library scan endpoint (POST /api/library/scan): discovers audio
files in registered folders. Full metadata extraction deferred
until MetadataExtractor moves to qbz-library crate.

Service files: systemd, OpenRC, runit + example qbzd.toml config.

cc
2026-04-09 04:52:58 -06:00
vicrodh 8fc6111782 feat(qbzd): mDNS discovery + registration for LAN pairing
Daemon side: registers _qbz._tcp.local mDNS service on startup
so desktop apps can discover it on the LAN. Uses hostname as
service name (configurable via qbzd.toml).

Desktop side: new qbzd_discovery.rs module with Tauri commands:
- v2_qbzd_start_discovery: browse _qbz._tcp services
- v2_qbzd_stop_discovery: stop browsing
- v2_qbzd_get_devices: list discovered daemons

Uses same mdns-sd crate already used for AirPlay discovery.

cc
2026-04-09 04:30:08 -06:00
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 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