Next.js + TypeScript で Spotify Web Playback SDK の公式サンプルを書き直してみた
音楽配信サービス「Spotify」はデベロッパー向けに様々なAPIやSDKを公開しており、Spotify Web Playback SDKもそのうちの一つです。このSDKを利用することで、自作のWebアプリにSpotifyのストリーミングサービスを組み込むことが可能になり、Premiumプランに加入しているユーザーはWebアプリを通して音楽を再生することができるようになります。
Spotify for Developersでは、Reactで書かれたサンプルアプリの作り方をGuideで紹介しています。また、ソースコードもGitHubで公開されています。
Spotify Web Playback SDKのGuideページ
本記事では、このサンプルアプリをNext.jsとTypeScriptを用いて書き直した過程をまとめたものです。完成形のソースコードはGitHubで公開しています。
Spotify Developersでの登録
Developerアカウントの登録
Spotify APIを利用するにあたり、SpotifyアカウントをDeveloperとして登録する必要があります。なお、Developerとして登録するアカウントはPremiumプランである必要はありません。
- SpotifyのMy Dashboardにアクセスし、Loginを押してログインする。(Spotifyのアカウントを持っていない場合は、Sign up for a free Spotify account hereからアカウント作成を行なってください)
- 利用規約に同意すると、Dashboard画面が開きます
My Dashboardページ - ログイン前
My Dashboardページ - ログイン後
アプリケーションの登録
Spotify Web Playback SDKを利用するにはSpotifyの認証機能を組み込む必要があり、そのためにはDashboardからアプリケーションを登録する必要があります。Spotifyの認証に関して詳しく知りたい方は、Authorization Guidesを参考にしてください。
- ログイン後のDashboardページから、CREATE AN APPをクリックします
- アプリ名とアプリ内容を記入し、利用規約等に同意した上でCREATEをクリックします
- 黒くてカッコイイ詳細画面が表示されます
アプリ名と内容を記入
詳細画面
この詳細画面には後に必要となるClient IDなどが表示されます。
リダイレクトURIの登録
Spotifyの認証機能を利用する際には、Spotify側でリダイレクトURIを登録しておく必要があります。リダイレクトURIがどう呼び出されるかについてはAuthorization Code Flowを参考にすると分かりやすいと思います。
- 黒くてカッコイイ詳細画面から、EDIT SETTINGSをクリックします
- Redirect URIsの欄に以下のURIを登録し、SAVEをクリックします
前者のURIは公式のサンプルアプリを動作させるためのもので、後者は本記事で作成するアプリを動作させるためのものです。今回は開発環境のみで動かすためlocalhostのみを登録していますが、本番環境で動かすためには別途登録が必要となるので注意してください。
Redirect URIsの登録
参考: Authorization Code Flow
これでSpotify側での登録が完了しました!
次は、Spotifyが公開しているサンプリアプリを動かしてみましょう!
公式サンプルアプリを動かしてみる
ソースコードをダウンロード
Spotify公式のGuideではハンズオン形式でサンプルアプリの作り方を公開していますが、今回は完成形をいきなりダウンロードして実行したいと思います。ソースコードがGitHubで公開されているので、git clone
でダウンロードしましょう。
$ git clone git@github.com:spotify/spotify-web-playback-sdk-example.git
$ cd spotify-web-playback-sdk-example
$ ls -a
. .git LICENSE package.json server
.. .gitignore README.md public src
環境変数の設定
Client IDとClient Secretを環境変数として.env
ファイルに記述していきます
- My Dashboardから黒くてカッコイイ詳細画面を開き、SHOW CLIENT SECRETをクリックする
- 表示されたClient IDとClient Secretをメモしておく
-
.env
ファイルを作成し、Client IDとClient Secretを記述する
$ touch .env
SPOTIFY_CLIENT_ID='my_client_id'
SPOTIFY_CLIENT_SECRET='my_client_secret'
パッケージインストール
npmでパッケージをインストールします
$ npm install
実行
npm run dev
で実行します
$ npm run dev
http://localhost:3000
にアクセスすると、Loginページが現れます。Premiumプランに加入したアカウントでログインをすると、「Instance not active. Transfer your playback using your Spotify app」と書かれたページが現れます。
http://localhost:3000 - ログイン前
http://localhost:3000 - ログイン後
ここで、Spotifyのアプリを開きSpotify Connectの機能を利用してデバイス接続を行います。下図のようなメニューを開き、Web Playback SDKを選択しましょう。うまくいかない時は、Spotifyアプリのアカウントが上記Premiumアカウントと一致しているかどうかを確認してみてください。
Web Playback SDKを選択してデバイスを接続
このマークが目印
http://localhost:3000 - デバイス接続後
上のような画面が出てきたら成功です!
PLAY/PAUSEボタンで曲の再生や一時停止ができるかどうか確認してみましょう!
Next.jsとTypeScriptで公式サンプルアプリのコピーを作る
プロジェクトの作成
create-next-appを用いてプロジェクトの作成をします。今回はTypeScriptを使用するため、オプションとして--ts
もつけましょう。
$ npx create-next-app@latest --ts
Need to install the following packages:
create-next-app@latest
Ok to proceed? (y) y
What is your project named? … sample-spotify-app
$ cd sample-spotify-app
$ ls
README.md package-lock.json styles
next-env.d.ts package.json tsconfig.json
next.config.js pages
node_modules public
パッケージインストール
npmで必要なパッケージをインストールします。Spotify Web Playback SDKの型は@types/spotify-web-playback-sdk
をインストールすることで利用することができるようになります。
$ npm install axios cookie
$ npm install -D @types/cookie @types/spotify-web-playback-sdk
ESlintの設定
Next.js version 11.0.0より、あらかじめESlintの設定をしてくれるようになりました。とてもありがたい機能なのですが、<img>
タグを利用するとNextの<Image>
コンポーネントを利用するよう注意が出る設定になっており、今回はスタイルを使い回す都合上<img>
タグをそのまま利用するので、その設定をオフにしたいと思います。
.eslintrc.json
のファイルを以下のように書き直します。
{
"extends": "next/core-web-vitals",
"rules": {
"@next/next/no-img-element": "off"
}
}
スタイルの適用
スタイルを定義するCSSファイルは、公式のサンプルアプリのCSSファイルをそのままお借りしたいと思います。先ほど利用したspotify-web-playback-sdk-example
プロジェクトのsrc
ディレクトリに入っているindex.css
とApp.css
をstyles
ディレクトリにコピーします。同時に、必要のないCSSファイルを削除します。
$ cp ../spotify-web-playback-sdk-example/src/index.css styles
$ cp ../spotify-web-playback-sdk-example/src/App.css styles
$ rm styles/globals.css styles/Home.module.css
$ ls styles
App.css index.css
コピーしたCSSファイルを適用するためにpages/_app.tsx
を以下のように書き換えます
import "../styles/index.css";
import "../styles/App.css";
import type { AppProps } from "next/app";
function MyApp({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />;
}
export default MyApp;
クライアントサイドの作成
クライアントサイドのコードを記述していきましょう。まずは、pages/index.tsx
を以下のように書き直します。このファイルは、公式サンプルアプリのsrc/App.js
に対応します。
import type { NextPage, GetServerSideProps } from "next";
import Head from "next/head";
import { Login } from "../components/login";
import { WebPlayback } from "../components/web_playback";
type Props = {
token: string;
};
const Home: NextPage<Props> = ({ token }) => {
return (
<>
<Head>
<title>Spotify Web Playback Example</title>
<meta
name="description"
content="An example app of Spotify Web Playback SDK based on Next.js and Typescript."
/>
</Head>
{token === "" ? <Login /> : <WebPlayback token={token} />}
</>
);
};
export const getServerSideProps: GetServerSideProps = async (context) => {
if (context.req.cookies["spotify-token"]) {
const token: string = context.req.cookies["spotify-token"];
return {
props: { token: token },
};
} else {
return {
props: { token: "" },
};
}
};
export default Home;
getServerSideProps
ではCookieからアクセストークンを取得しています。アクセストークンの有無でページの表示内容を変えています。もしトークンがあれば<WebPlayback token={token} />
コンポーネントを表示し、トークンが無ければ(空文字列であれば)<Login />
コンポーネントを表示するようにしています。
次は、この<Login />
コンポーネントと<WebPlayback token={token} />
コンポーネントを記述しましょう。まずはcomponents
ディレクトリを作成し、その中にlogin.tsx
とweb_playback.tsx
を作成します。
$ mkdir components
$ touch components/login.tsx
$ touch components/web_playback.tsx
$ ls components
login.tsx web_playback.tsx
login.tsx
とweb_playback.tsx
に以下のコードを記述します。これらのファイルは、それぞれ公式サンプルアプリのsrc/Login.js
とsrc/WebPlayback.jsx
に対応します。
import { VFC } from "react";
import Link from "next/link";
export const Login: VFC = () => {
return (
<div className="App">
<header className="App-header">
<Link href="/api/auth/login">
<a className="btn-spotify">Login with Spotify</a>
</Link>
</header>
</div>
);
};
import { VFC, useState, useEffect } from "react";
type Props = {
token: string;
};
export const WebPlayback: VFC<Props> = ({ token }) => {
const [is_paused, setPaused] = useState<boolean>(false);
const [is_active, setActive] = useState<boolean>(false);
const [player, setPlayer] = useState<Spotify.Player | null>(null);
const [current_track, setTrack] = useState<Spotify.Track | null>(null);
useEffect(() => {
const script = document.createElement("script");
script.src = "https://sdk.scdn.co/spotify-player.js";
script.async = true;
document.body.appendChild(script);
window.onSpotifyWebPlaybackSDKReady = () => {
const player = new window.Spotify.Player({
name: "Web Playback SDK",
getOAuthToken: (cb) => {
cb(token);
},
volume: 0.5,
});
setPlayer(player);
player.addListener("ready", ({ device_id }) => {
console.log("Ready with Device ID", device_id);
});
player.addListener("not_ready", ({ device_id }) => {
console.log("Device ID has gone offline", device_id);
});
player.addListener("player_state_changed", (state) => {
if (!state) {
return;
}
setTrack(state.track_window.current_track);
setPaused(state.paused);
player.getCurrentState().then((state) => {
if (!state) {
setActive(false);
} else {
setActive(true);
}
});
});
player.connect();
};
}, [token]);
if (!player) {
return (
<>
<div className="container">
<div className="main-wrapper">
<b>Spotify Player is null</b>
</div>
</div>
</>
);
} else if (!is_active) {
return (
<>
<div className="container">
<div className="main-wrapper">
<b>
Instance not active. Transfer your playback using your Spotify app
</b>
</div>
</div>
</>
);
} else {
return (
<>
<div className="container">
<div className="main-wrapper">
<div className=""></div>
{current_track && current_track.album.images[0].url ? (
<img
src={current_track.album.images[0].url}
className="now-playing__cover"
alt=""
/>
) : null}
<div className="now-playing__side">
<div className="now-playing__name">{current_track?.name}</div>
<div className="now-playing__artist">
{current_track?.artists[0].name}
</div>
<button
className="btn-spotify"
onClick={() => {
player.previousTrack();
}}
>
<<
</button>
<button
className="btn-spotify"
onClick={() => {
player.togglePlay();
}}
>
{is_paused ? "PLAY" : "PAUSE"}
</button>
<button
className="btn-spotify"
onClick={() => {
player.nextTrack();
}}
>
>>
</button>
</div>
</div>
</div>
</>
);
}
};
APIルートの作成
Spotifyの認証機能を使ってアクセストークンを得る際には、Client IDやClient Secretなどを送信する必要がありますが、これをブラウザ側で実装してしまうとClient Secretなどの秘密情報をユーザーに公開してしまうことになります。これを避けるため、ログインに関する処理をサーバー側で実装する必要があります。
公式のサンプルアプリではhttp-proxy-middleware
を用いて擬似サーバーを構築していますが、Next.jsにはAPI Routesの機能が提供されているため、これを使って実装してみます。
また、公式のサンプルアプリではアクセストークンを擬似サーバーのグローバル変数に直接書き込んで保存していますが、本記事ではCookieに書き込む形で実装したいと思います。
pages/api/auth
ディレクトリを作成し、その中にlogin.ts
ファイルとcallback.ts
ファイルを作成します。同時に不要なファイルを削除します。
$ mkdir pages/api/auth
$ touch pages/api/auth/login.ts pages/api/auth/callback.ts
$ rm pages/api/hello.ts
login.ts
とcallback.ts
に以下のコードを記述します。これらのファイルは、公式サンプルアプリのserver/index.js
に対応します。
import type { NextApiRequest, NextApiResponse } from "next";
const generateRandomString = (length: number): string => {
let text = "";
const possible =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (let i = 0; i < length; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
};
const login = (req: NextApiRequest, res: NextApiResponse) => {
const scope: string = "streaming user-read-email user-read-private";
const spotify_redirect_uri = "http://localhost:3000/api/auth/callback";
const state: string = generateRandomString(16);
let spotify_client_id: string = "";
if (process.env.SPOTIFY_CLIENT_ID) {
spotify_client_id = process.env.SPOTIFY_CLIENT_ID;
} else {
console.error(
'Undefined Error: An environmental variable, "SPOTIFY_CLIENT_ID", has something wrong.'
);
}
const auth_query_parameters = new URLSearchParams({
response_type: "code",
client_id: spotify_client_id,
scope: scope,
redirect_uri: spotify_redirect_uri,
state: state,
});
res.redirect(
"https://accounts.spotify.com/authorize/?" +
auth_query_parameters.toString()
);
};
export default login;
import type { NextApiRequest, NextApiResponse } from "next";
import { serialize, CookieSerializeOptions } from "cookie";
import axios from "axios";
type SpotifyAuthApiResponse = {
access_token: string;
token_type: string;
scope: string;
expires_in: number;
refresh_token: string;
};
export const setCookie = (
res: NextApiResponse,
name: string,
value: unknown
) => {
const stringValue =
typeof value === "object" ? "j:" + JSON.stringify(value) : String(value);
const options: CookieSerializeOptions = {
httpOnly: true,
secure: true,
path: "/",
};
res.setHeader("Set-Cookie", serialize(name, stringValue, options));
};
const callback = async (req: NextApiRequest, res: NextApiResponse) => {
const code = req.query.code;
const spotify_redirect_uri = "http://localhost:3000/api/auth/callback";
let spotify_client_id: string = "";
if (process.env.SPOTIFY_CLIENT_ID) {
spotify_client_id = process.env.SPOTIFY_CLIENT_ID;
} else {
console.error(
'Undefined Error: An environmental variable, "SPOTIFY_CLIENT_ID", has something wrong.'
);
}
let spotify_client_secret: string = "";
if (process.env.SPOTIFY_CLIENT_SECRET) {
spotify_client_secret = process.env.SPOTIFY_CLIENT_SECRET;
} else {
console.error(
'Undefined Error: An environmental variable, "SPOTIFY_CLIENT_SECRET", has something wrong.'
);
}
const params = new URLSearchParams({
code: code as string,
redirect_uri: spotify_redirect_uri,
grant_type: "authorization_code",
});
axios
.post<SpotifyAuthApiResponse>(
"https://accounts.spotify.com/api/token",
params,
{
headers: {
Authorization:
"Basic " +
Buffer.from(
spotify_client_id + ":" + spotify_client_secret
).toString("base64"),
"Content-Type": "application/x-www-form-urlencoded",
},
}
)
.then((response) => {
if (response.data.access_token) {
setCookie(res, "spotify-token", response.data.access_token);
res.status(200).redirect("/");
}
})
.catch((error) => {
console.error(`Error: ${error}`);
});
};
export default callback;
環境変数の設定
Client IDとClient Secretを環境変数として.env.local
ファイルに記述していきますこのファイルはあらかじめ.gitignore
によってGitで追跡されないようになっています。
$ touch .env.local
SPOTIFY_CLIENT_ID='my_client_id'
SPOTIFY_CLIENT_SECRET='my_client_secret'
実行
ここまで出来たら、npm run dev
で実行してみましょう!
localhost:3000
にアクセスし、公式のサンプルアプリと同じ挙動を示せば成功です!!
まとめ
Next.jsとTypeScriptを用いてSpotify Web Playback SDKの公式サンプルアプリを書き直すことができました。本記事で作成したWebアプリは、公式のサンプルアプリと比較して、Next.js特有の機能であるAPI Routingを用いたログイン認証が一番の特徴になると思います。
この記事が、皆さまの開発の一助になれば幸いです!!
Discussion