Skip to main content

pixoo-spotify

Your currently playing Spotify track — album art, title, and artist — live on a Divoom Pixoo64.

PyPI Python CI License: MIT

Album artwork with scrolling track title and artist, rendered as a 64x64 pixel-art GIF

▶️ See it running on a real Pixoo64:

https://github.com/user-attachments/assets/13760f42-3ae3-4668-8fbd-006f4dc64f96


✨ Why pixoo-spotify

  • Two CLI commands to a working display. Authenticate once, then run. A typical setup needs no IP address, port forwarding, or broker.
  • No client secret. Spotify Authorization Code with PKCE — you only paste a Client ID.
  • Finds your Pixoo for you. Device discovery, route-aware host-IP selection, and port selection happen automatically; rediscovery can recover after a network hiccup.
  • Built for unattended operation. Rich terminal UI when you're watching, plain logs when systemd is. Headless SSH login. Non-zero exit and a copy-pasteable fix when your Spotify session expires.
  • Real pixel art, not a downscale. A tuned filter pipeline (blur → median → posterize → quantize) keeps 64×64 artwork readable instead of muddy.
  • Rendering stays on your machine. Artwork is processed locally and served by a small local HTTP server — no third-party rendering service in the loop.

How it works

Spotify Web API  →  GIF renderer  →  local HTTP server  ←  Pixoo64

pixoo-spotify polls Spotify for the current track, renders a pixel-art GIF, serves it at /spotify_gif from a lightweight local server, and tells the Pixoo to fetch that URL. Each update uses a cache-busting URL so the display never gets stuck on a stale frame.

[!IMPORTANT] The Pixoo64 and the machine running pixoo-spotify must be able to reach each other over your local network. pixoo-spotify sends commands to the Pixoo, and the Pixoo connects back to download the GIF — so traffic has to flow in both directions.

Sharing an internet connection is not enough. Guest Wi-Fi, wireless client isolation, host firewalls, and some VPN or container setups block device-to-device traffic. A routed LAN is fine — the two do not have to sit on the exact same subnet, as long as both directions are routable.


📋 Requirements

What You need
Hardware A Divoom Pixoo64 on your local network
Spotify A Spotify Premium account and an app registered in the Spotify Developer Dashboard
Runtime uv (provides uvx); Python 3.10+ is fetched by uv automatically

[!NOTE] Spotify's Development Mode requires the app owner to have an active Premium subscription. Each listening account must also be added to the app's allowlist (up to five users). See Spotify's current quota-mode documentation if authorization succeeds but playback data stays empty.


🚀 Quick start

1. Register a Spotify app

In the Spotify Developer Dashboard, create an app and add this exact Redirect URI:

http://127.0.0.1:8888/callback

Copy the Client ID. You do not need a client secret.

2. Save the Client ID and authenticate

Create a .env file in the directory where you will run pixoo-spotify:

SPOTIFY_CLIENT_ID=YOUR_CLIENT_ID

Then authenticate without repeating the Client ID on the command line:

uvx pixoo-spotify auth

A browser opens for approval. The Client ID and token are stored in your user config directory, so later commands don't need --client-id.

Alternatively, skip .env and pass it directly with uvx pixoo-spotify auth --client-id "YOUR_CLIENT_ID".

[!TIP] If the browser redirect doesn't complete, the command also accepts the full redirected URL pasted straight into the terminal — no need to restart.

3. Run

uvx pixoo-spotify run

That's it. pixoo-spotify discovers your Pixoo, starts a local server on a free port, polls Spotify, and updates the display on every track change. Press Ctrl+C to stop.

uvx pixoo-spotify run --help   # every available option

🖥️ Servers, Raspberry Pi, and SSH

Authenticate without a local browser

uvx pixoo-spotify auth --client-id "YOUR_CLIENT_ID" --no-open-browser

Open the printed URL in a browser on any machine, approve access, then paste the full redirected URL back into the terminal. That browser will show "127.0.0.1 can't be reached" — expected, and irrelevant. The URL in its address bar is still what you paste.

