Commit Graph
111 Commits
Author SHA1 Message Date
vicrodh 0b7cd6c5e3 feat(qbz-app): settings bundle engine with importer-side classification
Export/import CLI on qbzd; requirements 4 and 5 complete with zero desktop builds.

cc
2026-07-14 19:44:01 -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 6fa0428127 fix(qbzd): pin player volume to 100 percent at connect in locked volume mode
cc
2026-07-14 18:27:43 -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 05e2216acc fix(qbzd): strict queue payload parsing, history in queue response, honest race comment
cc
2026-07-14 17:19:44 -06:00
vicrodh 92c49e648b feat(qbzd): queue routes and CLI verbs with server-side track materialization
cc
2026-07-14 17:05:15 -06:00
vicrodh 393a6e01a7 fix(qbzd): serialize volume canonically in JSON responses
cc
2026-07-14 16:49:20 -06:00
vicrodh e700084f0b feat(qbzd): playback routes and transport CLI verbs with full advance ritual
cc
2026-07-14 16:35:21 -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 9597573a61 fix(qbzd): bind login nonce into the redirect path instead of relying on state echo
cc
2026-07-14 15:26:56 -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 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 18a10d14d9 fix(qbzd): warn on config-dir create/chmod failure instead of silent discard
cc
2026-07-14 13:18:30 -06:00
vicrodh d911f43856 feat(qbzd): qbzd.toml with unknown-key warning, flock instance lock, daemon_prefs store
cc
2026-07-14 13:10:08 -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 f01fc17383 feat(queue): clear-queue includeCurrent flag stops + clears now-playing
QueueManager::clear previously always preserved the current track;
plus bridge.play_index only mutates queue state, never triggers
audio. Result: when the user hit Clear Queue with nothing playing,
a stale track stayed in NOW PLAYING across app restarts.

- QueueManager::clear(keep_current: bool) — existing callers pin to
  true (legacy behavior). New test covers the false path.
- QbzCore::clear_queue / CoreBridge::clear_queue take the flag through.
- v2_clear_queue accepts an optional includeCurrent flag from the
  frontend; when true, calls bridge.stop() first so the audio pipeline
  actually stops, then wipes the current slot too.
- qconnect sync path + qbzd daemon pin keep_current=true (no behavior
  change).

cc
2026-04-19 21:17:09 -06:00
vicrodh b2fb251783 feat(queue): add source_item_id_hint to QueueTrack
Reserved for upcoming Mixtape/Collection enqueue paths to mark item
boundaries. Existing enqueue paths populate it with the track's
album_id as a safe fallback so skip-to-item commands behave
correctly even for queues built via "play album" today.

