4
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

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

はじめに

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

作ったもの

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

作った背景・目的

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

環境・使用技術

React(version 17.0.1)
React Audio Player
Spotify API
Router
React Hooks
(useState,useEffect,useLocation)
Firebase(Hosting)
CSS/material-ui/Scss

概要

アーティスト検索

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

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

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を使い、入力値が変わるたびに検索候補をリセットしています。

検索結果画面

スクリーンショット 2021-02-19 10.15.41.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

アルバムの楽曲を取得

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}`);
  };

楽曲情報ページ

スクリーンショット 2021-02-19 10.29.07.png

スクリーンショット 2021-02-19 10.29.24.png

似ている楽曲の表示

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}`);
  };

今後の実装予定

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

まとめ

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

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
4
Help us understand the problem. What are the problem?