[!TIP] Already authenticated? auth refuses to overwrite existing credentials. Add --reauth to replace them.

Run unattended

Use text mode so output is plain, timestamped lines instead of a live Rich panel:

uvx pixoo-spotify run --ui text --log-format basic

For hosts with several interfaces, a VPN, a container network, or a Pixoo on a fixed IP, pin every moving part explicitly:

uvx pixoo-spotify run \
  --ui text \
  --log-format basic \
  --device-ip 192.168.1.50 \
  --no-discover \
  --server-port 18080 \
  --public-base-url http://192.168.1.20:18080
Option What it pins
--device-ip / --no-discover The Pixoo's address; skips discovery entirely
--server-port The port the local GIF server listens on
--public-base-url The URL the Pixoo uses to fetch the GIF — must be reachable from the Pixoo

The Pixoo must be able to reach that server port through the host firewall.

Add --auto-screen-off if the panel should turn off while nothing is playing.

When your Spotify session expires

Spotify refresh tokens expire six months after authorization. When that happens — at startup or mid-run — pixoo-spotify does not hang waiting for input. It discards the token rejected with invalid_grant, prints ready-to-run re-authentication commands (interactive and headless), and exits non-zero so systemd or your supervisor reports the failure.

Recover with:

uvx pixoo-spotify auth --client-id "YOUR_CLIENT_ID" --reauth --no-open-browser

Then restart the service. See Spotify's refresh token expiration announcement for the policy itself.

What the automatic networking actually does
Behavior Detail
Pixoo discovery Asks Divoom's cloud endpoint which Pixoo devices share your public IP, then uses the first one's private address. It needs outbound internet access, and it fails if the host leaves through a different public IP than the Pixoo — a VPN or an isolated container network is the usual culprit. Pass --device-ip to skip it.
Host IP selection Asks the OS which local address routes to the Pixoo, so the GIF URL doesn't advertise the wrong Wi-Fi, Ethernet, or VPN address.
Port selection Without a config file or explicit port, the first free port in 1808018099 is used.
Rediscovery After a Pixoo connection error — or a track change while the device is unreachable — discovery reruns and the GIF URL is rebuilt, so a DHCP address change heals itself.
Cache busting Every update appends a fresh timestamp to the GIF URL.

⚠️ Config-file port behavior: Automatic port selection applies only when --server-port is omitted and no config file is in play. If a config.toml sits in the working directory (it is loaded automatically) or you pass --config, the port falls back to the built-in default 8000 unless you set it. Set [server] port explicitly in any config file you use.

Rediscovery is also skipped when you supplied --device-ip — an explicit address is always respected.


🔄 Updating

uvx runs pixoo-spotify from a cached, isolated environment rather than installing it. Refresh that cache with:

uvx --upgrade pixoo-spotify --version

Afterwards the plain uvx pixoo-spotify ... commands use the new version.

Other update options

Explicitly request the newest release for a command:

uvx pixoo-spotify@latest --version

If you installed it persistently instead (uv tool install pixoo-spotify):

uv tool upgrade pixoo-spotify

All of these upgrade pixoo-spotify, not uv itself — for that, see the uv tools guide.


🎨 Customization

Artwork

The default filter chain (blur:0.6|median:3|posterize:4|quantize:32) reduces noise and color count so 64×64 artwork reads as pixel art. Override it with your own |-separated chain:

uvx pixoo-spotify run --image-filters "blur:0.6|median:3|posterize:4|quantize:32"

Or turn it off for plain resize-only behavior:

uvx pixoo-spotify run --no-image-filters

Every filter and its arguments are documented in docs/image_filters.md.

Text and layout

# Artwork only, no text
uvx pixoo-spotify run --artwork-only

# Bounce the text and pause at each edge
uvx pixoo-spotify run --scroll-mode bounce --scroll-pause-frames 15