cc
2026-04-18 17:23:59 -06:00
vicrodh 6de109b3b8 refactor(labels): purge legacy /label/get stack
/label/get is gone from the Qobuz v9.7.0.3 Android app (source:
c30/b.java doesn't declare it anymore). With the Qobuz beta rollout
the server-side deprecation timer is probably running, so we yank our
dependency now.

Removed:
- LABEL_GET path constant (qbz-qobuz + src-tauri/api)
- get_label() client method (qbz-qobuz + src-tauri/api)
- get_label() wrapper in QbzCore + CoreBridge
- v2_get_label tauri command + its lib.rs registration
- LabelDetail backend type (qbz-models + src-tauri/api/models)
- commands::search::get_label (dead, was never registered)
- Unused LabelDetail import in src/routes/+page.svelte

Preserved:
- Frontend LabelDetail interface (it's a UI view-model shaped by
  LabelReleasesView, not a backend contract)
- /api/labels/{id} HTTP route on qbzd — now composes /label/page +
  /label/getAlbums internally so external HTTP clients still see the
  same { id, name, description, image, albums: { items, total, ... } }
  shape

cc
2026-04-16 22:59:31 -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 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 5182197938 fix(qbzd): prevent playback restart from server state echo
Skip redundant resume() when already playing, and ignore seek-to-zero
from server refresh echoes after renderer join. Matches desktop's
should_force_remote_track_restart guard logic.

cc
2026-04-09 23:18:26 -06:00
vicrodh fc83ad6cc8 chore(qbzd): bump version to 0.1.7
cc
2026-04-09 22:49:48 -06:00
vicrodh 3df007fe9f fix(qbzd): download and play track on QConnect renderer SetState
When receiving RendererCommandApplied::SetState with a current_track,
actually download the track audio and feed it to the player before
applying the playing state. Previously only called core.resume() which
did nothing since no audio was loaded.

Matches desktop's ensure_remote_track_loaded pattern from
qconnect_service.rs. Also adds >2s threshold for seek to avoid
redundant seeks from server echo.

cc
2026-04-09 22:48:27 -06:00
vicrodh 79a86278f2 fix(qbzd): complete device_info with device_uuid + capabilities
Missing fields that the Qobuz server needs to register a renderer:
- device_uuid: persistent UUID (generated once, saved to disk)
- capabilities: min/max audio quality + volume_remote_control
- serial_number: null (same as desktop)

Without device_uuid, the server can't identify the renderer and
won't send SESSION_STATE.

Copied from desktop's default_qconnect_device_info_with_name.

cc
2026-04-09 22:35:03 -06:00
vicrodh ffa59b339a fix(qbzd): rewrite QConnect to exact desktop pattern
Complete rewrite of qconnect.rs following desktop's
QconnectServiceState::connect() line by line:

1. Subscribe to transport events BEFORE bootstrap (was after)
2. Event loop checks InboundQueueServerEvent for SESSION_STATE
   (was checking SessionManagementEvent via sink channel)
3. Forward events to handle_transport_event AFTER inspection
4. Bootstrap called AFTER event loop starts
5. Re-bootstrap on reconnect (Subscribed + has_disconnected)

The key bug: event loop was subscribing AFTER bootstrap, missing
the SESSION_STATE response. Desktop subscribes at line 1238,
bootstraps at line 1405.

cc
2026-04-09 18:37:15 -06:00
vicrodh bea94871eb fix(qbzd): immediate renderer join in bootstrap, don't wait for SESSION_STATE
The Qobuz server doesn't always send SESSION_STATE after a controller
JoinSession (only on topology changes). This left the daemon invisible
as a renderer because deferred_renderer_join never triggered.

Now: renderer join happens immediately in bootstrap after a 1s delay,
using null session_uuid. The server should assign one. If a
SESSION_STATE arrives later via the sink channel, a second join
is attempted with the real uuid.

cc
2026-04-09 18:33:13 -06:00
vicrodh 186e79d225 fix(qbzd): QConnect renderer join via SessionManagementEvent sink
SESSION_STATE comes as QconnectAppEvent::SessionManagementEvent
through the sink, NOT as a transport InboundQueueServerEvent.
The event loop was looking in the wrong place.

Now: sink receives SessionManagementEvent, extracts session_uuid,
sends it via mpsc channel to the event loop which does the
deferred_renderer_join. This matches the desktop pattern where
TauriQconnectEventSink processes session management events.

cc
2026-04-09 18:08:34 -06:00
vicrodh 82ea7517c8 chore(qbzd): own version 0.1.6, decoupled from workspace
cc
2026-04-09 14:45:20 -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 6cf396aa8a fix(qbzd): QConnect bootstrap clears pending between commands
The second command (AskForQueueState) was failing because the
first (JoinSession) was still pending. Now clears pending slot
between commands with 500ms delay, matching desktop behavior.

Also fixed state_handle() Arc lifetime issues.

cc
2026-04-09 12:53:29 -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 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 264302fd8e feat(qbzd): implement all wizard sections (cache, integrations, qconnect)
Cache: interactive RAM detection, L1/L2 size, prefetch config.
Saves to qbzd.toml via save_default_config.

Integrations: ListenBrainz token input, saves directly to V2
cache database. MusicBrainz enable/disable toggle.

QConnect: enable/disable + device name. Saves to qbzd.toml.

All sections read current values as defaults and persist changes.
No more placeholders.

cc
2026-04-09 11:39:22 -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 c21e87e1c0 fix(qbzd): headless-compatible login with LAN IP detection
qbzd login now works on remote/headless machines:
- Binds callback server to 0.0.0.0 (not 127.0.0.1)
- Detects LAN IP via UDP socket probe for redirect URL
- Always prints URL prominently for copy-paste to any browser
- Browser on same LAN redirects back to daemon's IP:port
- Timeout increased to 300s for remote copy-paste workflow
- open::that still tried but failure is silent (headless)

cc
2026-04-09 09:02:06 -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 632b134952 feat(qbzd): full metadata scan using MetadataExtractor
Library scan now extracts metadata (title, artist, album, duration,
sample rate, bit depth) from audio files using qbz-library's
MetadataExtractor. Tracks are inserted into the library database
with full metadata.

MetadataExtractor was already in qbz-library crate — no migration
needed.

cc
2026-04-09 05:42:27 -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 a5efd6ae19 feat(qbzd): setup subcommand placeholder + config save helper
CLI now shows all 5 subcommands: login, status, token, setup, help.
Setup placeholder ready for TUI wizard implementation (ratatui).
Config save helper for persisting changes to qbzd.toml.

cc
2026-04-09 05:30:12 -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