🎵

Next.js + TypeScript で Spotify Web Playback SDK の公式サンプルを書き直してみた

2021/11/18に公開

音楽配信サービス「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を参考にすると分かりやすいと思います。

前者のURIは公式のサンプルアプリを動作させるためのもので、後者は本記事で作成するアプリを動作させるためのものです。今回は開発環境のみで動かすためlocalhostのみを登録していますが、本番環境で動かすためには別途登録が必要となるので注意してください。


Redirect URIsの登録


参考: Authorization Code Flow

これでSpotify側での登録が完了しました!
次は、Spotifyが公開しているサンプリアプリを動かしてみましょう!

公式サンプルアプリを動かしてみる

ソースコードをダウンロード

Spotify公式のGuideではハンズオン形式でサンプルアプリの作り方を公開していますが、今回は完成形をいきなりダウンロードして実行したいと思います。ソースコードがGitHubで公開されているので、git cloneでダウンロードしましょう。

console
$ 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 IDClient Secretを環境変数として.envファイルに記述していきます

  • My Dashboardから黒くてカッコイイ詳細画面を開き、SHOW CLIENT SECRETをクリックする
  • 表示されたClient IDClient Secretをメモしておく
  • .envファイルを作成し、Client IDClient Secretを記述する
console
$ touch .env
.env
SPOTIFY_CLIENT_ID='my_client_id'
SPOTIFY_CLIENT_SECRET='my_client_secret'

パッケージインストール

npmでパッケージをインストールします

console
$ npm install

実行

npm run devで実行します

console
$ 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もつけましょう。

console
$ 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をインストールすることで利用することができるようになります。

console
$ 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のファイルを以下のように書き直します。

.eslintrc.json
{
  "extends": "next/core-web-vitals",
  "rules": {
    "@next/next/no-img-element": "off"
  }
}

スタイルの適用

スタイルを定義するCSSファイルは、公式のサンプルアプリのCSSファイルをそのままお借りしたいと思います。先ほど利用したspotify-web-playback-sdk-exampleプロジェクトのsrcディレクトリに入っているindex.cssApp.cssstylesディレクトリにコピーします。同時に、必要のないCSSファイルを削除します。

console
$ 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を以下のように書き換えます

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に対応します。

pages/index.tsx
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.tsxweb_playback.tsxを作成します。

console
$ mkdir components
$ touch components/login.tsx
$ touch components/web_playback.tsx
$ ls components
login.tsx		web_playback.tsx

login.tsxweb_playback.tsxに以下のコードを記述します。これらのファイルは、それぞれ公式サンプルアプリのsrc/Login.jssrc/WebPlayback.jsxに対応します。

components/login.tsx
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>
  );
};
components/web_playback.tsx
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();
                }}
              >
                &lt;&lt;
              </button>

              <button
                className="btn-spotify"
                onClick={() => {
                  player.togglePlay();
                }}
              >
                {is_paused ? "PLAY" : "PAUSE"}
              </button>

              <button
                className="btn-spotify"
                onClick={() => {
                  player.nextTrack();
                }}
              >
                &gt;&gt;
              </button>
            </div>
          </div>
        </div>
      </>
    );
  }
};

APIルートの作成

Spotifyの認証機能を使ってアクセストークンを得る際には、Client IDClient Secretなどを送信する必要がありますが、これをブラウザ側で実装してしまうとClient Secretなどの秘密情報をユーザーに公開してしまうことになります。これを避けるため、ログインに関する処理をサーバー側で実装する必要があります。

公式のサンプルアプリではhttp-proxy-middlewareを用いて擬似サーバーを構築していますが、Next.jsにはAPI Routesの機能が提供されているため、これを使って実装してみます。

また、公式のサンプルアプリではアクセストークンを擬似サーバーのグローバル変数に直接書き込んで保存していますが、本記事ではCookieに書き込む形で実装したいと思います。

pages/api/authディレクトリを作成し、その中にlogin.tsファイルとcallback.tsファイルを作成します。同時に不要なファイルを削除します。

console
$ mkdir pages/api/auth
$ touch pages/api/auth/login.ts pages/api/auth/callback.ts
$ rm pages/api/hello.ts

login.tscallback.tsに以下のコードを記述します。これらのファイルは、公式サンプルアプリのserver/index.jsに対応します。

login.ts
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;
callback.ts
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 IDClient Secretを環境変数として.env.localファイルに記述していきますこのファイルはあらかじめ.gitignoreによってGitで追跡されないようになっています。

console
$ touch .env.local
.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