Spotify API + Next.jsで作るPlaylist作成アプリ

# はじめに
Spotify APIの機能を使ってユーザーのTop Artist、Top TracksからPlaylistを作ることのできるアプリケーションを作ったので紹介します。

# 使用したライブラリなど
React
Next.js
Vercel
axios
Spotify API
- Artists API
- Tracks API
- Playlists API
- Personalization API
- Users Profile API

スタイルはCSS Modulesで実装しました。

# アプリケーション概要
■URL
[https://spellista.tky-ngnm.net](https://spellista.tky-ngnm.net)
![スクリーンショット 2021-07-27 15.12.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307986/49e9ff4d-1531-4cb9-1cb4-ec556dc0e382.png)
![スクリーンショット 2021-07-27 15.13.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307986/e2137683-0e42-0430-a2de-e704c8273dd7.png)

## 使い方
Sign in with SpotifyボタンでSpotifyにログインします。
そのあと、Top TracksボタンもしくはTop Artistボタンでどちらかのページに遷移します。
ログインしたユーザーの全期間のTop Tracksが表示され、"Last month"で約4週間分、"Last 6 month"で約半年分のデータが表示されます。
Top Tracksページでは再生ボタンで曲の視聴をできるようにしています。
最後に右下の"Create Playlist"ボタンでSpotifyアカウントに表示されているデータをもとにプレイリストを作成します。

# 実装

## Spotify API

### Step1 認証
[Spotify for Developers](https://developer.spotify.com/dashboard/)にログインし"CREATE AN APP"でアプリケーションの作成と設定を行います。
作成したアプリケーションの詳細画面に`Client ID`と`Client Secret`があり認証に使用します。
"Client Secret"はSHOW CLIENT SECRETをクリックすると表示できます。
"EDIT SETTING"ボタンで設定を行います。
`Redirect URIs`にリダイレクトURIを設定します。
リダイレクトURIはユーザーが認証の許可または拒否したあとにリダイレクトするURIです。
今回は開発用では`http://localhost:3000`、本番では[`https://spellista.tky-ngnm.net/`](https://spellista.tky-ngnm.net/)にしました。
![スクリーンショット_2021-07-27_17_12_24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307986/92979af1-e005-f915-dcb5-e4ae20af13fc.png)
![スクリーンショット 2021-08-08 14.23.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307986/cef87eb9-32bb-cd5a-e177-f90f629bc5b5.png)

認証フローは"[Authorization Code Flow](https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow)"で実装しました。

以下の順番で行います。
1. "[https://accounts.spotify.com/authorize](https://accounts.spotify.com/authorize)"にリクエストを送る。
2. ユーザーが認証画面でアカウント上のSpotifyの操作を許可する。
3. 設定したURIにリダイレクトされ、レスポンスに`code`と`state`のパラメータが返ってくる。
4. `code`、`redirect_uri`、`Client ID`、`Client Secret`を入れて"[https://accounts.spotify.com/api/token](https://accounts.spotify.com/api/token)"にリクエスト。このリクエストに入れる`redirect_uri`は実際にリダイレクトされるわけではありませんが、設定したリダイレクトURIと一致している必要があります。
5. "access token"と"refresh token"が返ってくる。

```index.js
import axios from 'axios';
import React, { useEffect } from 'react';
import auth from '../lib/auth.js';

useEffect(() => {
    const getAccessToken = async () => {
      const endpoint = `${config.BASE_URL}/api/spotify/getAccessToken`;
      const params = {
        code: new URL(window.location.href).searchParams.get('code'),
      };
      // params.codeに値が無ければreturn
      if (! params.code) return;

      const response = await axios
        .get(endpoint, { params })
        .then((res) => res.data.data)
        .catch((error) => {
          const statusCode = error.response.status;
          if (statusCode === 500) {
            alert('Spotifyのサーバーで障害が起きています。復旧までお待ちください。');
            return false;
          }
        });
      // localStorageにTokenをセット
      localStorage.setItem('accessToken', response.access_token);
      localStorage.setItem('refreshToken', response.refresh_token);

      // Tokenの有効期限を時間に直してセット
      const now = new Date();
      const expiration = response.expires_in / 60 / 60;
      now.setHours(now.getHours() + expiration);
      localStorage.setItem('expiredAt', now);

      // ブラウザURL部分のクエリを削除
      const url = new URL(window.location.href);
      url.searchParams.delete('code');
      url.searchParams.delete('state');
      history.pushState({}, '', url);
    };
    getAccessToken();
  }, []);
// 省略

 return (
 	 <div>
 	  <button
      className={`${buttonStyles.button} ${buttonStyles.login}`}
      onClick={auth}>
      Sign in with Spotify
    </button>
   </div>
 )
}
```

```JavaScript
import axios from 'axios';
import config from '../config';

const auth = () => {
  const endpoint = `${config.BASE_URL}/api/spotify/auth`;
  axios.get(endpoint).then((res) => {
    window.location.href =
      'https://accounts.spotify.com' + res.data.redirect_url;
  }).catch((error) => {
    alert('致命的なエラーです。サーバー管理者にお問い合わせください。');
  });
};

export default auth;
```

```JavaScript
// pages/api/spotify/auth.js
import axios from 'axios';
import config from '../../../config/index.js';

const generateRandomString = (length) => {
  let text = '';
  const possible =
    'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';

  for (let i = 0; i < length; i++) {
    text += possible.charAt(Math.floor(Math.random() * possible.length));
  }
  return text;
};

export default async (req, res) => {
  const params = new URLSearchParams();
  const scopes =
    'user-read-private user-read-email user-read-recently-played user-top-read playlist-modify-public playlist-modify-private streaming';
  const state = generateRandomString(16);
  params.append('client_id', process.env.CLIENT_ID);
  params.append('response_type', 'code');
  params.append('redirect_uri', config.BASE_URL);
  params.append('scope', scopes);
  params.append('state', state);
  const endpoint = 'https://accounts.spotify.com/authorize';
  const response = await axios.get(endpoint, { params });

  res.end(JSON.stringify({ redirect_url: response.request.path }));
};
```

```JavaScript
// pages/api/spotify/getAccessToken.js
import axios from 'axios';
import config from '../../../config/index.js';

export default async (req, res) => {
  const params = new URLSearchParams();
  params.append('grant_type', 'authorization_code');
  params.append(
    'code',
    new URL(config.BASE_URL + req.url).searchParams.get('code')
  );

  params.append('redirect_uri', config.BASE_URL);
  const endpoint = 'https://accounts.spotify.com/api/token';
  const response = await axios.post(endpoint, params, {
    headers: {
      Authorization: `Basic ${Buffer.from(
        `${process.env.CLIENT_ID}:${process.env.CLIENT_SECRET}`,
        'utf-8'
      ).toString('base64')}`,
    },
  });
  res.end(JSON.stringify({ data: response.data }));
};
```
"Sign in with Spotify"ボタンでauth関数を実行し`pages/api/auth.js`にリクエストします。
Client IDやアプリケーションに必要なスコープなどを設定し`https://accounts.spotify.com/authorize`にリクエストをるとSpotifyから`code`と`state`パラメータが返ってくるので、問題なければ`getAccessToken`関数が実行されます。
access tokenを取得するにはリクエストボディに`grant_type`、`code`、`redirect_uri`を、AuthorizationヘッダーにはBase64でエンコードしたClient IDとClient Secretを入れる必要があります。
最後にindex.jsにレスポンスを返しlocalStorageに`access token` と `refresh token` を保存して認証は完了です。
`access token` を使って様々なSpotify APIを使用します。
Next.jsは.envで定義するとビルド時にprocess.envに設定してくれていて、Client IDとClient Secretは.envファイルに書いた内容にアクセスしています。
またNext.jsではapiフォルダ内のファイルはサーバー側でのみアクセス可能になるのでClient Secretなどフロント側で露出したくないコードを記述します。

### Step2 楽曲一覧表示
楽曲一覧を表示するには`Personalization API`の`Get a User's Top Artists and Tracks` を使います。

[Personalization API](https://developer.spotify.com/documentation/web-api/reference/#category-personalization)を使用するのに必要なパラメータは以下の通りです。
- `type`  artistsもしくはtracks ※必須
- `time_range` long_term、medium_termもしくはshort_term ※任意、デフォルトはmedium_term
- `limit` 返されるデータの数 ※任意
- `offset` 返されるデータのインデックスを指定できます。 ※任意

Spotify APIと通信する部分はSpotifyApiクラスにして、インスタンスを作り使用するようにしました。

```JavaScript
// SpotifyApi.js

import axios from 'axios';
import config from '../config';

export class SpotifyApi {
  constructor() {
    this.accessToken = localStorage.getItem('accessToken');
    this.headers = {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${this.accessToken}`,
    };
    this.userId = null;
    this.playlistId = null;
  }

  // ユーザーのTop Tracks取得
  async getTopTracksByUser() {
    return await axios
      .get(`${config.API_URL}/me/top/tracks?limit=15`, { headers: this.headers })
      .then((res) => {
        return res.data.items;
      })
      .catch((error) => {
        return error.response.data;
      });
  }
}
```
Spotify APIの通信に必要になるaccessTokenなどはプロパティとして保持します。

```tracks.js
export default function tracks() {
	const [tracks, setTracks] = useState([]);

	useEffect(() => {
	    spotifyAPI.current = new SpotifyApi();

	    const getTracks = async () => {
	      const accessToken = localStorage.getItem('accessToken');
	      if (!accessToken) {
	        throw 'アクセストークンを取得できていません';
	      }
	      checkExpiration();

	      // Spotify ユーザーのTOP Tracks取得
	      const response = await spotifyAPI.current.getTopTracksByUser();
	      if (response.error) {
	        alert(
	          `認証エラーです。\n${response.error.status} ${response.error.message}`
	        );
	        location.href = '/';
	        return;
	      }
	      setTracks(response);
	      installWebPlayer();
	    };
	    getTracks();
	  }, []);

	const displayTracks = tracks.map((track, i) => {
	    return (
	      <li key={track.id} className={contentStyles.list}>
	        <span className={contentStyles.order_number}>{i + 1}</span>
	        <img
	          className={contentStyles.img}
	          src={track.album.images[1].url}
	          alt={track.name}
	        />
	        <span className={contentStyles.music_info}>
	          <p className={contentStyles.content_name}>{track.name}</p>
	          <p className={contentStyles.genre_info}>{track.artists[0].name}</p>
	        </span>
	        <button
		        className={buttonStyles.play_icon}
	          onClick={() => playbackTrack(track)}>
	          <Image
	            src={playingTrack === track ? '/stop.png' : '/play.png'}
	            alt="再生する"
	            width={30}
	            height={30}
	          />
	        </button>
	      </li>
	    );
	});
	
  return (
    <div>
      <ul>{ displayTracks }</ul>
    </div>
  )
}
```
useEffectを使うことでレンダリングされたときに`getTracks()` を実行し`checkExpiration()` でAccess Tokenの有効期限チェックを行います。
問題なければSpotifyApiクラスの`getTopTracksByUser()` で曲情報を取得しstateに入れます。
`displayTracks` で曲の画像や曲名などを表示します。

### Step3 楽曲期間別 一覧表示
楽曲期間別一覧表示で使うAPIはStep2と同じ`Personalization API` です。
`time_range` を変更してデータを取得し期間別の表示を実装します。

```JavaScript
import axios from 'axios';
import config from '../config';

export class SpotifyApi {

  //　省略

  // 期間ごとのartist, tracksの出しわけ
  async getDataByTerm(term, type) {
    let endpoint;
    switch (type) {
      case 'artists':
        endpoint = `${config.API_URL}/me/top/artists?time_range=${term}&limit=15`;
        break;
      case 'tracks':
        endpoint = `${config.API_URL}/me/top/tracks?time_range=${term}&limit=15`;
    }

    return await axios
      .get(endpoint, { headers: this.headers })
      .then((res) => {
        return res.data;
      })
      .catch((error) => {
        const statusCode = error.response.status;
        let message = messagesByErrorCode[statusCode];
        alert(`${message}\n${error.response.status} ${error.response.message}`);
        return error.response.data;
      });
  }
}
```
期間別一覧はartist、tracksともに同じメソッドを使用します。

```JavaScript
import React, { useEffect, useState, useRef, useCallback } from 'react';
import Image from 'next/image';
import styles from '../styles/layout/Layout.module.scss';
import contentStyles from '../styles/layout/Content.module.scss';
import buttonStyles from '../styles/components/Button.module.scss';
import checkExpiration from '../lib/checkExpiration';
import Header from '../components/Header';
import { SpotifyApi } from '../lib/SpotifyApi';

export default function tracks() {
  const [tracks, setTracks] = useState([]);

  // 省略

  const getTrackByTerm = async (term) => {
    const response = await spotifyAPI.current.getDataByTerm(term, 'tracks');
    if (response.error) return;
    setTracks(response.items);
  };

  return (
    <div className={styles.container}>
      <Header currentPage={'tracks'} title={'Top Tracks'} />
      <main className={styles.main}>
        <section className={contentStyles.sec_contents}>
          <div className={contentStyles.time_range_selector}>
            <button
              className={contentStyles.timeRange}
              onClick={() => getTrackByTerm('short_term')}>
              Last month
            </button>
            <button
              className={contentStyles.timeRange}
              onClick={() => getTrackByTerm('medium_term')}>
              Last 6 month
            </button>
            <button
              className={contentStyles.timeRange}
              onClick={() => getTrackByTerm('long_term')}>
              All time
            </button>
          </div>
          <ul>{displayTracks}</ul>
        </section>
      </main>
    </div>
  );
}
```
`onClick`イベントで`getTrackByTerm`を実行し引数に期間を入れることで`time_range`を変更します。

### Step4 Playlist作成
Playlistの作成です。Playlistの作成には`User Profile API` 、`Playlists API` を使用します。また、曲のSpotifyIDが必要です。

`User Profile API`
- Get Current User's Profile

`Playlists API` 
- Create a Playlist
- Add Items to a Playlist

Playlistを作成の流れは以下です。
1. Spotify User IDを取得する
2. Spotify User IDを使って空のプレイリストを作成、Playlist IDを取得する
3. Playlist IDを使ってプレイリストに曲を入れる

```JavaScript
// SpotifyApi.js
export class SpotifyApi {
  // 省略

  // Spotify UserID 取得
  async getUserId() {
    if (this.userId) return;

    return await axios
      .get(`${config.API_URL}/me`, { headers: this.headers })
      .then((res) => {
        this.userId = res.data.id;
        return res.data.id;
      })
      .catch((error) => {
        const statusCode = error.response.status;
        let message = messagesByErrorCode[statusCode];
        alert(
          `${message}\n${error.response.status} ${error.response.data.error.message}`
        );
        location.href = "/";
        return;
      });
  }

  // SpotifyのUserアカウントに空のplaylistを作成しIDを取得
  async getPlaylistId(playlistsConfig) {
    await this.getUserId();

    return await axios
      .post(
        `${config.API_URL}/users/${this.userId}/playlists`,
        playlistsConfig,
        {
          headers: this.headers,
        }
      )
      .then((res) => {
        this.playlistId = res.data.id;
        return res.data.id;
      })
      .catch((error) => {
        const statusCode = error.response.status;
        let message = messagesByErrorCode[statusCode];
        alert(
          `${message}\n${error.response.status} ${error.response.data.error.message}`
        );
        location.href = '/';
        return;
      });
  }

  // playlistを作成する
  async createPlaylist(tracks_uri) {
    if (!this.playlistId) {
      await this.getPlaylistId();
    }
    return await axios
      .post(
        `${config.API_URL}/playlists/${this.playlistId}/tracks`,
        tracks_uri,
        { headers: this.headers }
      )
      .then((res) => {
        return res.status;
      })
      .catch((error) => {
        const statusCode = error.response.status;
        const message = messagesByErrorCode[statusCode];
        alert(
          `${message}\n${error.response.status} ${error.response.data.error.message}`
        );
        location.href = '/';
        return;
      });
  }
}
```
Spotify User IDは1度だけ取得すれば良いのでSpotifyApiクラスのプロパティとして持つようにしました。

```JavaScript
// tracks.js
// 省略

export default function tracks() {
  const [tracks, setTracks] = useState([]);
　　　　const [flag, setFlag] = false;
  // 省略

  const createPlaylistHandler = async () => {
    const playlistsConfig = {
      name: 'Playlists of your favorite tracks',
      description: 'Playlists of your favorite tracks',
      public: true,
    };
    await spotifyAPI.current.getPlaylistId(playlistsConfig);

    const tracks_uri = await tracks.map((track) => {return track.uri});
    const responseStatus = await spotifyAPI.current.createPlaylist(tracks_uri);
    if (responseStatus === 201) setFlag(true);
  };

  const closeModal = useCallback(() => setFlag(false), []);

  return (
    <div className={styles.container}>
      <Header currentPage={'tracks'} title={'Top Tracks'} />
      <main className={styles.main}>
        <section className={contentStyles.sec_contents}>
          // 省略

          <ul>{displayTracks}</ul>
          <div className={contentStyles.create_playlists}>
            <div className={contentStyles.create_playlists_inner}>
              <button
                className={`${buttonStyles.button} ${buttonStyles.playlist}`}
                onClick={createPlaylistHandler}>
                Create Playlist
              </button>
            </div>
          </div>
        </section>
      </main>
      <Modal flag={flag} closeModal={closeModal} />
    </div>
  );
}
```
`tracks.js` では`createPlaylistHandler` メソッドを実行します。
`playlistsConfig` は空のプレイリストを作成するときにリクエストボディに入れるためのオブジェクトです。プレイリストの名前、説明、公開するかどうかを設定できます。
このアプリケーションでは実装しませんでしたが、プレイリストの名前や説明の部分をユーザー入力にすれば自由にプレイリストの名前をつけることもできるはずです。
`tracks_uri` ではstateに曲情報が入っているので曲のURIが入った新しい配列を作ります。これがプレイリストに入る曲になります。
問題なく処理が進むと最後に作成しました！というモーダルがでるので、プレイリストの作成が完了します。

# 気を使った点
## Spotify APIの機能をClassにしてまとめる
このアプリケーションではSpotify APIを使う部分がユーザーのTopTracksの一覧表示やプレイリストの作成など複数あります。
ひとつひとつ使用する部分で各ファイルで書いても良いのですが、クラス化することでSpotify APIの通信処理が一つのファイルにまとまり修正やエラーが出た時の切り分けなどがやりやすくなりました。

## デプロイ
Vercelを使いました。
VercelにログインしてGitHubのリポジトリを連携するだけで終わるのでとても簡単でした。

# まとめ
制作物: [https://spellista.tky-ngnm.net/](https://spellista.tky-ngnm.net/)
Spotify APIを使うとアーティスト情報や曲の情報など取得できて色々遊ぶことができますし、ドキュメントが充実しているので使いやすかったです。
ぜひ使ってみてください。

# 参考
- [Next.js Document](https://nextjs.org/docs/getting-started)
- [React Document](https://ja.reactjs.org/docs/getting-started.html)
- [axios](https://github.com/axios/axios)
- [Spotify API + Next.jsで作る選曲支援Webアプリ](https://qiita.com/Yuki_Oshima/items/82116e4044687b16ef60)