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
This commit is contained in:
3 files changed
+207
-1
No files matched your search
@@ -112,6 +112,13 @@ pub async fn run(roots: ProfileRoots, cfg: QbzdConfig, warns: Vec<String>) -> Re
|
||||
// clone, so it is aborted+joined ahead of `drop(booted)` (#521 ordering).
|
||||
let queue_persist = spawn_queue_persist(booted.runtime.clone(), booted.bus.subscribe());
|
||||
|
||||
// 10c. Scrobble-on-play (CONSOLE): a CoreEvent-bus subscriber that sends
|
||||
// "now playing" on TrackStarted and scrobbles once past the Last.fm
|
||||
// threshold, to whichever of Last.fm / ListenBrainz is connected +
|
||||
// enabled in the scrobbler store. Holds NO Arc<AppRuntime>, so it sits
|
||||
// outside the #521/§8.2 ordering — aborted for a clean shutdown below.
|
||||
let scrobbler = crate::scrobble_engine::spawn(roots.clone(), booted.bus.subscribe());
|
||||
|
||||
// 11. HTTP serve (02 §3) on the already-bound socket. `ApiState` carries a
|
||||
// second read-only audio-store connection (WAL) for the status audio
|
||||
// block, the tokio handle for the async queue read, and the opt-in
|
||||
@@ -187,6 +194,9 @@ pub async fn run(roots: ProfileRoots, cfg: QbzdConfig, warns: Vec<String>) -> Re
|
||||
// `Arc<AppRuntime>` clone alive past `drop(booted)` (#521 ordering).
|
||||
queue_persist.abort();
|
||||
let _ = queue_persist.await;
|
||||
// Stop the scrobble-on-play subscriber (holds no Arc<AppRuntime>; order-free).
|
||||
scrobbler.abort();
|
||||
let _ = scrobbler.await;
|
||||
// Final full session save (queue + position) now that playback is quiesced.
|
||||
playback_driver::save_session_now(booted.runtime.as_ref()).await;
|
||||
// The background auth-retry task also holds an Arc<AppRuntime> clone — abort
|
||||
|
||||
Reference in new issue
Block a user