Add spotify integration
Deploy HEOS panel / deploy (push) Successful in 25s

This commit is contained in:
2026-09-15 22:07:43 +02:00
parent 6a0f1fa5e8
commit 7c19ff9096
17 changed files with 810 additions and 44 deletions
+72 -7
View File
@@ -9,7 +9,9 @@ Everything it does fits on one screen:
- **Volume** up/down for the Home 400 and the Living Room pair. A tap lands
on the next multiple of `VOLUME_STEP` — from 23 it goes to 25, not 28 —
so the levels stay round. Hold to keep moving.
- **Play or pause** either room. A room that is grouped shares the AVR's
- **Play or pause** either room, or skip a track, while it is playing
Spotify — the buttons only show up then, since an AVR input has nothing
to pause or skip. A room that is grouped shares the AVR's
transport, so pausing it pauses the group — HEOS's doing, not the panel's:
a group has one thing playing, by definition
- **Group** either room with the AVR — the AVR is always the host, so its
@@ -18,6 +20,9 @@ Everything it does fits on one screen:
- **Ungroup** either room again, or all of them at once
- **Change the AVR's input**, listed under the names you gave them, minus
the sources you deleted in the AVR's setup menu
- **Resume Spotify** on a room from Fifou's or Clarita's account, one
button each — the reverse of connecting to it from the Spotify app.
Optional; see [Spotify](#spotify) below
## The kit it assumes
@@ -78,6 +83,62 @@ by their HEOS input id (`GET /api/avr/inputs` shows the exact strings, e.g.
`inputs/aux_in_1`), and sets their order; leave it empty to list everything
HEOS reports for it.
## Spotify
Spotify dropped native, browsable HEOS integration years ago — it is
Connect-only on HEOS now, and Connect only works phone → speaker: the
Spotify app pushes playback to a room, and there is nothing in HEOS that
asks for the reverse. So "Resume Spotify" doesn't go through HEOS at all;
it calls Spotify's own Web API to transfer the account's current playback
onto the room's Spotify Connect receiver, which the receiver already
advertises on the LAN whether or not anything is playing. This needs
Spotify Premium and a one-time login, since Spotify has no way to grant
that without a human approving it once.
1. Create an app at the
[Spotify Developer dashboard](https://developer.spotify.com/dashboard)
(any name), and add this Redirect URI in its settings:
`http://127.0.0.1:8899/callback`. Spotify allows plain `http` for a
`127.0.0.1` redirect specifically, which is why the login below needs no
HTTPS setup.
2. While the app is in development mode, Spotify only lets accounts you
have listed log in to it: add both accounts' email addresses under the
app's **User Management**.
3. Run the one-time login once per account, from a machine with a browser
(your laptop is fine — it doesn't have to be the Pi):
```bash
python3 tools/spotify_auth.py --client-id <id> --client-secret <secret> --account fifou
python3 tools/spotify_auth.py --client-id <id> --client-secret <secret> --account clarita
```
Log in as that account each time — use a private window for the second
run, or Spotify just approves whichever account the browser is already
logged in as. Each run prints `SPOTIFY_CLIENT_ID` and
`SPOTIFY_CLIENT_SECRET` (the same both times) plus that account's own
`SPOTIFY_FIFOU_REFRESH_TOKEN` or `SPOTIFY_CLARITA_REFRESH_TOKEN`.
4. Put those four lines in a `.env` file in this directory (already
git-ignored, and excluded from the deploy rsync the same way
`members.json` is — see "What survives a deploy" below). `config.py`
reads it directly, so no shell-sourcing step or restart-in-the-right-
terminal gotcha — just `python3 app.py` as normal. A real exported
environment variable (or systemd's `EnvironmentFile`, in the service)
still overrides the file rather than the other way round.
`config.py` never sees the credentials themselves — it only reads them out
of the environment. Each speaker's card then gets one Spotify button per
account in `SPOTIFY_ACCOUNTS` that has a refresh token; an account without
one simply gets no button. The AVR's card has none. Whichever account is
playing on a room right now gets a border around its button — the panel
asks Spotify, but only while HEOS says a room is on Spotify. Each room resumes on the Spotify Connect device named
`spotify_name` in its `TARGETS` entry, falling back to `heos_name` if that
key is missing (they usually match). `GET /api/spotify/devices?account=fifou`
lists what Spotify actually calls each device, if a room's button ever says
one isn't visible.
A room's play/pause and next buttons only appear while HEOS reports it is
playing (or paused on) Spotify. To see what HEOS reports for a room, use
`GET /raw/player/get_now_playing_media?pid=<pid>` with a pid from
`/api/targets`: Spotify shows up as `"sid": 4`.
## Add it to the iOS home screen
Open the page in Safari → Share → **Add to Home Screen**. It then launches
@@ -159,11 +220,11 @@ else or runs as someone else. Re-run the workflow and it goes green.
### What survives a deploy
The rsync excludes `.venv` and `members.json`, so the runtime and the stereo
pair's learned membership are left alone by `--delete`. Everything else in
the deploy path is made to match the repo, `config.py` included: your device
names live in git, so change them there and push rather than editing the
deployed copy.
The rsync excludes `.venv`, `members.json` and `.env`, so the runtime, the
stereo pair's learned membership, and Spotify's credentials are left alone by
`--delete`. Everything else in the deploy path is made to match the repo,
`config.py` included: your device names live in git, so change them there and
push rather than editing the deployed copy.
## Behind a reverse proxy, at /heos
@@ -277,6 +338,8 @@ Used by the interface:
| `POST /api/group/none` | every room back on its own |
| `GET /api/avr/inputs` | your renamed sources, over HEOS |
| `POST /api/avr/input` | `{"code": "inputs/aux_in_1"}` |
| `GET /api/spotify/devices?account=fifou` | every Spotify Connect receiver that account currently sees (needs [Spotify](#spotify) configured) |
| `POST /api/spotify/resume` | `{"target": "home400", "account": "fifou"}` — transfers that account's current playback there and resumes it |
`POST /volume/up` and `/volume/down` take one snapped tap by default; pass
`?step=3` and they move that many raw points instead, as they always did.
@@ -306,8 +369,10 @@ python3 tools/make_icons.py # re-render the icons from static/lo
app.py Flask: the UI, the API, and the old bridge's routes
controller.py what a room is, what grouping means, volume, the AVR's inputs
heos.py HEOS CLI client (persistent socket, reconnects itself)
spotify.py Spotify Web API client, for the "Resume Spotify" button
config.py your devices and preferences
demo.py fake speakers for --demo
templates/ static/ the interface
tests/ a fake HEOS server, and tests against it
tests/ fake HEOS and Spotify servers, and tests against them
tools/spotify_auth.py one-time Spotify login, prints the refresh token
```