Commit Graph
3 Commits
Author SHA1 Message Date
Filippo VicentiniandClaude Fable 5 8ec1f3c275 fix(qbzd): re-emit TrackStarted when the same track replays after a stop
The bridge's track-change dedup kept the last track id across a stop,
so stop -> play of the same track produced only PlaybackStateChanged
and consumers treating TrackStarted as now-playing (scrobbler, hooks)
missed the replay. A stop ends the current track: reset the tracker.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 02:24:21 +02:00
Filippo VicentiniandClaude Fable 5 e336d2d1db fix(qbzd): dedup bridge state changes on the mapped three-way state
A pause -> stop transition never emitted PlaybackStateChanged: the
bridge deduped on the raw is_playing boolean, and paused and stopped
are both not-playing. Track the mapped Playing/Paused/Stopped state
instead. Found by driving the hook end-to-end on a Raspberry Pi.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 02:14:08 +02: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