# Change what the text says and where it sits
uvx pixoo-spotify run \
  --text-format "{artist}\n{title}" \
  --text-position bottom-right

--text-format accepts {title}, {artist}, and {album}, up to three \n-separated lines. --text-position takes bottom-left (default), bottom-right, top-left, or top-right. --gif-size accepts 16, 32, or 64.

Fonts

A packaged Misaki Gothic 8-pixel font covers English and Japanese out of the box. For broader Latin, Japanese, Korean, and Simplified/Traditional Chinese coverage, install the recommended Fusion Pixel Font:

uvx pixoo-spotify font-install

The command shows the license (OFL-1.1) and asks for confirmation before downloading. You can also assign your own font to one language:

uvx pixoo-spotify font-install --lang ja --font-path ./font.ttf

📟 Device commands

# List Pixoo devices
uvx pixoo-spotify devices

# Turn the panel on or off
uvx pixoo-spotify display on  --device-ip 192.168.1.50
uvx pixoo-spotify display off --device-ip 192.168.1.50

# Set or read brightness (0-100)
uvx pixoo-spotify brightness set --value 40 --device-ip 192.168.1.50
uvx pixoo-spotify brightness get --device-ip 192.168.1.50

# Dump everything the device reports
uvx pixoo-spotify settings all --device-ip 192.168.1.50

Omit --device-ip on any of these and discovery is used instead.


⚙️ Configuration file

run accepts TOML or JSON. A config.toml in the working directory is loaded automatically; use --config to point elsewhere. auth accepts the same option.

poll_interval = 5
idle_poll_interval = 20

[spotify]
client_id = "YOUR_CLIENT_ID"

[pixoo]
device_ip = "192.168.1.50"
discover = false
auto_screen_off = true

[server]
host = "0.0.0.0"
port = 18080
public_base_url = "http://192.168.1.20:18080"

[gif]
size = 64
image_filters = ["default"]
scroll_mode = "loop"
text_format = "{title}\n{artist}"

[ui]
mode = "text"
log_format = "basic"
uvx pixoo-spotify run --config ./config.toml

Client ID sources use this precedence, from highest to lowest:

  1. --client-id
  2. [spotify].client_id in the selected TOML/JSON configuration
  3. An existing SPOTIFY_CLIENT_ID environment variable
  4. SPOTIFY_CLIENT_ID in .env in the working directory
  5. The Client ID cached by a previous successful auth command

This makes TOML preferable for an explicit server configuration while .env remains convenient for a local checkout. Never commit .env.


📂 Where your data lives

Credentials, fonts, and the generated GIF are kept in your user config directory — never in the repository. Don't commit .env, credentials, or token caches.

Data File Location
Cached Client ID auth_spotify_client.json config directory
Spotify token spotify_token.json config directory
Installed fonts fonts/ config directory
Latest GIF output/latest.gif config directory

By default, the config directory is ~/Library/Application Support/pixoo-spotify/ on macOS and ~/.config/pixoo-spotify/ on Linux.

Relocate credentials and fonts with the global --config-path, placed before the command:

uvx pixoo-spotify --config-path /srv/pixoo-spotify auth --client-id "YOUR_CLIENT_ID"
uvx pixoo-spotify --config-path /srv/pixoo-spotify run

[!NOTE] --config-path moves credentials and fonts, but not the generated GIF — it stays in the platform config directory. Use run --gif-output PATH to move that.


🧪 Try it without a Pixoo

Render a GIF from bundled sample data:

uvx pixoo-spotify demo --output output/demo.gif

Or from track metadata you supply:

uvx pixoo-spotify gif \
  --artist "Artist" \
  --title "Track" \
  --album "Album" \
  --artwork-url "https://example.com/artwork.jpg" \
  --output output/manual.gif

🩺 Troubleshooting

