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
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
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
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
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
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
/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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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