🔥

【個人開発】Spotify APIを使用してプレイリストを作りました

に公開

はじめに

こんにちは。ASDエンジニアの橋田至です。

私は自分自身のポートフォリオサイトを作成しております。

エンジニア交流会や新規案件参画の際にいちいちおんなじことを何回も何回も聴かれるのがめんどくさいため、自己紹介となるサイトをReact,Vite,Vercelあたりを使用して作成しております。

https://my-dq-portfolio.vercel.app/music

今回このサイトに新たに自分の好きな音楽を紹介するページを追加したいと思い、Spotify APIを使用してプレイリストを作成いたしました。

そこで、他にもこのようなサイトを実装したい方がいるのではないかと思い、この記事を執筆しました。

TODO

やるべきこととしては

  • Spotify APIの利用登録
  • client idとclient secretを環境変数として登録
  • プレイリストのUIを作成

あたりです

では一つづつ解説していきます。

Spotify APIの使い方

まずは以下の記事に従って、spotifyの開発向けapiに登録し、アプリを作成してください

https://developer.spotify.com/documentation/web-api/tutorials/getting-started

アプリを作成するとclient idとclient secretが発行されるため、こちらは後に環境変数に設定するため、控えておいてください。

そして、.envとVercelの環境変数にclient idとclient secretを追加します

.env

# Spotify
VITE_SPOTIFY_CLIENT_ID=
VITE_SPOTIFY_CLIENT_SECRET=

src/components/MusicPlayer/constants.ts

export const CLIENT_ID = import.meta.env.VITE_SPOTIFY_CLIENT_ID;
export const CLIENT_SECRET = import.meta.env.VITE_SPOTIFY_CLIENT_SECRET;

UIの実装

最初はpreview_urlを表示するような形を想定していましたが、なぜかpreview_urlがnullになり取得できなかったです

どうやら現在ではspotifyが提供しているSDKを使用しないとwebアプリ内から音声を再生することは不可能なようです。

src/components/MusicPlayer/ArtistCard.tsx

import { useState } from 'react';

import type { ArtistDetails, Track } from './type';

/** 共通化されたアーティストカードコンポーネント
 * このコンポーネントは、アーティストの情報を表示するためのものです。
 */
export const ArtistCard = ({
  title,
  data,
}: {
  data: { artist: ArtistDetails; topTrack: Track };
  title: string;
}) => {
  const [audio, setAudio] = useState<HTMLAudioElement | null>(null);

  const handlePlay = (previewUrl: string) => {
    if (audio) {
      audio.pause();
    }
    const newAudio = new Audio(previewUrl);
    setAudio(newAudio);
    newAudio.play();
  };

  console.log('data', data.topTrack);

  return (
    <div className="mb-4 p-4 border rounded shadow">
      <h2 className="text-xl font-semibold">{title}</h2>
      {data.artist.images && data.artist.images[0] && (
        <img
          alt={data.artist.name}
          className="w-32 h-32 object-cover mb-2"
          src={data.artist.images[0].url}
        />
      )}
      <p>
        <strong>トップトラック:</strong> {data.topTrack.name}
      </p>
      {data.topTrack.album.images && data.topTrack.album.images[0] && (
        <img
          alt={data.topTrack.name}
          className="w-full h-48 object-cover mb-2"
          src={data.topTrack.album.images[0].url}
        />
      )}
      {data.topTrack.preview_url ? (
        <button
          className="bg-blue-500 hover:bg-blue-600 mt-2 px-4 py-2 rounded text-white"
          onClick={() => {
            const url = data.topTrack.preview_url;
            if (url) {
              handlePlay(url);
            }
          }}
        >
          再生
        </button>
      ) : (
        <p className="mt-2 text-red-500"></p>
      )}
    </div>
  );
};

src/components/MusicPlayer/MusicPlayer.tsx

/* eslint-disable react/jsx-sort-props */
import { useState, useEffect, useCallback } from 'react';

import { ArtistCard } from './ArtistCard';
import { CLIENT_ID, CLIENT_SECRET } from './constants';

import type { Track, ArtistDetails } from './type';

/*
  注意: この実装はデモ用です。本番環境ではクライアント側で Client Secret を扱うのはセキュリティ上好ましくないため、
  サーバー経由でアクセストークンを取得する実装に変更してください。
  下記の Spotify API のクライアントクレデンシャルは .env ファイルから読み込みます。
*/

// アーティスト名で検索する
const AVICII_NAME = 'Avicii';
const MRCHILDREN_NAME = 'Mr.Children';

