Skip to content

Instantly share code, notes, and snippets.

@amane-katagiri
Last active March 29, 2026 03:03
Show Gist options
  • Select an option

  • Save amane-katagiri/facc46bfd4709109e0ea46e8920b36f6 to your computer and use it in GitHub Desktop.

Select an option

Save amane-katagiri/facc46bfd4709109e0ea46e8920b36f6 to your computer and use it in GitHub Desktop.
10秒に一回のペースでSpotifyから再生中の曲を取得してSSEで配信する簡単なスクリプト
cd /d "%~dp0"
set SPOTIFY_CLIENT_ID=xxx
set SPOTIFY_CLIENT_SECRET=yyy
node spotify-now-playing.mjs
// @ts-check
// Spotify Now Playing SSE Server
// Usage: node spotify-now-playing.mjs
import { createServer } from "node:http";
import { exec } from "child_process";
const CLIENT_ID = process.env.SPOTIFY_CLIENT_ID ?? "YOUR_CLIENT_ID";
const CLIENT_SECRET = process.env.SPOTIFY_CLIENT_SECRET ?? "YOUR_CLIENT_SECRET";
const REDIRECT_URI = "http://127.0.0.1:3000/callback";
const SCOPES = "user-read-currently-playing user-read-playback-state";
const POLL_INTERVAL = 10_000;
const PORT = 3000;
let accessToken = "";
let refreshToken = "";
let tokenExpiresAt = 0;
const basicAuth = () =>
Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString("base64");
/**
* @param {string} code
*/
async function exchangeCode(code) {
const res = await fetch("https://accounts.spotify.com/api/token", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Authorization: `Basic ${basicAuth()}`,
},
body: new URLSearchParams({
grant_type: "authorization_code",
code,
redirect_uri: REDIRECT_URI,
}),
});
const data = await res.json();
accessToken = data.access_token;
refreshToken = data.refresh_token;
tokenExpiresAt = Date.now() + data.expires_in * 1000;
}
async function ensureToken() {
if (Date.now() < tokenExpiresAt - 60_000) return;
if (!refreshToken) return;
const res = await fetch("https://accounts.spotify.com/api/token", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Authorization: `Basic ${basicAuth()}`,
},
body: new URLSearchParams({
grant_type: "refresh_token",
refresh_token: refreshToken,
}),
});
const data = await res.json();
accessToken = data.access_token;
if (data.refresh_token) refreshToken = data.refresh_token;
tokenExpiresAt = Date.now() + data.expires_in * 1000;
}
async function getNowPlaying() {
await ensureToken();
if (!accessToken) return null;
const res = await fetch(
"https://api.spotify.com/v1/me/player/currently-playing",
{ headers: { Authorization: `Bearer ${accessToken}` } },
);
if (res.status === 204 || res.status === 401) return null;
const data = await res.json();
if (!data.item) return null;
const track = data.item.name;
const artists = data.item.artists
.map((/** @type {{ name: string; }} */ a) => a.name)
.join(", ");
return `${track} - ${artists}`;
}
createServer(async (req, res) => {
const url = new URL(req.url ?? "/", `http://127.0.0.1:${PORT}`);
if (url.pathname === "/") {
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
res.end(
accessToken
? `GET http://127.0.0.1:${PORT}/now-playing から楽曲情報をSSEで配信します`
: `GET http://127.0.0.1:${PORT}/login からSpotifyで認証を行ってください`,
);
return;
}
if (url.pathname === "/login") {
const authUrl = new URL("https://accounts.spotify.com/authorize");
authUrl.searchParams.set("response_type", "code");
authUrl.searchParams.set("client_id", CLIENT_ID);
authUrl.searchParams.set("scope", SCOPES);
authUrl.searchParams.set("redirect_uri", REDIRECT_URI);
res.writeHead(302, { Location: authUrl.toString() });
res.end();
return;
}
if (url.pathname === "/callback") {
const code = url.searchParams.get("code");
if (!code) {
res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
res.end("コードがありません");
return;
}
await exchangeCode(code);
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
res.end(
`GET http://127.0.0.1:${PORT}/now-playing から楽曲情報をSSEで配信します`,
);
return;
}
if (url.pathname === "/now-playing") {
if (!accessToken) {
res.writeHead(401, { "Content-Type": "text/plain; charset=utf-8" });
res.end(
`GET http://127.0.0.1:${PORT}/login からSpotifyで認証を行ってください`,
);
return;
}
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
"Access-Control-Allow-Origin": "*",
});
let alive = true;
let lastTrack = "";
req.on("close", () => {
alive = false;
});
while (alive) {
try {
const track = await getNowPlaying();
if (track != null && track !== lastTrack) {
res.write(`data: 再生中:${track}\n\n`);
lastTrack = track;
}
} catch (e) {
console.error("polling error:", e);
res.write("data: 楽曲情報の取得に失敗しました\n\n");
}
await new Promise((r) => setTimeout(r, POLL_INTERVAL));
}
return;
}
res.writeHead(404, { "Content-Type": "text/plain" });
res.end();
}).listen(PORT, () => {
console.log(`Spotify Now Playing SSE Server: http://127.0.0.1:${PORT}/`);
console.log(
`http://127.0.0.1:${PORT}/login からSpotifyで認証を行ってください`,
);
exec(`start http://127.0.0.1:${PORT}/login`);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment