spotify-cli

spotify-cli / Docs

Building on spotify-cli

Everything is open source: https://github.com/unlikefraction/spotify-cli. Reproduce a bug, patch it, open a pull request, then spotify report '<what>' --pr <url>.

Layout

crates/client   silicon-spotify-client   stateless library: models, URI/time parsing, AppleScript,
                                         spotify_player wrapper, verified control, trigger engine,
                                         backend API client; feature `runtime` adds the per-home
                                         store and daemon IPC used by the CLI and daemon
crates/cli      silicon-spotify-cli      the `spotify` command
crates/daemon   silicon-spotify-daemon   `spotify-daemon` (macOS: objc2 NSAppleScript + notifications)
src/            silicon-spotify          backend (`spotify-api`): IAM exchange, Ting, reports, telemetry
docs/           these guides (bundled into the CLI by scripts/sync-cli-docs.sh)
docs-site/      static site generator for spotify.unlikefraction.com (+ install.sh)
deploy/         backend deployment (systemd, Caddy, CloudFormation), Honeycomb and Ting setup

Use the library

silicon-spotify-client = { git = "https://github.com/unlikefraction/spotify-cli", tag = "v0.1.1" }
# no HTTP at all:
silicon-spotify-client = { git = "https://github.com/unlikefraction/spotify-cli", tag = "v0.1.1", default-features = false }
use std::time::Duration;
use silicon_spotify_client::{applescript::Osascript, control::*, player::SpotifyPlayer, timing::SeekTarget};

let script = Osascript::default();                    // or your own `applescript::Runner`
let player = SpotifyPlayer::locate(None);
let spotify = Controller {
    script: &script,
    player: player.as_ref().map_err(Clone::clone),
    strategy: Strategy::Auto,
    verify_timeout: Duration::from_millis(2500),
    launch_spotify: false,
};
let now = spotify.status()?;                          // Playback
let outcome = spotify.seek(SeekTarget::parse("50%")?)?;   // Outcome { via, fallback, playback }

The trigger engine is pure: feed trigger::Tracker::observe readings, pass the events to trigger::evaluate, deliver the returned Firings however you like (Firing::data() is the Ting payload, Firing::key() the idempotency key).

Talk to the daemon

Newline-delimited JSON over ~/.silicon-spotify/daemon.sock, one request per connection:

{"v":1,"id":"<uuid>","op":"player.status","home":"/abs/SILICON_HOME","args":{"full":true},"client_version":"0.1.0"}
{"v":1,"id":"<uuid>","ok":true,"data":{…}}
{"v":1,"id":"<uuid>","ok":false,"error":{"code":"…","message":"…","hint":"…","retryable":false}}

Ops: daemon.status, daemon.shutdown, doctor, player.{status,play,pause,toggle,next,previous,seek,volume,shuffle,repeat,like}, spotify.{launch,auth.status,auth.login}, track.info, lyrics, search, library.get, devices.{list,connect}, queue.{list,add,remove,move,clear}, playlist.{list,show,create,delete,add,remove,rename,import,fork,sync}, podcast.saved, trigger.{add,list,get,remove,clear,history,test,wait,retry}, telemetry.{record,clear}, update.{check,apply}. Argument shapes: crates/cli/src/ops.rs (the CLI is the reference client) and crates/daemon/src/*.rs. Unknown ops fail with unknown_op. The socket accepts only the same OS user.

Build and test

cargo test --workspace                        # unit + backend contract tests (fake IAM and Ting)
cargo run --example dev_backend --features dev -- 127.0.0.1:8787 /tmp/tings.jsonl
SPOTIFY_API_URL=http://127.0.0.1:8787 cargo run -p silicon-spotify-cli -- login si:dev-silicon
SPOTIFY_API_URL=http://127.0.0.1:8787 cargo run -p silicon-spotify-cli -- trigger test

The dev backend serves the real router with in-process IAM and Ting fakes and appends every "sent" Ting to the JSONL file, so the whole path (CLI → daemon → session → backend → Ting) can be exercised on a Mac without an IAM application secret. Release artifacts: scripts/package-release.sh.

Conventions

Offline: spotify docs development · Source: docs/development.md