const MusicPlayer = () => {
  const [aviciiData, setAviciiData] = useState<{
    artist: ArtistDetails;
    topTrack: Track;
  } | null>(null);
  const [mrChildrenData, setMrChildrenData] = useState<{
    artist: ArtistDetails;
    topTrack: Track;
  } | null>(null);
  const [loading, setLoading] = useState<boolean>(true);
  const [error, setError] = useState<string | null>(null);

  const clientId = CLIENT_ID;
  const clientSecret = CLIENT_SECRET;

  // アクセストークンを取得する共通関数
  const fetchAccessToken = useCallback(async (): Promise<string> => {
    const tokenResponse = await fetch(
      'https://accounts.spotify.com/api/token',
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
          Authorization: 'Basic ' + btoa(clientId + ':' + clientSecret),
        },
        body: 'grant_type=client_credentials',
      }
    );
    const tokenData = await tokenResponse.json();
    return tokenData.access_token;
  }, [clientId, clientSecret]);

  // 指定したアーティスト名で検索し、IDを取得する関数
  const getArtistIdByName = useCallback(
    async (artistName: string): Promise<string> => {
      const accessToken = await fetchAccessToken();
      const searchResponse = await fetch(
        `https://api.spotify.com/v1/search?q=${encodeURIComponent('artist:' + artistName)}&type=artist`,
        {
          headers: {
            Authorization: 'Bearer ' + accessToken,
          },
        }
      );
      const searchData = await searchResponse.json();
      const items = searchData.artists.items;
      if (!items || items.length === 0) {
        throw new Error('No artist found for ' + artistName);
      }
      return items[0].id;
    },
    [fetchAccessToken]
  );

  // 指定したアーティスト名のトップトラックを取得する関数(国コードはJP)
  const fetchTopTrack = useCallback(
    async (
      artistName: string
    ): Promise<{ artist: ArtistDetails; topTrack: Track }> => {
      const accessToken = await fetchAccessToken();
      const artistId = await getArtistIdByName(artistName);
      // アーティストの情報取得
      const artistResponse = await fetch(
        `https://api.spotify.com/v1/artists/${artistId}`,
        {
          headers: {
            Authorization: 'Bearer ' + accessToken,
          },
        }
      );
      const artistData: ArtistDetails = await artistResponse.json();
      // アーティストのトップトラック取得
      const topTracksResponse = await fetch(
        `https://api.spotify.com/v1/artists/${artistId}/top-tracks?country=JP`,
        {
          headers: {
            Authorization: 'Bearer ' + accessToken,
          },
        }
      );
      const topTracksData = await topTracksResponse.json();
      // 一番人気のトラック(配列の先頭を使用)
      const topTrack: Track = topTracksData.tracks[0];
      return { artist: artistData, topTrack };
    },
    [fetchAccessToken, getArtistIdByName]
  );

  // 両アーティストの情報とトップトラックを一括取得
  useEffect(() => {
    const fetchData = async () => {
      try {
        const [avicii, mrChildren] = await Promise.all([
          fetchTopTrack(AVICII_NAME),
          fetchTopTrack(MRCHILDREN_NAME),
        ]);
        setAviciiData(avicii);
        setMrChildrenData(mrChildren);
      } catch (err) {
        console.error('Error fetching top tracks:', err);
        setError('トップトラックの取得に失敗しました。');
      } finally {
        setLoading(false);
      }
    };
    fetchData();
  }, [fetchTopTrack]);

  return (
    <div className="p-4">
      <h1 className="text-2xl font-bold mb-4">Music Player</h1>
      {loading ? (
        <p>データを読み込み中...</p>
      ) : error ? (
        <p className="text-red-500">{error}</p>
      ) : (
        <div className="space-y-8">
          {aviciiData && <ArtistCard title="Avicii" data={aviciiData} />}
          {mrChildrenData && (
            <ArtistCard title="Mr.Children" data={mrChildrenData} />
          )}
        </div>
      )}
    </div>
  );
};

export default MusicPlayer;


SDKを使用してみたのですが、再生埋め込み機能を利用するには課金が必要なのかもしれません。

ここらへん詳しい方が居たらぜひとも教えて下さい🙏

とはいえとりあえず好きなアーティストを表示することは出来たので、一旦満足しています!

https://my-dq-portfolio.vercel.app/music

実装詳細

https://github.com/developerhost/my-dq-portfolio/pull/90

最後に

もしこの記事が参考になったらいいねをもらえると嬉しいです!

また、リポジトリにスターをもらえると泣いて喜びます!

https://github.com/developerhost/my-dq-portfolio

参考

https://qiita.com/Prgckwb/items/21ec6cdbfa7fcf5aa466

https://qiita.com/sayuyuyu/items/4ca06a851fca41f6b270

https://developer.spotify.com/documentation/web-api/tutorials/getting-started

https://stackoverflow.com/questions/24705253/play-full-spotify-track-inside-my-own-website-using-spotify-web-api

https://developer.spotify.com/documentation/web-playback-sdk/howtos/web-app-player

Discussion