Metadata-Version: 2.4
Name: spopy
Version: 0.3.1
Summary: A production-quality Spotify CLI in a single Python file
Project-URL: Homepage, https://spopy.amitkot.com
Project-URL: Repository, https://github.com/amitkot/spopy
Project-URL: Changelog, https://github.com/amitkot/spopy/blob/main/CHANGELOG.md
Author: Amit Kotlovski
License-Expression: MIT
License-File: LICENSE
Keywords: cli,music,spotify,spotipy,terminal
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: End Users/Desktop
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Multimedia :: Sound/Audio
Requires-Python: >=3.12
Requires-Dist: rich==14.3.3
Requires-Dist: spotipy==2.26.0
Requires-Dist: typer==0.24.1
Description-Content-Type: text/markdown

# spopy

[![PyPI](https://img.shields.io/pypi/v/spopy)](https://pypi.org/project/spopy/)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![Python 3.12+](https://img.shields.io/pypi/pyversions/spopy)](https://pypi.org/project/spopy/)

A production-quality Spotify CLI in a single Python file. Runs anywhere with [uv](https://docs.astral.sh/uv/) — no install step, no virtualenv, no package manager.

Designed for both local use and self-hosting on a remote gateway (Dokku, VPS, etc.).

## Features

- **Single file** — one script, zero config files, inline dependencies via PEP 723
- **Two auth modes** — local browser login or headless gateway bootstrap (paste-back flow)
- **Persistent tokens** — authenticate once, use forever (auto-refresh)
- **Full playback control** — play, pause, seek, volume, shuffle, repeat, queue
- **Search and browse** — tracks, albums, artists, playlists
- **Playlist management** — create, add, remove, reorder, clear, replace
- **Library operations** — save, unsave, check, list saved tracks/albums
- **Discovery** — top tracks/artists, recently played, genre/mood search
- **Three output modes** — rich (human), plain (pipes), JSON (machines)
- **Honest API handling** — unsupported endpoints are reported clearly, never faked
- **Safe for servers** — never leaks tokens, no secrets in logs or output

## Install

### PyPI (recommended)

```bash
uv tool install spopy
```

Or with pipx:

```bash
pipx install spopy
```

### Script installer

```bash
curl -fsSL https://raw.githubusercontent.com/amitkot/spopy/main/install.sh | bash
```

### Standalone script

```bash
curl -fsSL https://raw.githubusercontent.com/amitkot/spopy/main/spopy.py -o ~/.local/bin/spopy && chmod +x ~/.local/bin/spopy
```

The standalone script uses PEP 723 inline deps and runs with `uv run spopy.py`.

## Quick Start

No setup required — spopy includes a built-in Spotify app.

```bash
spopy auth login
spopy status
spopy play "bohemian rhapsody"
```

### Remote / headless server

```bash
spopy auth url
# Open the printed URL in a browser on another machine
# After approving, copy the command from the callback page
spopy auth callback-url '<paste_the_url>'
```

### Transfer token from local to remote

```bash
# On local machine
spopy auth login
spopy auth export-token-info --raw --yes > token.json

# Copy token.json to remote, then:
spopy auth import-token-info token.json
```

## Configuration

All configuration is via environment variables. **None are required** — spopy works out of the box with PKCE auth.

### Auth (all optional)

| Variable | Default | Description |
|---|---|---|
| `SPOTIFY_CLIENT_ID` | (built-in) | Override with your own Spotify app |
| `SPOTIFY_CLIENT_SECRET` | | Set to switch to classic OAuth flow |
| `SPOTIFY_REDIRECT_URI` | (auto) | Override redirect URI |

> Setting `SPOTIFY_CLIENT_SECRET` switches spopy to the classic OAuth flow with your own Spotify app. See `spopy auth setup-guide` for details.

### Runtime (all optional)

| Variable | Default | Description |
|---|---|---|
| `SPOTIFY_CACHE_PATH` | `~/.config/spopy/token_cache` | Token cache file path |
| `SPOTIFY_USERNAME` | | Spotify username (for multi-user) |
| `SPOTIFY_SCOPES` | (sensible defaults) | Override OAuth scopes |
| `SPOTIFY_DEFAULT_DEVICE_ID` | | Fallback device ID |
| `SPOTIFY_DEFAULT_DEVICE_NAME` | | Fallback device name |
| `SPOTIFY_MARKET` | | ISO country code for market |
| `SPOTIFY_OUTPUT` | `rich` | Default output: `rich`, `plain`, `json` |
| `SPOTIFY_TIMEOUT_SECONDS` | `15` | API request timeout |
| `SPOTIFY_RETRIES` | `3` | Max retry attempts |
| `SPOTIFY_BACKOFF_FACTOR` | `0.5` | Exponential backoff factor |
| `SPOTIFY_DEBUG` | `0` | Enable debug logging (`1`) |
| `SPOTIFY_OPEN_BROWSER` | `1` | Allow browser opening (`0` to disable) |
| `SPOTIFY_NO_COLOR` | `0` | Disable color output (`1`) |
| `SPOTIFY_STATE_FILE` | | Path to persist auth state |

## Commands

### Global flags

```
--json          JSON output
--plain         Plain text output (pipe-friendly)
--debug         Debug logging
--market CC     Spotify market (ISO country code)
--device-id ID  Target device ID
--device-name N Target device name
--limit N       Result limit
--offset N      Result offset
--yes           Skip confirmations
--exact         Prefer exact name matches
--interactive   Interactive selection from results
--version       Show version
```

### Auth

| Command | Description |
|---|---|
| `auth setup-guide` | First-time setup instructions |
| `auth status` | Show auth config and token status |
| `auth url` | Print authorization URL (for gateway flow) |
| `auth login` | Interactive login (local browser) |
| `auth callback-url <url>` | Exchange redirect URL for tokens |
| `auth code <code>` | Exchange raw auth code for tokens |
| `auth import-token-info <path>` | Import token JSON (`-` for stdin) |
| `auth export-token-info` | Export token JSON (`--raw` for real tokens) |
| `auth whoami` | Show current user |
| `auth logout` | Remove token cache |

### Playback

| Command | Description |
|---|---|
| `play [query]` | Play or resume. Search query, URI, URL, or ID |
| `pause` | Pause playback |
| `resume` | Resume playback |
| `stop` | Stop (alias for pause — no true stop API) |
| `next` | Skip to next track |
| `previous` | Skip to previous |
| `seek <pos>` | Seek: `1:30`, `+10s`, `-15s`, or milliseconds |
| `volume <0-100>` | Set volume |
| `repeat <off\|track\|context>` | Set repeat mode |
| `shuffle <on\|off\|toggle>` | Set shuffle |

### Search

| Command | Description |
|---|---|
| `search <query>` | Search (`--type track,album,artist,playlist`) |

### Devices

| Command | Description |
|---|---|
| `devices list` | List available devices |
| `devices transfer <device>` | Transfer playback (name or ID) |

### Track

| Command | Description |
|---|---|
| `track show <query>` | Track details |
| `track play <query>` | Play a track |
| `track queue <query>` | Add to queue |
| `track save <query>` | Save to library |
| `track unsave <query>` | Remove from library |
| `track check <query>` | Check if saved |
| `track open <query>` | Print Spotify URL |
| `track audio <query>` | Audio features (restricted API) |

### Album

| Command | Description |
|---|---|
| `album show <query>` | Album details |
| `album play <query>` | Play album |
| `album tracks <query>` | List album tracks |
| `album save <query>` | Save to library |
| `album unsave <query>` | Remove from library |
| `album check <query>` | Check if saved |

### Artist

| Command | Description |
|---|---|
| `artist show <query>` | Artist details |
| `artist top <query>` | Top tracks (search-based) |
| `artist albums <query>` | List albums |
| `artist follow <query>` | Follow artist |
| `artist unfollow <query>` | Unfollow artist |
| `artist related <query>` | Related artists (restricted API) |

### Playlist

| Command | Description |
|---|---|
| `playlist list` | Your playlists (`--all` for full list) |
| `playlist show <pl>` | Playlist details |
| `playlist create <name>` | Create (`--description`, `--public/--private`) |
| `playlist rename <pl> <name>` | Rename |
| `playlist describe <pl> <desc>` | Set description |
| `playlist set-public <pl>` | Make public |
| `playlist set-private <pl>` | Make private |
| `playlist follow <pl>` | Follow playlist |
| `playlist unfollow <pl>` | Unfollow playlist |
| `playlist items <pl>` | List items |
| `playlist add <pl> <tracks...>` | Add tracks |
| `playlist remove <pl> <tracks...>` | Remove tracks |
| `playlist clear <pl>` | Remove all items |
| `playlist reorder <pl>` | Reorder (`--from`, `--to`, `--length`) |
| `playlist replace <pl> <tracks...>` | Replace all items |

### Library

| Command | Description |
|---|---|
| `library tracks` | Saved tracks |
| `library albums` | Saved albums |
| `library save <query>` | Save item |
| `library unsave <query>` | Remove item |
| `library check <query>` | Check if saved |

### Queue

| Command | Description |
|---|---|
| `queue list` | Current queue |
| `queue add <query>` | Add to queue |
| `queue clear` | Not supported (no API) |
| `queue remove` | Not supported (no API) |

### Discovery

| Command | Description |
|---|---|
| `status` | Current playback summary |
| `current` | Detailed now-playing info |
| `recent` | Recently played tracks |
| `top tracks` | Your top tracks (`--time-range`) |
| `top artists` | Your top artists (`--time-range`) |
| `discover` | Discovery suggestions from your history |
| `radio <query>` | Build a queue from a seed (search heuristic) |
| `genre list` | Well-known genres |
| `genre search <genre>` | Search by genre |
| `mood search <mood>` | Search by mood (heuristic) |
| `doctor` | Diagnose auth, config, devices, connectivity |

## Output Modes

### Rich (default)

```
$ spopy status
╭─ Bohemian Rhapsody  —  Queen  (A Night at the Opera) ───╮
│ Playing  2:15 / 5:55                                     │
│ Device: MacBook Pro  |  Volume: 65%  |  Shuffle: off     │
╰──────────────────────────────────────────────────────────╯
```

### JSON

```
$ spopy --json status
{
  "ok": true,
  "command": "status",
  "data": {
    "name": "Bohemian Rhapsody",
    "artists": "Queen",
    "playing": true,
    "progress": "2:15",
    "duration": "5:55",
    "device": "MacBook Pro"
  }
}
```

### Plain

```
$ spopy --plain status
Bohemian Rhapsody	Queen	A Night at the Opera	playing	2:15/5:55	MacBook Pro
```

## AI Agent Skill

A [Claude Code](https://docs.anthropic.com/en/docs/claude-code) skill is included for AI agents.

To install it, copy the `skills/` directory into your project or Claude Code config:

```bash
# Project-level
cp -r skills/ /path/to/your/project/.claude/skills/

# Or user-level
cp -r skills/ ~/.claude/skills/
```

The skill teaches the agent how to invoke the CLI, parse JSON output, and handle errors.

## Exit Codes

| Code | Meaning |
|---|---|
| 0 | Success |
| 2 | Invalid user input |
| 3 | Auth/config error |
| 4 | Spotify API error (Premium required, no device, 403, 404) |
| 5 | Rate limit exhausted |
| 10 | Internal error |

## Device Selection

For playback commands, devices are selected in this order:

1. `--device-id` flag
2. `--device-name` flag
3. Currently active Spotify device
4. `SPOTIFY_DEFAULT_DEVICE_ID` env var
5. `SPOTIFY_DEFAULT_DEVICE_NAME` env var
6. Error with helpful message

## Known Limitations

These are Spotify API limitations, not CLI bugs:

- **Queue clear/remove** — no API endpoint exists
- **Recommendations** — removed from Spotify API (Nov 2024)
- **Audio features** — restricted to apps with extended API access
- **Related artists** — restricted to apps with extended API access
- **Artist top tracks** — removed from API (Feb 2026); CLI uses search fallback
- **Search limit** — max 10 results per type (Spotify API limit)
- **Volume control** — some devices (phones, smart speakers) don't support remote volume
- **Premium required** — all playback control requires Spotify Premium

## License

MIT License. See [LICENSE](LICENSE).
