LoginSignup
8
7

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-08-08

はじめに

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
スクリーンショット 2021-07-27 15.12.25.png
スクリーンショット 2021-07-27 15.13.01.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にログインし"CREATE AN APP"でアプリケーションの作成と設定を行います。
作成したアプリケーションの詳細画面にClient IDClient Secretがあり認証に使用します。
"Client Secret"はSHOW CLIENT SECRETをクリックすると表示できます。
"EDIT SETTING"ボタンで設定を行います。
Redirect URIsにリダイレクトURIを設定します。
リダイレクトURIはユーザーが認証の許可または拒否したあとにリダイレクトするURIです。
今回は開発用ではhttp://localhost:3000、本番ではhttps://spellista.tky-ngnm.net/にしました。
スクリーンショット_2021-07-27_17_12_24.png
スクリーンショット 2021-08-08 14.23.07.png

認証フローは"Authorization Code Flow"で実装しました。

以下の順番で行います。
1. "https://accounts.spotify.com/authorize"にリクエストを送る。
2. ユーザーが認証画面でアカウント上のSpotifyの操作を許可する。
3. 設定したURIにリダイレクトされ、レスポンスにcodestateのパラメータが返ってくる。
4. coderedirect_uriClient IDClient Secretを入れて"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>
 )
}
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;
// 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 }));
};
// 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からcodestateパラメータが返ってくるので、問題なければgetAccessToken関数が実行されます。
access tokenを取得するにはリクエストボディにgrant_typecoderedirect_uriを、AuthorizationヘッダーにはBase64でエンコードしたClient IDとClient Secretを入れる必要があります。
最後にindex.jsにレスポンスを返しlocalStorageにaccess tokenrefresh token を保存して認証は完了です。
access token を使って様々なSpotify APIを使用します。
Next.jsは.envで定義するとビルド時にprocess.envに設定してくれていて、Client IDとClient Secretは.envファイルに書いた内容にアクセスしています。
またNext.jsではapiフォルダ内のファイルはサーバー側でのみアクセス可能になるのでClient Secretなどフロント側で露出したくないコードを記述します。

Step2 楽曲一覧表示

楽曲一覧を表示するにはPersonalization APIGet a User's Top Artists and Tracks を使います。

Personalization APIを使用するのに必要なパラメータは以下の通りです。
- type artistsもしくはtracks ※必須
- time_range long_term、medium_termもしくはshort_term ※任意、デフォルトはmedium_term
- limit 返されるデータの数 ※任意
- offset 返されるデータのインデックスを指定できます。 ※任意

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

// 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 を変更してデータを取得し期間別の表示を実装します。

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ともに同じメソッドを使用します。

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 APIPlaylists 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を使ってプレイリストに曲を入れる

// 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クラスのプロパティとして持つようにしました。

// 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/
Spotify APIを使うとアーティスト情報や曲の情報など取得できて色々遊ぶことができますし、ドキュメントが充実しているので使いやすかったです。
ぜひ使ってみてください。

参考

8
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
7