A module for the MagicMirror² project displaying the song currently playing on Spotify.
This is my fork of raywo/MMM-NowPlayingOnSpotify (MIT). I added handling for Spotify's refresh-token expiry (effective 20 July 2026), a self-managed token store, and an integrated re-authorization flow — so a dead token no longer silently blanks the display.
After you set up a Spotify app and authorize it once, the module shows the track you are currently listening to, on which device, and optionally the album cover.
Since 20 July 2026 Spotify expires user refresh tokens after 6 months. When that happens this module:
- detects the
invalid_granterror, stops hammering the API, and shows a clear red re-authorization banner on the mirror (instead of pretending nothing is playing); - warns proactively (about two weeks before the hard expiry) with a smaller banner above the cover art, showing the same re-authorization URL so you can renew early — the music keeps playing;
- lets you re-authorize from any computer via a short SSH tunnel — no config editing, no token copy-paste, and the mirror recovers automatically without a restart.
Note on the "nothing playing" logo: when nothing is playing, the module shows a logo instead of cover art. I replaced the Spotify logo with a Volumio logo; the original Spotify logo is kept as
img/Spotify_Logo_RGB_White_original.png. To switch back to the Spotify logo:cp img/Spotify_Logo_RGB_White_original.png img/Spotify_Logo_RGB_White.png
- A MagicMirror² instance (Node.js >= 18, for native
fetch) - A Spotify account
This module has no runtime dependencies.
cd ~/MagicMirror/modules
git clone https://github.com/rkorell/MMM-NowPlayingOnSpotify.git
cd MMM-NowPlayingOnSpotify
npm installCreate an app in the Spotify Developer Dashboard. Note the Client ID and Client Secret.
Add this exact Redirect URI:
http://127.0.0.1:8888/callback
⚠️ This is effectively the only value that works — take it literally.Spotify accepts
httpredirect URIs only for the loopback address127.0.0.1. Watch out for the trap: a LAN IP or hostname overhttpmay be accepted in the dashboard yet rejected at the actual redirect as insecure — andlocalhostis not allowed at all (it must be the numeric127.0.0.1).In practice the only part you might ever change is the port (
8888), and then only consistently in both the redirect URI andconfig.js. The single real alternative would be anhttps://URI with a valid certificate — which on a headless mirror means running a reverse proxy plus certificate management, wildly disproportionate for a twice-a-year login. So for all practical purposes: usehttp://127.0.0.1:8888/callback.
{
module: "MMM-NowPlayingOnSpotify",
position: "top_right",
config: {
showCoverArt: true,
clientID: "<YOUR_CLIENT_ID>",
clientSecret: "<YOUR_CLIENT_SECRET>",
redirectURI: "http://127.0.0.1:8888/callback"
}
}You no longer paste tokens into config.js. Tokens are obtained in Step 3 and stored by the module itself in tokens.json (git-ignored — it holds secrets, never commit it).
Migration from v1.x: if your config.js still contains accessToken and refreshToken, they are picked up once and moved into the token store automatically. After the first successful run you should remove accessToken and refreshToken from config.js — only clientID, clientSecret and redirectURI need to stay.
On a fresh install there are no tokens yet, so the module starts directly in the authorization state and shows the red banner right away — that is the normal first-time setup, not an error. Renewal after the 6-month expiry uses the exact same steps.
Because the redirect URI is a loopback address, the browser doing the login must reach the mirror's 127.0.0.1:8888 — bridge it from another computer with a one-line SSH tunnel:
- When authorization is needed (first run, or after expiry), the mirror shows a red banner.
- On your computer, open an SSH tunnel to the mirror and keep it open:
ssh -L 8888:127.0.0.1:8888 pi@<mirror-ip> - In a browser on that computer, open
http://127.0.0.1:8888and click Log in with Spotify. - Spotify redirects back through the tunnel, the token is stored, and the mirror recovers on its own — no restart, no file editing.
Alternatively, run a browser directly on the mirror's own desktop and open http://127.0.0.1:8888 there.
That's it — twice a year, one login.
Spotify uses two tokens, and their names are misleading:
accessToken— short-lived (about 1 hour). It is the token actually sent with each API request, but it is disposable: the module treats any stored access token as already expired and mints a fresh one on startup, then roughly every hour. You never supply a lasting access token — its value is throwaway state.refreshToken— long-lived (now 6 months). This is the credential that actually keeps the module working: it is exchanged for new access tokens over and over. When it expires, everything stops until you re-authorize — which is what this whole module is about.
So despite the naming — access sounds primary, refresh sounds like a mere helper — the refreshToken is the one that matters. Both are obtained during authorization and managed for you in tokens.json; you never edit them by hand. This is also why the accessToken that used to sit in a v1.x config.js was effectively irrelevant: it was replaced by a refresh on every start, so it was safe to leave stale — and safe to delete now.
| Option | Description |
|---|---|
clientID |
REQUIRED string – the Client ID of your Spotify app. |
clientSecret |
REQUIRED string – the Client Secret of your Spotify app. |
redirectURI |
REQUIRED string – the redirect URI whitelisted in your Spotify app, and the port the local auth server listens on. In practice this is fixed to http://127.0.0.1:8888/callback (loopback only — see the callout in Step 1); the port is the only part you would realistically change. |
showCoverArt |
Optional boolean – show the album cover. Default true. |
updatesEvery |
Optional integer – display update interval in seconds. Default 1. Lower is more responsive; higher relieves the Raspberry Pi. |
Override the module's styles in MagicMirror's css/custom.css (this survives updates — don't edit the module's own css/styles.css). For example, the album cover fills the module width by default (100%), which can be quite large:
.NPOS_albumCover {
width: 50%;
}cd ~/MagicMirror/modules/MMM-NowPlayingOnSpotify
git pullThis module has no dependencies, so there is nothing to npm install.
The re-authorization and warning texts follow MagicMirror's language setting. English (en) and German (de) are included in translations/; add another locale by dropping a matching JSON file there.
I forked raywo's original module; my changes are described above.
- Original module: raywo — MMM-NowPlayingOnSpotify (MIT).