Symptom What to check
No Pixoo found Run uvx pixoo-spotify devices. Discovery needs outbound internet and matches devices by public IP, so a VPN, guest Wi-Fi, or client isolation will break it. Pass --device-ip to bypass discovery.
The Pixoo never loads the GIF Allow the server port through the host firewall, and make sure --public-base-url is an address the Pixoo can reach. From another device on the LAN, curl http://<host-ip>:<port>/ should return {"status": "ok", "gif": "/spotify_gif"}.
Spotify authorization missing or expired Run the re-authentication command printed by run, then restart it. See When your Spotify session expires.
Spotify rejects the callback Register http://127.0.0.1:8888/callback exactly in the dashboard. localhost is not accepted.
Boxes instead of characters Run uvx pixoo-spotify font-install, or install a font for that language.
Display stays blank while music plays Confirm the account is added to your Spotify app's user list, and that uvx pixoo-spotify run reports a discovered device rather than not found.
Need more detail Add the global --verbose before the command: uvx pixoo-spotify --verbose run.

🛠️ Development

git clone https://github.com/hotchpotch/pixoo-spotify.git
cd pixoo-spotify
uv sync --extra dev
uv run --extra dev tox

Tox runs pytest, Ruff, and ty.

See docs/release.md for PR validation, local package checks, and the PyPI Trusted Publishing release process.

Live Spotify E2E tests (opt-in)

These tests call Spotify's real token and playback endpoints. They use a dedicated token cache and never touch your normal application token.

cp .env.sample .env   # then set SPOTIFY_CLIENT_ID

Create the dedicated token once:

set -a
source .env
set +a
uv run pixoo-spotify auth \
  --client-id "$SPOTIFY_CLIENT_ID" \
  --cache-path "$SPOTIFY_E2E_TOKEN_CACHE" \
  --reauth \
  --no-open-browser

Keep RUN_SPOTIFY_E2E=0 in .env so ordinary test runs stay offline, and enable live access for a single command:

RUN_SPOTIFY_E2E=1 uv run --extra dev pytest -m spotify_e2e

The suite verifies invalid_grant handling, refreshes the dedicated token, and calls the current playback API. .env is git-ignored — keep it that way.


License

MIT — see LICENSE.

Author

Yuichi Tateno (@hotchpotch)

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pixoo_spotify-0.1.2.tar.gz (308.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pixoo_spotify-0.1.2-py3-none-any.whl (308.5 kB view details)

Uploaded Python 3

File details

Details for the file pixoo_spotify-0.1.2.tar.gz.

File metadata

  • Download URL: pixoo_spotify-0.1.2.tar.gz
  • Upload date:
  • Size: 308.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for pixoo_spotify-0.1.2.tar.gz
Algorithm Hash digest
SHA256 dedfd61d004e60a6fe40fa371ab8dc47ec70987e81259817380e9e2de15ae5b3
MD5 22cd10b13b0de71bf0d26d695f2db757
BLAKE2b-256 69d12972ef69d265c9f811068452baca847ee40b4eb5e5ebf70ae6797efd8b12

See more details on using hashes here.

Provenance

The following attestation bundles were made for pixoo_spotify-0.1.2.tar.gz:

Publisher: release.yml on hotchpotch/pixoo-spotify

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pixoo_spotify-0.1.2-py3-none-any.whl.

File metadata

  • Download URL: pixoo_spotify-0.1.2-py3-none-any.whl
  • Upload date:
  • Size: 308.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for pixoo_spotify-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 b1f7241305b20d1a597eb9c1b0eb00e68bf8549ec7e090871d3331d9fdcc07cd
MD5 165bf15bcb6d5f24ad2cff6dcddc0b7f
BLAKE2b-256 3aae01a4ba201fedde80acc4740757a111a3c0ce0c14ebeb5e6d314d597d3d9d

See more details on using hashes here.

Provenance

The following attestation bundles were made for pixoo_spotify-0.1.2-py3-none-any.whl:

Publisher: release.yml on hotchpotch/pixoo-spotify

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.1.2 This release

2 files

0.1.1

2 files

0.1.0

2 files

0.0.4

2 files

0.0.3

2 files

0.0.1

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page