はじめに
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
使い方
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 ID
とClient Secret
があり認証に使用します。
"Client Secret"はSHOW CLIENT SECRETをクリックすると表示できます。
"EDIT SETTING"ボタンで設定を行います。
Redirect URIs
にリダイレクトURIを設定します。
リダイレクトURIはユーザーが認証の許可または拒否したあとにリダイレクトするURIです。
今回は開発用ではhttp://localhost:3000
、本番ではhttps://spellista.tky-ngnm.net/
にしました。
認証フローは"Authorization Code Flow"で実装しました。
以下の順番で行います。
- "https://accounts.spotify.com/authorize"にリクエストを送る。
- ユーザーが認証画面でアカウント上のSpotifyの操作を許可する。
- 設定したURIにリダイレクトされ、レスポンスに
code
とstate
のパラメータが返ってくる。 -
code
、redirect_uri
、Client ID
、Client Secret
を入れて"https://accounts.spotify.com/api/token"にリクエスト。このリクエストに入れるredirect_uri
は実際にリダイレクトされるわけではありませんが、設定したリダイレクトURIと一致している必要があります。 - "access token"と"refresh token"が返ってくる。
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から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を使用するのに必要なパラメータは以下の通りです。
-
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などはプロパティとして保持します。
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 API
、Playlists API
を使用します。また、曲のSpotifyIDが必要です。
User Profile API
- Get Current User's Profile
Playlists API
- Create a Playlist
- Add Items to a Playlist
Playlistを作成の流れは以下です。
- Spotify User IDを取得する
- Spotify User IDを使って空のプレイリストを作成、Playlist IDを取得する
- 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を使うとアーティスト情報や曲の情報など取得できて色々遊ぶことができますし、ドキュメントが充実しているので使いやすかったです。
ぜひ使ってみてください。