Writing
Jan 20, 2023

给你的博客添加 Spotify 播放状态

使用 Spotify API 和 Next.js 实现,上手简单。
1822 字,预计阅读时长 9 分钟

这两天在设计新的博客界面,做了一个 Spoyify 音乐播放的动画,为了实现同步 Spotify 当前正在播放歌曲的状态,研究了一下 Spotify 的 API,在这里记录一下实现的过程。

创建一个 Spotify 应用

Spotify 对于开发者没有资格限制,只要你有一个 Spotify 账号,就能利用 API 进行开发。你需要首先前往 Developer Dashboard 创建一个应用。名称和描述是用户在使用你的应用时能够看到的内容,尽可能与应用的用途相关,也方便自己识别。

创建应用后,会来到应用详情页面,在这里可以获取应用的 Client ID 和 Client Secret,相当于专属于这个应用的账号和密码。在后续授权的流程中会使用到这两个信息。

应用创建后,即使你创建这个应用时使用的 Spotify 账户也是不会自动授权的,因此我们需要让应用能够获取 Spotify 的账户信息,即授权。在此之前需要编辑一下应用设置,填入图示的两个 Redirect URIs,这在后续的授权过程中会用到。

授予应用 Spotify 账户权限

账号授权的流程说明可以参考官方的指南。简单来说,为了让博客能够实时获取 Spotify 的播放状态,需要首先由我们的应用向 Spotify 的账号服务发起获取某个指定范围内的权限的请求,然后我们再使用自己的 Spotify 账号验证登录,并同意授权。授权成功之后会返回 access token 和 refresh token,前者用于每次利用 API 请求时进行身份验证,后者用于刷新前者,因为 access token 每过一个小时就会过期。

Spotify 应用授权流程。
Spotify 应用授权流程。

由于我们只是希望为博客加上一个播放状态,用户只有自己,因此并不需要专门为此开发一个用于授权的应用。Spotify 官方提供了一个简单的授权示例项目,我们可以用这个项目来进行授权并获取 access token 和 refresh token。

Fork 示例项目,参考 GitHub 上的说明在本地运行 authorization_code 这个文件夹下的项目,运行前需要修改 client_idclient_secretscopeScope 指的是你的应用需要的权限范围,由于我们需要使用 Spotify 官方 API 中的 Player - Get Currently Playing Track,因此需要加上 user-read-playback-stateuser-read-currently-playing

运行并授权之后,你将会看到这个页面,说明你的账号已经给这个应用授权了。上面给出了返回的 access token 和 refresh token,把这两个 token 复制下来,然后开始测试 API。

授权结果,在此复制 token。
授权结果,在此复制 token。

你也可以在 Spotify 账户设置页面中查看你当前已经授权的应用。如果你在授权后又在代码中修改了 scope,那么是需要重新进行授权才能获得新的权限的,可以先移除当前的应用权限,然后重新运行授权程序。

查看自己授权过的 Spotify 应用。
查看自己授权过的 Spotify 应用。

测试 Spotify API 是否可用

为了避免同时处理 API 的 问题和请求的问题,我们先在 Postman 里面测试一下 API 是否可用。创建一个新的 Collection,在变量中加入 access_token,粘贴你刚才复制的值。

新建一个 Refresh Access Token 请求。在 Authorization 中选择 Basic Auth,并且在 Username 和 Password 中填入应用的 client id 和 client secret。在 Body 中如图填入 grant_type 和刚刚复制的 refresh_token。在 Test 中填入下面的代码,这样在每次请求之后就能更新 access_token 变量了。

pm.collectionVariables.set("access_token", pm.response.json().access_token);

通过这个请求,你可以刷新 access token,请求成功时的返回值如下。

然后我们再来测试获取当前播放曲目的 API。新建一个 Get Currently Playing Track 请求,在 Header 中修改 Content-Type、Authorization 和 Host 的值为如图所示。

如果返回结果正确,说明你的 Spotify 账户和应用能够正常使用 Spotify API 了,接下来我们再来调试博客中的请求,并且实现 access token 的自动刷新。

实现博客中的播放状态同步

前面提到,access token 每过一个小时就会过期,因此不能仅仅使用 access token 来向 API 发送请求,我们需要一个机制能够在 access token 过期后自动重新获取,这里就是 refresh token 派上用场的地方。

