Spotify API + Reactを使って似ている曲を教えてくれるアプリを作ってみた

<h2>はじめに</h2>
フロントエンドエンジニアを目指し学習しています。
アウトプットを兼ねたアプリケーションを作ってみました。

<h2>作ったもの</h2>
SpotifyAPIを使った楽曲検索・分析アプリを作りました。
気になる曲を検索すると、その曲の特性を表示し、
特性に基づいた似ている曲を教えてくれます。

<h2>作った背景・目的</h2>
・自分の好きなことの深掘り
・おすすめのアーティストなどで好きな曲を探していくやり方が非効率だと感じ、Youtubeの動画の探し方をヒントに曲単位で調べられないかと思ったため

<h2>環境・使用技術</h2>
React（version 17.0.1）
React Audio Player
Spotify API
Router
React Hooks
(useState,useEffect,useLocation)
Firebase(Hosting)
CSS/material-ui/Scss

<h2>概要</h2>

https://search-music-f43a5.web.app/

<h3>アーティスト検索</h3>
アーティストの名前を入力すると検索候補を表示し、
クリックするとアーティストのアルバムを取得できます。

取得されたアルバムをクリックするとアルバムの楽曲がページ上部に表示され、
気になった曲をクリックすると検索結果ページに遷移します。

```ArtistView.js
const ArtistView = (props) => {
  const [artistInformation, setArtistInformation] = useState([]);
  const [album, setAlbum] = useState([]);

  /* アーティスト情報を取得 START */

  const getArtist = () => {
    // setTopTrack([]);
    setArtistInformation([]);
    setAlbum([]);

    axios(
      `https://api.spotify.com/v1/search?q=${props.artistTerm}&type=artist&limit=20`,
      {
        method: "GET",
        headers: { Authorization: "Bearer " + props.token },
      }
    )
      .then((artistContentsReaponse) => {
        setArtistInformation(artistContentsReaponse.data.artists.items);
      })
      .catch((err) => {
        console.log("err:", err);
      });
  };
  /* アーティスト情報を取得 END */

  useEffect(
    () => {
      if (props.artistTerm === "") {
        console.log("no-data");
      } else {
        getArtist();
      }
    },
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [props.artistTerm]
  );

  const trackView = (id) => {

    // GET ALBUM START

    axios(
      `https://api.spotify.com/v1/artists/${id}/albums?market=ES&limit=10`,
      {
        method: "GET",
        headers: { Authorization: "Bearer " + props.token },
      }
    )
      .then((tracksReaponse) => {
        setAlbum(tracksReaponse.data.items);
      })
      .catch((err) => {
        console.log("err:", err);
      });
    // GET ALBUM END

    // 検索候補をリセット
    setArtistInformation([]);
  };

  return (
    <div>
      {artistInformation.map(({ name, id }) => (
        <div key={id}>
          <p onClick={() => trackView(id)}>{name}</p>
        </div>
      ))}

      <TrackView album={album} token={props.token} />
    </div>
  );
};

export default ArtistView;

```

SpotifyAPIから取得したデータをマップで展開しています。


mapの引数に設定されている{ name, id }は

```
setArtistInformation(artistContentsReaponse.data.artists.items);
```
で取得できたデータと引数名を比べ、合致するデータを取得という意味になります。
なので、nameの取れる値は

```
artistContentsReaponse.data.artists.items.items.name
//結果 "くるり"
```

```
tracksReaponse.data.items.id
//結果 "26WuprsX7JRG69T0PXkze4"
```
となります。
(※データの階層によって変わります)

また、useEffectを使い、入力値が変わるたびに検索候補をリセットしています。

<h4>検索結果画面</h4>

![スクリーンショット 2021-02-19 10.15.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/624637/1caa0ae2-ccd0-fb52-4411-be757cd99281.png)

検索候補のアーティスト名をクリックすると`trackView関数`が実行されます。

``````ArtistView.js
const trackView = (id) => {

    // GET ALBUM START

    axios(
      `https://api.spotify.com/v1/artists/${id}/albums?market=ES&limit=10`,
      {
        method: "GET",
        headers: { Authorization: "Bearer " + props.token },
      }
    )
      .then((tracksReaponse) => {
        setAlbum(tracksReaponse.data.items);
      })
      .catch((err) => {
        console.log("err:", err);
      });
    // GET ALBUM END

    // 検索候補をリセット
    setArtistInformation([]);
  };
```

この関数は上記で取得しできたアーティストIDを引数に受け取り、
アーティストのアルバムを取得しています。

また、検索候補が表示されたままになってしまうので、

```
 setArtistInformation([]);
```
で`stateの初期化`を行っています。

表示画面
![スクリーンショット 2021-02-19 10.15.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/624637/7ac47825-cafc-9842-c215-e759d4b5dd08.png)

アルバムの楽曲を取得

```TrackView.js
import React, { useState, useEffect } from "react";
import { useHistory } from "react-router-dom";
import axios from "axios";
import Style from "./TrackView.module.scss";

