Commit Graph
55 Commits
Author SHA1 Message Date
vicrodh d58fdf45b0 fix(log): never rotate a log file another live process is writing; qbzd logs to qbzd.log
open_log_file_at renamed the live file to .prev on every open, so a second
qbz launch (the #749 two-line qbz.log) or a qbzd in the same directory took
the running process's log away. Writers now hold a shared advisory lock on
<name>.lock; an opener that cannot take it exclusively appends instead of
rotating. The daemon gets its own qbzd.log.

cc
2026-09-13 03:36:50 -06:00
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 a7629dc962 feat(log): report fatal signals instead of dying as a bare status
The packaged AppDir has an intermittent SIGSEGV during startup, in the
window between QGuiApplication construction and the QML engine coming up,
with the tokio workers, the audio thread, the loudness analyzer, the
visualizer producer and connectivity all running beside it. It hit twice
on 2026-09-08 on unrelated commits: the v2.1.0 tag's own release-flatpak
attempt 1, which then passed on a re-run of the identical commit, and the
#745 bugfix branch. Both left the same evidence - "status -11" and a log
that stops mid-startup - which is nothing to act on, and a user who hits
this has no re-run to reach for.

The process now reports its own death. A handler for SIGSEGV, SIGBUS,
SIGILL, SIGFPE and SIGABRT writes the signal, the faulting address, the
thread and a backtrace to stderr and flushes the file sink, so the next
occurrence names a frame instead of costing another round trip. It runs
on a dedicated signal stack so a stack-overflow fault can still report,
latches against re-entry, and arms alarm() BEFORE capturing anything:
taking a backtrace allocates and takes loader locks, so a fault inside
malloc could otherwise deadlock the handler and turn a crash into a hang.

The signal is re-raised with the default disposition restored, so the
wait status stays the signal and CI, the smoke harness and any core dump
still see a crash. The integration test asserts exactly that pair on a
child segfaulted on purpose, and it is a named gate in test-crates and
its local mirror.

qbzd installs it too: a headless daemon that dies of a signal leaves even
less behind than the GUI.

cc
2026-09-08 09:51:12 -06:00
vicrodh db84e733d6 fix(log): compact consecutive repeated diagnostics
Group identical redacted diagnostics across stderr, file and the live ring. Flush repeat summaries on visible transitions, exports and shutdown while keeping periodic counts for sustained bursts.

cx
2026-09-05 18:36:51 -06:00
vicrodh 2b7d804f09 feat(auth): member accounts are valid sessions; typed entitlements
Qobuz answers `/user/login` with 200 for an account without an active
subscription and the web player signs it in as a "Qobuz member" with its
favorites, playlists, purchases and 30-second previews. QBZ threw that
session away: `parse_login_response` returned `IneligibleUser` whenever
`credential.parameters` was empty (verified 2026-09-02 with a lapsed
account). The gate was ours, not Qobuz's.

The parser now reads the entitlement flags (`lossy/lossless/hires/
hires_purchases/offline/mobile_streaming`) into a typed `Entitlements`
on `UserSession`, labels a member "Member", and keeps `subscription.
end_date`/`offer` as information (the embedded Qobuz fixture has a past
end_date with every flag true: the flags are the verdict, the date is
not). `StreamUrl` gains `sample`, the flag Qobuz sets when it serves a
preview instead of the track. `IneligibleUser` stays in the enum for its
existing matchers; nothing emits it from this path any more. Persisted
pre-member sessions still load (`serde(default)`).

cc
2026-09-02 07:27:38 -06:00
vicrodh c927eb9c9c feat(qbzd): harden qconnect ownership lifecycle
cx
2026-08-30 22:22:39 -06:00
vicrodh c3556fe3ec feat(qbzd): expose ALSA hardware volume controls
Carry hardware-volume capability, selection, diagnostics, and status through the daemon API, CLI, and TUI while preserving fail-closed behavior on unsupported devices.

cx
2026-08-30 09:14:39 -06:00
vicrodh 3e060d6535 feat(qbzd): listen log subscriber beside the scrobbler
A CoreEvent-bus sibling of scrobble_engine WITHOUT its enabled gate: the
log is local and on by default, so a headless streamer finally records
history. TrackStarted opens a row and closes the previous one with an
inferred reason (natural within 2 s of the end, skip otherwise — nobody
emits TrackEnded); PositionUpdated feeds the accumulator; Stopped closes
with the same inference. origin_id = qbzd:<hostname>; the shutdown path
aborts the task and closes the open row.

cc
2026-08-28 20:30:43 -06:00
vicrodh 6587201a1c fix(qbzd): log loudly when the artist-vector store fails to open
The boot comment claimed suggestions work un-cached without the store;
suggestions.rs and builder.rs both answer "No active session" when it
is None. Warn on failure and say what the symptom will be.

cc
2026-08-28 19:54:47 -06:00
vicrodh 2df2dafe67 fix(audio): make ALSA transitions capability-aware
cx
2026-08-28 02:58:04 -06:00
vicrodh 5bae442864 fix(qbzd): enforce quality cap for QConnect
cx
2026-08-27 19:37:00 -06:00
vicrodh e109e88ec2 merge: integrate qbzd event hooks from PR #700
cx
2026-08-27 19:26:13 -06:00
Filippo VicentiniandClaude Fable 5 ee1bbf9b76 feat(qbzd): run a user hook script on daemon events (hooks.script)
Adds an event-hook facility for headless integrators (moOde, Volumio,
DIY audio boxes): 'qbzd settings set hooks.script /path/to/script' (or
the QBZD_HOOK env var) names an executable the daemon forks once per
daemon event with the event described in QBZ_* environment variables --
the same push-integration contract pleezer (--hook) and shairport-sync
(run_this_...) established, so a box can stop its local player when a
Qobuz Connect session starts and restore state when it ends, without
keeping a poller or SSE client alive.

Getting there required closing a gap on the CoreEvent bus: the playback
variants (TrackStarted, PlaybackStateChanged, PositionUpdated,
VolumeChanged) were defined and consumed -- mpris.rs keeps its widget
fresh from them, scrobble_engine.rs times scrobbles by them -- but
nothing ever emitted them, so those consumer arms were dead and
/api/events (SSE) / 'qbzd watch' carried no playback traffic. A new
events_bridge task wakes on the driver's existing ReportEdge pulse
(plus a 2 s poll fallback for track-id-clearing stops), dedups against
what it last published, and sends the deltas to the bus. This revives
live MPRIS updates and scrobble timing as a side effect.

Also emitted now: QconnectSessionChanged (new CoreEvent variant) from
the qconnect lifecycle latches -- off/connecting/connected/retrying/
exhausted -- and PlaybackError from the driver's stream-error latch, so
integrators can react to a busy audio device at play time.

The hook dispatcher never waits for the script inside the recv loop
(each event forks detached, a reaper task collects the exit status) and
the LoggedIn session payload (auth token) is never forwarded to child
environments. hooks.script validates as absolute-or-empty; the setting
lives in daemon_prefs.json and is deliberately NOT part of the settings
bundle (a machine-local path).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 01:57:56 +02:00
vicrodh a69491a49e feat(player): re-wire the low-memory profile into qbz/qbzd (#660)
The system_capabilities profiler (Normal vs LowMemory, built for #331)
lost all its consumers in the Tauri removal; 2.0.2 shipped it as dead
code and qbzd got OOM-killed (exit 137) on 1 GB Pi hosts at track
transitions. Phase A wiring:

- Frontends read memory_profile() at startup and push it down via the
  new player memory_tuning (initial-buffer cap, L1 budget, low-memory
  gate) before the Player is created.
- L1 audio cache sized from the profile (400 MB Normal / 50 MB
  LowMemory; was hardcoded 400 MB).
- Oversized tracks (>L1/4 = 12.5 MB on LowMemory) skip the promotion
  clone and the parallel CMAF RAM accumulator entirely; the finished
  track is streamed to the L2 disk cache in 1 MB chunks straight from
  the playback buffer — no second in-RAM copy. Gapless degrades to a
  small gap for those tracks (a gap beats an OOM kill).
- Stream buffer preallocates total_size up front (try_reserve_exact,
  1 GiB cap, dynamic fallback) — no more ~8 doubling reallocs + memcpy
  per track (weak-CPU relief).
- New memory-pressure watchdog (15 s): critical pressure evicts the
  L1 (L2 disk kept) and halts prefetch; low pressure pauses HiRes
  prefetch (downgrades to Lossless). Spawned by both frontends; qbzd
  aborts+joins it for the #521 shutdown ordering.
- Prefetch honors the profile (depth cap, HiRes->Lossless downgrade).

Normal-profile behavior is byte-identical (every behavioral change is
gated on the LowMemory class; profile values equal the old hardcodes).

Phase B (follow-ups): tempfile spill of the live stream buffer for
giant tracks, POLLERR flood rate-limit, desktop kick_prefetch halt
check, L2-read on the play_track cache-hit path.

km
2026-07-26 23:56:39 -06:00
vicrodh 8c2dfd5727 feat(models): capture account country and language on login
parse_login_response now reads user.country_code and user.language_code
(snake_case wire names, verified against Qobuz's own embedded login
fixture) into two new serde-defaulted Option fields on UserSession, so
pre-v10 persisted sessions still load with both as None (translation
stays Auto-off). qbzd's test construction updated for the new fields.

km
2026-07-22 19:11:52 -06:00
vicrodh f51eb6dc51 fix(qbzd): publish daemon-originated queues to the Connect cloud
The daemon was queue receive-only: a CLI/TUI/MPRIS/restored queue never
reached the cloud, so controllers rendered a different queue than the
one playing. New qconnect/publish.rs ports the desktop's
sync_local_queue_if_changed with its full gate set (active-renderer,
offline-only, echo-suppress vs last-applied, per-session pushed-ids
latch, all-or-nothing admission), triggered by a debounced
CoreEvent::QueueUpdated subscriber — which also covers queue edits
while paused/stopped, unlike a transition-only hook. Live-verified: a
CLI queue move produced exactly one QueueLoadTracks push (233 tracks)
and the cloud echo was suppressed.

km
2026-07-22 09:03:43 -06:00
vicrodh 47506cb5b7 fix(credentials): derive the daemon token key without the XDG portal secret
The token encryption key mixed in the XDG portal secret (session-bus
dependent): a login run in a graphical session wrote a token an
init-started/headless qbzd could never decrypt, reported as a bare
"not logged in". PortalKey policy: desktop root keeps Session, daemon
root uses Never; one-shot migration on read (decrypt with Session,
rewrite portal-free); oauth_token_file_present_at() +
latch_undecryptable_token() so status distinguishes "no token" from
"token I cannot decrypt". Live-verified 3x (OpenRC, headless Pi,
migration path).

km
2026-07-22 09:03:07 -06:00
vicrodh 7f2898396d feat(qbzd): live event stream — GET /api/events (SSE) + qbzd watch
Add a Server-Sent Events feed so a client (a plasmoid, a bar applet, a script)
reacts to playback/queue/library changes without polling — the last CONSOLE
surface.

Server (api/sse.rs): `GET /api/events` streams one SSE frame per CoreEvent
(`event: <type>` + the tagged `data:` JSON). Because the control-plane serve
loop is single-threaded, an open stream would starve it, so the route is moved
onto its OWN thread (Request is Send) after the same origin/token gate every
route runs; the bus is drained with `blocking_recv()` there. Bulky/internal
events (search payloads, loading/download/navigation hints, diagnostics) are
filtered out; everything else — playback, queue, volume, auth, favorites,
playlists, errors, device changes — is emitted. ApiState gains the bus sender.

Client (`qbzd watch`): a bespoke reqwest client with NO read timeout (only the
connect is bounded); default output is newline-delimited JSON (one data payload
per line, `qbzd watch | jq`), `--raw` passes the SSE frames through verbatim.
P1_ROUTES → 26 with the route-table test updated (caller: `qbzd watch`).

283 tests green.

cc
2026-07-19 22:58:22 -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 1c5c44b904 feat(qbzd): scrobble-on-play engine — Last.fm / ListenBrainz (CONSOLE)
The daemon now scrobbles what it plays. A CoreEvent-bus subscriber
(scrobble_engine, spawned in run() next to queue-persist):
- on TrackStarted -> "now playing" to each active provider (LastFm
  update_now_playing / ListenBrainz submit_playing_now);
- scrobbles ONCE when the position crosses the threshold from
  qbz_app::scrobble_timing::scrobble_delay_secs (Last.fm's half-or-4-min rule)
  -> LastFm scrobble / ListenBrainz submit_listen, best-effort + logged;
- re-reads the canonical ScrobblerSettingsStore on each track, so `qbzd scrobble
  login/enable/disable` take effect on the next track with no reload signal;
- holds no Arc<AppRuntime>; aborted on shutdown alongside queue-persist.

Album is dropped when empty / "Unknown Album". Pure helpers (due, album_opt)
are unit-tested. The ListenBrainz offline queue (ListenBrainzCache) is a
follow-up — this slice submits best-effort.

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

cc
2026-07-19 19:22:50 -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 e6cc082669 feat(qbzd): default control-plane and login binds to 0.0.0.0 for LAN-first posture
cc
2026-07-15 11:32:44 -06:00
vicrodh 722a0207ec fix(qbzd): latch network.online from real network-class outcomes
cc
2026-07-14 21:59:50 -06:00
vicrodh 47081b7666 fix(qbzd): drop qconnect control handle before device release in shutdown
cc
2026-07-14 21:54:55 -06:00
vicrodh a864aa8d55 feat(qbzd): settings reload endpoint and settings/qconnect/config CLI verbs
Route table complete: 17 P0 routes, CI-counted.

cc
2026-07-14 19:02:10 -06:00
vicrodh adccb98e76 feat(qbzd): report tick, daemon device identity, volume modes and queue persistence
cc
2026-07-14 18:14:52 -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 dd63b18ae9 fix(qbzd): treat empty [server] token as unset
cc
2026-07-14 16:10:34 -06:00
vicrodh 286cb51957 feat(qbzd): tiny_http control plane, Origin shield + opt-in token, ping/info/status and CLI diagnostics
cc
2026-07-14 15:59:17 -06:00
vicrodh 297bbad326 feat(qbz-app): headless playback driver with pure plan_tick and thin IO shell
cc
2026-07-14 14:09:22 -06:00
vicrodh 4d0f52138a feat(qbzd): headless runtime bring-up with daemon-root stores and credential restore
Adds additive _at credential fns (crypto/format untouched), DaemonAdapter
broadcast bus, numbered boot per 01 §8.1 incl. the #521 shutdown pair.

cc
2026-07-14 13:40:49 -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 0461c36912 security: address CodeQL alerts from the v1.2.5 scan
Triaged the 8 open CodeQL alerts raised on the 1.2.5 merge commit.

Critical (1) — real, fixed:

  * .github/workflows/release-updater-manifest.yml:29 — the resolve
    step interpolated `${{ github.event.workflow_run.head_branch }}`
    directly into a shell heredoc. `head_branch` on `workflow_run` is
    attacker-controllable (any outside contributor can push a branch
    whose name embeds a payload). Route the value through an env var
    (HEAD_BRANCH) and validate the shape with a strict regex before
    letting it flow into shell or $GITHUB_OUTPUT.

Warnings (2) — real, fixed:

  * .github/workflows/build-qbzd.yml had no `permissions:` block, so
    every job inherited the repo's default GITHUB_TOKEN scope. Add a
    top-level `permissions: contents: read`; the existing `release`
    job keeps its own `contents: write` override for publishing
    artifacts.

Warnings (3) — real, low severity, mitigated:

  * crates/qbzd/src/{daemon.rs,session.rs} logged `user_id` at info
    level (CodeQL rust/cleartext-logging). Demoted the three lines to
    `log::debug!` so default-level logs stay free of the numeric user
    id, with a comment noting RUST_LOG=debug re-enables them for
    diagnostics.

Warnings (2) — false positives, dismissed in GitHub:

  * crates/qbzd/src/login.rs:103 — `println!` to stdout inside the
    interactive `qbzd login` subcommand; login confirmation UX, not a
    persistent log. (alert #63 dismissed, reason: false positive)
  * crates/qbzd/src/qconnect.rs:540 — `session_uuid` is an ephemeral
    per-connection identifier QConnect exchanges in cleartext itself;
    not a credential or token. (alert #65 dismissed, reason: false
    positive)

cc
2026-04-15 16:10:06 -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 c2e8285dfa fix(qbzd): prevent orchestrator race on play-track + QConnect debug logging
play-track/play-album set skip_auto_advance flag before stop() to
prevent the orchestrator from auto-advancing during the download
window. This fixes the 'track starts then immediately stops' bug.

Added QConnect event type logging to debug why SESSION_STATE never
triggers the deferred renderer join.

cc
2026-04-09 14:30:32 -06:00
vicrodh 39142aad3b feat(qbzd): proper playback orchestrator with gapless + repeat modes
Replaces naive auto-advance polling with full orchestrator matching
desktop behavior:

Gapless: when player signals gapless_ready (5s before track end),
downloads next track and calls play_next() for seamless transition.
Handles repeat-one (queue same track), defensive same-track check,
and upcoming queue peek (same as desktop's setGaplessGetNextTrackId).

Auto-advance: on track end, advances queue respecting repeat mode.
Repeat-one replays same track. Repeat-all wraps queue. Off stops
at queue end.

Shared download_track() helper used by play-track, play-album, and
orchestrator to avoid code duplication.

cc
2026-04-09 13:51:23 -06:00
vicrodh 30bc325f68 feat(qbzd): auto-advance queue + play-album endpoint
Auto-advance: background task monitors when track ends and
automatically downloads and plays the next track in the queue.
Polls every 500ms. Queue exhaustion logs and stops.

Play-album: POST /api/playback/play-album sets the queue with
all album tracks and starts playing from the specified index.

cc
2026-04-09 13:30:15 -06:00
vicrodh e9fe6b6245 fix(qbzd): QConnect form-encoded createToken + mDNS hostname + response parsing
3 fixes:
1. qws/createToken needs form body (jwt=jwt_qws, user_auth_token_needed,
   strong_auth_needed) not empty body. Was getting 400 Bad Request.
2. mDNS hostname must end with '.local.' for service registration.
3. Response parsing: jwt_qws.endpoint + jwt_qws.jwt, not tokens array.

All 3 patterns copied from working desktop implementation in
qconnect_service.rs.

cc
2026-04-09 12:29:07 -06:00
vicrodh 457c628318 feat(qbzd): implement play-track endpoint (download + play)
POST /api/playback/play-track accepts track_id + quality, downloads
the audio stream from Qobuz, and feeds it to the Player.

This was the missing piece — without this, the daemon could search
and manage queue but couldn't actually play anything.

cc
2026-04-09 12:05:16 -06:00
vicrodh 7a9f8407a8 fix(qbzd): file-based token fallback when keyring unavailable
On headless systems without gnome-keyring/kwallet, the keyring
fails with ServiceUnknown. Now saves token to ~/.local/share/qbz/.oauth-token
(mode 0600) as fallback. Load also checks file after keyring.

This fixes the 'login works but credentials lost on restart' bug
on headless machines.

cc
2026-04-09 10:57:30 -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 b3e5be39eb feat(qbzd): MPRIS metadata from CoreEvent::TrackStarted
Spawns a dedicated event bus listener that updates MPRIS metadata
(title, artist, album, duration) when a new track starts playing.
Clears metadata on logout.

MPRIS now fully functional: media keys (play/pause/next/prev) +
metadata display (track info in system media widget).

cc
2026-04-09 05:35:52 -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 892560dff4 feat(qbzd): LAN-only access restriction, no token auth
Replaces token-based authentication with LAN-only IP restriction.
Requests from non-private IPs (not 10.x, 172.16.x, 192.168.x,
127.x, link-local, ULA) are rejected with 403.

Security model: LAN = trusted, WAN = blocked.
Remote control outside LAN is exclusively via QConnect (cloud).

cc
2026-04-09 05:00:40 -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 42a8ff5665 feat(qbzd): library management + integrations API (67 endpoints)
Library endpoints for standalone operation:
- GET /api/library/albums, artists, album tracks, search, stats
- POST/DELETE /api/library/folders (add/remove scan directories)

Integrations endpoints:
- GET/POST/DELETE /api/integrations/listenbrainz (status, connect, disconnect)
- GET /api/integrations/lastfm (status, setup deferred to TUI wizard)

ListenBrainz connects by writing token directly to the V2 cache
database (same format as desktop app). Last.fm requires OAuth
flow which will be implemented in the TUI wizard.

Total: 67 HTTP endpoints. The daemon is now fully operable
without the desktop app for all core functionality.

cc
2026-04-09 04:46:12 -06:00
vicrodh b524d62318 feat(qbzd): API tiers 2-3 — discover, playlists, favorites, labels, system (55 endpoints)
Tier 2 - Browsing & Management:
- Discover: index, playlists, featured, genres (4 endpoints)
- Favorites: get, add, remove (3)
- Playlists: CRUD + tracks + search (8)
- Extended catalog: artist page, similar, labels, tags (6)

Tier 3 - Server Management:
- System resources: RAM, cache stats (1)
- Cache clear (1)

Total: 55 HTTP endpoints covering playback, queue, search, catalog,
discover, favorites, playlists, audio settings, labels, and system.

cc
2026-04-09 04:41:49 -06:00
vicrodh 8c947c02b4 feat(qbzd): audio settings API + remote settings routing (Phase 5e)
Daemon: 5 new audio settings endpoints:
- GET /api/audio/settings (full settings)
- PATCH /api/audio/settings (partial update)
- GET /api/audio/backends (available backends)
- GET /api/audio/devices?backend= (devices for backend)
- GET /api/audio/hardware-status (current hw state)

Frontend: commandRouter gains audio settings commands that route
to daemon API when in remote mode, local invoke when local.

Total daemon endpoints: 32.

cc
2026-04-09 04:35:49 -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