Refresh token 是不会过期的,因此你可以将它保存在博客的环境变量中,每当 access token 过期后,用它和应用的 client id 以及 client secret 来重新获取一个新的 access token。

在这里为了方便我直接将它们保存为了常量,通过 getAccessToken() 请求获取到新的 access token,并且通过 getCurrentPlayingTrack(accessToken) 请求来获取 Spotify 的播放状态信息。

// "@/utils/request.ts" import { SPOTIFY_CLIENT_ID, SPOTIFY_CLIENT_SECRET, SPOTIFY_REFRESH_TOKEN } from "./constants"; import querystring from "querystring"; // Get access token using refresh token export const getAccessToken = async () => { return fetch("https://accounts.spotify.com/api/token", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", "Authorization": `Basic ${Buffer.from(`${SPOTIFY_CLIENT_ID}:${SPOTIFY_CLIENT_SECRET}`).toString("base64")}`, }, body: querystring.stringify({ grant_type: "refresh_token", refresh_token: SPOTIFY_REFRESH_TOKEN, }) }) .then(res => { if (res.ok) { return res.json() } else { return res; } }) .catch((error) => { return Promise.reject(error); }); }; // Request for current playing status export const getCurrentPlayingTrack = async (accessToken: string) => { return fetch("https://api.spotify.com/v1/me/player/currently-playing", { method: "GET", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${accessToken}`, "Host": "api.spotify.com", }, }) .then(res => { if (res.ok) { return res.json() } }) .catch((error) => { return Promise.reject(error); }); };

在每次获取到 access token 之后,将其保存到浏览器的 local storage 中,并在过期之后重新刷新。这样每次发送 API 请求时,只需要从 local storage 读取最新的 access token 就行了。

为了同步更新 Spotify 的播放状态,使用 useEffect() 钩子设置一定的间隔时间进行轮询,获取最新的播放状态信息,并且更新给对应的组件状态。这样做虽然有点浪费网络资源,但实现起来最简单。

// “@components/NavigationBar/index.tsx" import SpotifyCard from "./components/SpotifyCard"; import { getCurrentPlayingTrack, getAccessToken } from "@/utils/request"; // Spotify - in navigation bar component const [isSpotifyPlaying, setIsSpotifyPlaying] = useState(false); const [imgLink, setImgLink] = useState(""); const [trackName, setTrackName] = useState(""); const [trackLink, setTrackLink] = useState(""); const [artistName, setArtistName] = useState(""); // Save new access token to local storage const updateAccessToken = async () => { const res = await getAccessToken(); const accessToken = res.access_token; localStorage.setItem("spotify_access_token", accessToken); }; // Update Spotify status useEffect(() => { const interval = setInterval(() => { updateSpotifyStatus() }, 200); return () => clearInterval(interval); }); const updateSpotifyStatus = async () => { const accessToken = localStorage.getItem("spotify_access_token"); // Get access token on first load if (accessToken === null) { await updateAccessToken(); updateSpotifyStatus(); } else { const res = await getCurrentPlayingTrack(accessToken); // Refresh access token if expired if (res === undefined) { await updateAccessToken(); } if (res && res.is_playing) { setIsSpotifyPlaying(true); setImgLink(res.item.album.images[0].url); setTrackName(res.item.name); setTrackLink(res.item.external_urls.spotify); setArtistName(res.item.artists[0].name); } else { setIsSpotifyPlaying(false); setImgLink(""); setTrackName(""); setTrackLink(""); setArtistName(""); } } } return ( {isSpotifyPlaying ? <SpotifyCard isPlaying={true} imgLink={imgLink} trackLink={trackLink} trackName={trackName} artistName={artistName} /> : <SpotifyCard isPlaying={false}/>} )

在上面的代码中我将更新播放状态的请求间隔设置为了 200 毫秒,这个数字是从《串流先锋》中看到的,算是一个小小的彩蛋。如果请求发送得太频繁会报错 451,200 毫秒作为间隔基本上能够即时同步了。

以上。

参考资料:

  • 注册开发者账户并创建应用:Your Dashboard|Spotify
  • 账号授权流程:Authorization Code Flow | Spotify for Developers
  • 账号授权示例项目:Spotify Accounts Authentication Examples
  • 账号权限范围:Authorization Scopes | Spotify for Developers
  • Spotify API 文档:Web API Reference | Spotify for Developers
  • 管理 Spotify 账户授权应用状态:Manage Apps - Spotify