Skip to content

spotify-cli v0.1.3

Choose a tag to compare

@unlikefraction unlikefraction released this 26 Sep 13:01
· 13 commits to main since this release

Playback control:

  • like and unlike never change the wrong song. spotify_player can only like the track it
    believes is playing, which could be an earlier one, so they now check that it is the song
    Spotify.app plays. If it is not, they nudge it once (an inaudible playback volume <current>)
    and wait up to verify_timeout_ms + 1.1 s. If it still does not match, they refuse with the new
    track_mismatch and change nothing (before, like could save the previous song, or report
    success for a podcast episode and do nothing). Episodes, ads and local files are refused as
    unsupported.
  • Before a spotify_player command whose effect depends on its own memory (play, pause, toggle,
    shuffle, next, previous, seek), that memory is compared with Spotify.app. When they disagree,
    AppleScript acts at once and fallback.reason.code is the new state_mismatch; under strategy
    spotify_player that is the error. toggle sends an explicit pause or play chosen from
    Spotify.app's state.
  • Seeks are exact: AppleScript sets the position first; spotify_player's relative seek is only
    the fallback, and under strategy spotify_player it is refused with state_mismatch when
    spotify_player is on another item. play <track|episode|show> goes to AppleScript only, so a
    start never leaves Spotify.app stopped with nothing loaded.
  • play --liked plays the Liked Songs list itself through AppleScript, so next and previous
    stay in it; it no longer stops playback. --random turns shuffle on for Liked Songs (Spotify
    keeps it) and starts at a random song; --limit applies only when spotify_player starts the
    list. A failed start that leaves Spotify.app empty puts back what was playing, under any
    strategy (details.restored, or details.restore_failed).
  • Refusals the running spotify_player logs (429, 403) are noticed at once instead of after the
    verify timeout: fallback.reason is rate_limited, no_effect or no_active_device, with
    details.request and details.spotify_player. play, pause, toggle, volume and
    shuffle wait at most 1.5 s before falling back. With the state checks and AppleScript-first
    seeks, toggle, seek and previous no longer take 3.6–6.8 s when spotify_player's command
    would have no effect.
  • previous behaves the same on every path: from 3 s in, or when there is no item before it,
    it restarts the item; otherwise it goes to the previous item. The outcome's new result
    (restarted or previous_item) and the first line of the human output ("Back to the start of
    this item." / "Back to the previous item.") say which.
  • repeat follows spotify_player's real order (off → track → context), confirms each step and
    checks the result in Spotify.app. It no longer reports success while leaving another mode, and
    a repeat off it cannot confirm never passes through repeat-one. When a refused step leaves
    repeat-one on, it returns that failure instead of an AppleScript success that leaves the song
    repeating. When spotify_player refuses for now, repeat track returns its error (e.g.
    rate_limited, retryable) instead of unsupported.
  • AppleScript volume lands on the requested level (it was one below for most levels); 19, 39,
    59, 79 and 99 land one above. Verification is exact.
  • spotify status --full: web gains item_uri, is_playing, source (spotify_player or
    web_api) and stale. When spotify_player's memory disagrees with Spotify.app, a fresh Web API
    read (1–4 s) is used instead; when even that is for another item, web.stale is true, repeat
    and shuffle that contradict Spotify.app are left out, and the new warning web_state_stale
    says so. New warnings also include no_active_device and timeout. The human output then
    takes repeat from Spotify.app and adds "web data out of date".
  • Time left, (-m:ss), in spotify status and spotify track is rounded to the nearest
    second, as is playback.remaining in trigger firings: a --remaining 20s firing reads 0:20,
    not 0:19 (remaining_ms is unchanged).
  • spotify launch --json answers launched: false, already_running: true when Spotify.app was
    already running (it said launched: true) and does not run open; the human output says
    "Spotify.app was already running." or "Started Spotify.app (hidden).".

Queue:

  • Quick or concurrent spotify nexts are applied one after another and never hand off the same
    item or lose one. A next that arrives while the previous hand-off is still in flight skips
    that item and plays the one after it (skipped in the reply, and a
    skipped <uri> (it was still being switched to) line).
  • play <item> (also --liked and radios) and previous wait for a next or queue hand-off in
    progress, and those wait for them, so changes reach Spotify in the order they were asked. A
    failed play <item> keeps the queue's resume point; if the managed item still plays, it stays
    the managed item.
  • Episodes queued by URI get their name, show and length (a Web API lookup with
    spotify_player's cached token), and queue add --search passes the chosen hit's facts to the
    daemon (known). Items without a known name show as episode <id> / track <id>, Spotify's
    upcoming items show their artists or show, and queue add prints what it added
    ("Queued 1 item. Managed queue now has 3." plus a + name — by line per item). Albums,
    artists, playlists and shows are refused as "is an album" (and so on), with a hint that fits.
  • spotify queue leaves out Spotify's repeats of the item playing now (an episode played without
    a context was listed ten times) and prints a note saying so (current_repeats_left_out,
    note in --json).
  • Resuming the interrupted list after the managed queue drains also works when its next item is
    a podcast episode (its URI was built as a track's).

Details, library and playlists:

  • spotify track shows liked: yes|no for songs (liked in --json, from a Web API check made
    alongside the lookup, left out when it fails or takes over 2.5 s).
  • spotify track <playlist> --json has the same envelope as the other kinds: kind, item,
    playlist, owner, collaborative, track_count, duration_ms, duration, tracks, raw.
  • spotify track --type offers only track, album, artist and playlist; show or
    episode is a usage error. A bare id that is not found as a track gets a hint to pass
    --type album|artist|playlist or a full URI.
  • Library and playlist reads (library <section>, playlist list, playlist show,
    track <uri>) are retried once by the daemon after a transient transport or
    spotify_player_busy error, and library, playlist list and playlist show once more by the
    CLI. Rate limits, timeouts and writes are not retried.
  • A library or playlist read right after a playlist change or like/unlike waits until it can see
    the change (1.1 s), also when another identical read was in flight during the change, so
    playlist list right after playlist delete no longer lists the deleted playlist.

Daemon and its spotify_player:

  • The warm spotify_player is the daemon's own and actually serves the CLI: copies left by earlier
    daemons (the pid in the new warm-player.json, or orphans with the daemon's exact overrides)
    are stopped before a start; it is reported running only once it holds the client port;
    another spotify_player holding the port (your own TUI) is left alone and reported as
    deferred with its pid; a copy that never gets the port is stopped and retried
    (not_serving). It dies with the daemon, even on SIGKILL.
  • The warm copy refreshes playback every 3 s (playback_refresh_duration_in_ms; a smaller
    positive value in your app.toml is kept), about 20 small Web API requests a minute, so its
    view is rarely more than 3 s behind Spotify.app.
  • spotify daemon status describes the warm spotify_player in words (pid and port, refresh
    interval, who holds the port, retry delays, errors). spotify doctor's
    spotify_player_warm_instance check passes only when the daemon's own copy holds the port, and
    shows the state and the pid that holds the port when it fails.
  • spotify daemon restart with the launchd agent loaded restarts the job in place
    (launchctl kickstart -k) and waits for a new pid; it used to stop the daemon and never start
    it again. It prints "spotify-daemon restarted via launchd (pid N, vX).". daemon stop and
    uninstall wait until launchd has unloaded the job; daemon start waits up to 15 s and keeps
    retrying while a previous instance is still exiting.
  • spotify update --check reports the real auto_update setting and no longer leaves
    daemon status saying it is off.
  • The telemetry relay drains the whole backlog (up to 2 000 events) every minute instead of 40
    events a minute.

CLI:

  • Human output keeps Spotify names on one line (line breaks, tabs and other control characters
    become spaces); --json keeps the original strings. While a podcast episode plays, the status
    header has no dangling " — ", and spotify track shows show: instead of album:.
  • The spotify search footer suggests queue it: spotify queue add <uri> only for tracks and
    episodes, and look inside: spotify track <uri> for albums, artists and playlists.
  • spotify report --attach takes text files only: binary files, directories and a home's
    session.json or testing.json are refused with invalid_input before anything is sent. Of
    long files only the last 64 KiB are sent, starting at a whole character.
  • Every usage error has details.usage, and details.argument and details.value when the
    parser names them; the hint always includes the usage line. --limit -1 is
    invalid_input ("--limit -1 is out of range: it takes 1 to 10.") instead of the misleading
    -- -1 tip, and a hyphenated value after an option (--note -x) gets the tip to write
    --note=-x.
  • spotify config set search_limit=5 or config set search_limit 5 gets the JSON form as its
    hint: spotify config set '{"search_limit": 5}'. config set --help lists examples.
  • spotify doctor, daemon status, queue, launch, previous and next say more in human
    output, as described above; the --help of status, play, toggle, previous, seek,
    repeat, like, unlike, launch and track describes the new behaviour.

Docs:

  • The playback guide documents the control rule and its exceptions, the state checks, refusals,
    previous, repeat, volume, like/unlike and failed-start restores; the daemon guide the warm
    spotify_player, its states and costs, restart and library reads; the queue guide next under
    concurrency, ordering with explicit commands and item names. The error table gains
    state_mismatch, track_mismatch and web_state_stale and drops queue_empty, which no
    command returns (spotify next with an empty managed queue runs Spotify's own next).
  • Developers can run control commands against Spotify.app without the daemon:
    cargo run -p silicon-spotify-client --example control -- <command>.