Skip to content

feat(lms-connect): LMS glue feature -- event dispatch to LMS, streaming pipe, token helpers#27

Closed
stiefenm wants to merge 11 commits into
michaelherger:spottyfrom
stiefenm:pr/lms-glue
Closed

feat(lms-connect): LMS glue feature -- event dispatch to LMS, streaming pipe, token helpers#27
stiefenm wants to merge 11 commits into
michaelherger:spottyfrom
stiefenm:pr/lms-glue

Conversation

@stiefenm

@stiefenm stiefenm commented May 21, 2026

Copy link
Copy Markdown

Summary

Add the lms-connect feature: LMS glue for Spotify Connect, implementing event dispatch
to Lyrion Music Server via JSON-RPC, a streaming pipe output mode, and token helpers
for binary-assisted OAuth token refresh.

Background

The Spotty LMS plugin uses the spotty binary as a Connect endpoint receiver. This PR
adds a dedicated lms-connect cargo feature that gates all LMS-specific code behind a
compile-time flag, keeping the base librespot build clean for non-LMS users.

When built with --features lms-connect, the binary accepts --lms <url> and
--player-mac <mac> to connect to an LMS instance. It then dispatches PlayerEvent
notifications (TrackChanged, PlaybackStart, PlaybackStop, VolumeChanged) to LMS via
HTTP JSON-RPC, allowing the LMS plugin to control playback in real time.

The --keymaster-token flag adds binary-assisted OAuth token refresh: when the Perl
PKCE refresh token expires, the LMS plugin can delegate token acquisition to the binary,
which has a persistent Spotify session.

Changes

Cargo.toml / Cargo.lock

  • New lms-connect feature flag (off by default)
  • Add tokio features net and io-util for async LMS HTTP client

src/main.rs

  • Add CLI flags: --lms, --player-mac, --keymaster-token, --get-token, --save-token
  • Branch on lms-connect feature at startup

src/spotty.rs

  • Add lms_connect module under #[cfg(feature = "lms-connect")]

src/lib.rs (new: src/spotty.rs lms_connect submodule)

  • LMS struct: HTTP client for LMS JSON-RPC notifications
  • ConnectNullSink: audio backend that discards audio (pure control-plane mode)
  • dispatch_event: maps PlayerEvent to LMS JSON-RPC calls
  • --get-token / --save-token: token file persistence

Pre-built Binaries

Pre-built binaries for this PR are available at:
https://github.com/stiefenm/librespot/releases/tag/spotty-v2.1.0-lms-glue-preview

Platform File Status
Linux x86_64 spotty-v2.1.0-x86_64-linux Built and tested
Linux aarch64 spotty-v2.1.0-aarch64-linux Cross-compiled (musl, static); architecture verified; runtime test on ARM64 hardware pending

Dependency Notes

