はじめに
フロントエンドエンジニアを目指し学習しています。
アウトプットを兼ねたアプリケーションを作ってみました。
作ったもの
SpotifyAPIを使った楽曲検索・分析アプリを作りました。
気になる曲を検索すると、その曲の特性を表示し、
特性に基づいた似ている曲を教えてくれます。
作った背景・目的
・自分の好きなことの深掘り
・おすすめのアーティストなどで好きな曲を探していくやり方が非効率だと感じ、Youtubeの動画の探し方をヒントに曲単位で調べられないかと思ったため
環境・使用技術
React(version 17.0.1)
React Audio Player
Spotify API
Router
React Hooks
(useState,useEffect,useLocation)
Firebase(Hosting)
CSS/material-ui/Scss
概要
アーティスト検索
アーティストの名前を入力すると検索候補を表示し、
クリックするとアーティストのアルバムを取得できます。
取得されたアルバムをクリックするとアルバムの楽曲がページ上部に表示され、
気になった曲をクリックすると検索結果ページに遷移します。
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を使い、入力値が変わるたびに検索候補をリセットしています。
検索結果画面
検索候補のアーティスト名をクリックするとtrackView関数
が実行されます。
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の初期化
を行っています。
アルバムの楽曲を取得
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}`);
};
楽曲情報ページ
似ている楽曲の表示
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
に加え、似ている楽曲を検索。
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の理解度がまだまだ足りていないことを痛感しました。
理解度を上げるために引き続きインプットも行っていき、並行してアウトプットもしていきたいです。
誰かの新しい音楽との出会いにこのアプリが貢献できれば幸いです。