const TrackView = (props) => {
  const history = useHistory();
  const [albumTrack, setAlbumTrack] = useState([]);
  const [albumImg, setAlbumImg] = useState("")

  const trackChange = (id) => {
    history.push(`/Search?query=${id}`);
  };

  useEffect(() => {
    // AlbumTrackのリセット
    setAlbumTrack([])
    setAlbumImg('')
  }, [props.album])

  const albumTrackPreview = (id) => {

    // track情報とともにアルバム画像も反映

    axios(`https://api.spotify.com/v1/albums/${id}`, {
      method: "GET",
      headers: { Authorization: "Bearer " + props.token },
    })
      .then((albumReaponse) => {
        setAlbumImg(albumReaponse.data.images[1].url);
      })
      .catch((err) => {
        console.log("err:", err);
      });

    // album Track START
    axios(`https://api.spotify.com/v1/albums/${id}/tracks?market=ES&limit=20`, {
      method: "GET",
      headers: { Authorization: "Bearer " + props.token },
    })
      .then((tracksReaponse) => {
        setAlbumTrack(tracksReaponse.data.items);
      })
      .catch((err) => {
        console.log("err:", err);
      });
    // album Track END
  };


  return (
    <div className={Style.container}>
        <div className={Style.albumPreview}>
        <div className={Style.imgWrapper}>
          <img src={albumImg}></img>
        </div>
      <div className={Style.trackContents}>
        {albumTrack.map(({ name, id }) => (
          <li onClick={() => trackChange(id)} className={Style.albumTrack} key={id}>
            {name}
          </li>
        ))}
      </div>
      </div>
      <div className={Style.album}>
        {props.album.map(({ images, name, id }) => (
          <div
            className={Style.wrapper}
            onClick={() => albumTrackPreview(id)}
            key={id}
          >
            <img src={images[1].url} />
            <p>{name}</p>
          </div>
        ))}
      </div>
    </div>
  );
};

export default TrackView;
```
ここでは、取得できた楽曲をクリックすると楽曲IDがURLに反映され、
楽曲情報ページに遷移させています。

```
const trackChange = (id) => {
    history.push(`/Search?query=${id}`);
  };
```

<h2>楽曲情報ページ</h2>
![スクリーンショット 2021-02-19 10.29.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/624637/ab851015-51ab-3c25-287b-ad0e61ad8e97.png)

![スクリーンショット 2021-02-19 10.29.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/624637/926482c2-2206-88d4-b2b7-48bc1f3701e0.png)

<h4>似ている楽曲の表示</h4>

```SimilarPage.js
import React, { useState, useEffect } from "react";
import { useHistory } from "react-router-dom";
import axios from "axios";
import ReactAudioPlayer from "react-audio-player";
import Style from "./SimilarPage.module.scss";

const SimilarPage = (props) => {
  const [similarTrack, setSimilarTrack] = useState([]);
  const history = useHistory();

  // tokenが変更されるたびに更新
  useEffect(() => {
    /* 似ている曲を取得 START */
    axios(`https://api.spotify.com/v1/recommendations?limit=10&market=US`, {
      method: "GET",
      headers: {
        Authorization: "Bearer " + props.token,
      },
      params: {
        seed_tracks: props.queryResult,
        target_danceability: props.trackInformation.danceability,
        target_energy: props.trackInformation.energy,
        target_key: props.trackInformation.key,
        target_loudness: props.trackInformation.loudness,
        target_mode: props.trackInformation.mode,
        min_popularity: 0,
        target_tempo: props.trackInformation.tempo,
        target_time_signature: props.trackInformation.signature,
        target_valence: props.trackInformation.valence,
      },
    })
      .then((similarReaponse) => {
        setSimilarTrack(similarReaponse.data.tracks);
      })
      .catch((err) => {
        console.log("err:", err);
      });
    /* 似ている曲を取得 END */
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [props.token]);

  // クリックされたらIDを取得し、メインコンテンツを変更
  const contentsChange = (id) => {
    history.push(`/Search?query=${id}`);
  };

  return (
    <div>
      <div className={Style.container}>
        {similarTrack.map(({ id, artists, name, preview_url, album }) => (
          <div
            className={Style.wrapper}
            key={id}
            onClick={() => contentsChange(id)}
          >
            <img src={album.images[1].url} alt="アルバム画像" />
            <div className={Style.textArea}>
              <div className={Style.artistsName}>{artists[0].name}</div>
              <div className={Style.trackName}>{name}</div>
            </div>
            <ReactAudioPlayer
              className={Style.audio}
              src={preview_url}
              controls
            />
          </div>
        ))}
      </div>
    </div>
  );
};

export default SimilarPage;

```

分析された数値を`params`に加え、似ている楽曲を検索。

```SimilarPage.js
 useEffect(() => {
    /* 似ている曲を取得 START */
    axios(`https://api.spotify.com/v1/recommendations?limit=10&market=US`, {
      method: "GET",
      headers: {
        Authorization: "Bearer " + props.token,
      },
      params: {
        seed_tracks: props.queryResult,
        target_danceability: props.trackInformation.danceability,
        target_energy: props.trackInformation.energy,
        target_key: props.trackInformation.key,
        target_loudness: props.trackInformation.loudness,
        target_mode: props.trackInformation.mode,
        min_popularity: 0,
        target_tempo: props.trackInformation.tempo,
        target_time_signature: props.trackInformation.signature,
        target_valence: props.trackInformation.valence,
      },
    })
      .then((similarReaponse) => {
        setSimilarTrack(similarReaponse.data.tracks);
      })
      .catch((err) => {
        console.log("err:", err);
      });
    /* 似ている曲を取得 END */
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [props.token]);
```
また、表示された似ている楽曲をクリックすると分析できるようにURLにIDを加えています。

```
const contentsChange = (id) => {
    history.push(`/Search?query=${id}`);
  };
```

<h2>今後の実装予定</h2>
ユーザー認証
お気に入りへの追加
ユーザーのお気に入り曲を取得・アップロードし、検索
コード可動生の向上
UI/UXの向上

<h2>まとめ</h2>
初めて一人で一から作成したアプリになりますので、コードが煩雑な部分も多々あります。
作成していると自分のJSやReactの理解度がまだまだ足りていないことを痛感しました。
理解度を上げるために引き続きインプットも行っていき、並行してアウトプットもしていきたいです。

誰かの新しい音楽との出会いにこのアプリが貢献できれば幸いです。