This PR is independent of the Spotty-Plugin PR series (librespot-org#215-librespot-org#222) on a git level.
The companion Plugin PRs (librespot-org#220-librespot-org#222 on michaelherger/Spotty-Plugin) implement the Perl side
that invokes the --lms mode introduced here.

The lms-connect feature is gated at compile time; existing spotty feature builds
are unaffected.

Tested On

  • Lyrion Music Server 9.2.0 (build 1778040950) on Debian Linux x86_64
  • Perl 5.38.2 (x86_64-linux-gnu-thread-multi)
  • Rust 1.87 (MSRV: 1.85)
  • spotty binary v2.1.0 / librespot 0.8.0
  • x86_64 (--check): {"keymaster-token":true,"lms-auth":true,"save-token":true,"version":"2.1.0",...}
  • x86_64 (Connect): Spotify app on phone sees LMS as endpoint; TrackChanged events
    dispatched to LMS; Play/Pause controls work
  • aarch64: ELF 64-bit ARM64, statically linked, 6.9 MB; runtime test on Raspberry Pi
    pending (Pi currently running prior binary)

stiefenm and others added 11 commits May 21, 2026 07:48
Adds the tokio runtime features needed by the LMS-glue layer's TCP
notifier (TcpStream::connect + AsyncWriteExt::write_all). Also adds
serde_json as an explicit top-level dependency (it is already a
transitive workspace dep via librespot-core, but the LMS::notify
JSON body builder needs it as a direct dep).
Introduces a new module `librespot::spotty` (gated by the `lms-connect`
feature) that holds the LMS-glue layer:

- `pub const VERSION = "2.1.0"` — the version label the Spotty-Plugin
  helper-check regex expects.
- `LMS` struct with `host_port`, `player_mac`, `auth`, and a shared
  `Arc<AtomicBool>` for the suppress-next-volume flag.
- `ConnectNullSink` — a real-time-rate-limited audio sink that
  consumes decoded PCM at SAMPLE_RATE and discards it. Used in
  Connect-receiver mode where LMS owns the audio path. Critically,
  `stop()` does NOT exit() the process (unlike StdoutSink) so the
  daemon survives track transitions.

The handle_player_event dispatcher and the notify TCP transport land
in the next commit so each commit reads as a single concern.

Written from scratch against librespot-org HEAD's audio-backend
trait surface (Sink::start/stop/write with SinkResult).
Implements LMS::handle_player_event (the dispatcher) and LMS::notify
(the JSON-RPC TCP transport).

Wire vocabulary — 5 commands the Phase 8 plugin handler must match:
- start  : new track playing (None -> Some transition)
- change : track id changed (replaces previous)
- stop   : Paused or Stopped collapse here (NOT `pause`)
- volume : 0-100 percent, suppressed once after SessionConnected
- seek   : position in seconds (3 decimals)

Same-id Playing re-emits (post-seek, buffer-underrun) are no-ops so
LMS doesn't see a flood of redundant start events. SetQueue and other
HEAD-only variants fall through the wildcard arm.

Transport: one-shot tokio::net::TcpStream + raw HTTP/1.0 POST to
`/jsonrpc.js` on the configured LMS host:port. Failures are logged
at WARN; the daemon must never panic on a transient LMS outage.
Optional --lms-auth HTTP-Basic header is sent verbatim (caller-encoded).
Adds the wiring half of the LMS-glue layer:

- Three new long-only CLI flags (gated by lms-connect feature):
  --lms <host:port>, --lms-auth <base64>, --player-mac <mac>.
- A --check flag that prints the BIN-03 helperCheck-format header
  + capabilities JSON and exits — required by Spotty-Plugin's
  Helper::helperCheck regex ("^ok spotty v2.1.0 - using librespot ...").
- ConnectNullSink::open is passed DIRECTLY as the sink builder to
  Player::new under #[cfg(feature = "lms-connect")]. The
  audio_backend::find registry is NOT mutated; no new backend name is
  registered; --backend parsing is untouched (W6-locked direct-pass).
- A Tokio task that pumps the PlayerEvent channel into
  LMS::handle_player_event, only when both --lms and --player-mac
  are present (LMS::is_configured() == true).

Non-feature builds fall through to the upstream (backend)(device,
format) closure unchanged, so the lms-connect=off path remains pure
librespot-org HEAD.
Add four missing CLI flags required by Spotty-Plugin's OAuth credential
save flow (Settings/Callback.pm:358):
  --get-token     authenticate with --access-token + save credentials.json
  --save-token F  same, additionally write credentials to file F
  --client-id     override Spotify client ID for the session
  --scope         accept OAuth scopes (informational with --access-token)

The --check handler already claimed save-token:true but the flags were
never implemented, causing "Unrecognized option: get-token" at runtime.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…fresh

When the PKCE OAuth refresh token expires or is revoked by Spotify,
the Plugin can now recover automatically by asking the binary for a
fresh bearer token via login5 (using stored credentials.json).

Adds --keymaster-token CLI flag that connects to Spotify using cached
credentials, retrieves an access token via the login5 auth flow, and
outputs it as JSON to stdout. Also registers keymaster-token: true in
the --check capabilities JSON so the Plugin can gate on availability.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
TrackChanged was previously ignored ("no LMS equivalent"), but it fires
when librespot loads a new track via Spirc command — e.g. when the user
taps a track further down in the playlist in the Spotify app. Without
dispatching it, the Perl-side Connect handler never learned about
playlist jumps, causing LMS to keep playing the old track.

Now TrackChanged updates the current_track cursor and emits a `change`
(or `start` on first track) event, exactly like the Playing handler.
When Playing fires later for the same track_id, it becomes a same-id
no-op.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Regenerate Cargo.lock after cherry-pick series
- Remove duplicate serde_json = "1" from Cargo.toml (already present as serde_json = "1.0")
- Remove setup_logging() call in KEYMASTER_TOKEN handler (fn only exists in debug builds)
…ly in spotty.rs

When spotty.rs is compiled as a library module (pub mod spotty in lib.rs
under cfg(feature = lms-connect)), 'use librespot::core::*' becomes a
circular reference since the lib IS librespot. Switch to the direct
sub-crate paths (librespot_core::*, librespot_playback::*) which are
available as workspace dependencies regardless of context.

Also fix to_id() calls: SpotifyUri::to_id() returns Result<String, Error>
in Herger's fork. Add .unwrap_or_default() for graceful handling of
parse failures. Affects PlayerEvent::Playing and PlayerEvent::TrackChanged
handlers in the lms_connect dispatcher.

Required for: cross-compilation with --no-default-features --features default-linux,lms-connect
Revealed by: aarch64-unknown-linux-musl cross-compile build

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 21, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@stiefenm has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 43 minutes and 48 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f4bfe1ac-1111-4134-bba0-4200158c6a79

📥 Commits

Reviewing files that changed from the base of the PR and between b0af1fb and 845871a.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (5)
  • CHANGELOG.md
  • Cargo.toml
  • src/lib.rs
  • src/main.rs
  • src/spotty.rs
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@stiefenm

Copy link
Copy Markdown
Author

Closing in favor of consolidated PRs as discussed — will resubmit as focused issues + PRs.

@stiefenm stiefenm closed this May 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant