Last active
March 29, 2026 03:03
-
-
Save amane-katagiri/facc46bfd4709109e0ea46e8920b36f6 to your computer and use it in GitHub Desktop.
10秒に一回のペースでSpotifyから再生中の曲を取得してSSEで配信する簡単なスクリプト
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| cd /d "%~dp0" | |
| set SPOTIFY_CLIENT_ID=xxx | |
| set SPOTIFY_CLIENT_SECRET=yyy | |
| node spotify-now-playing.mjs |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // @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