カレーのライスをtech忘れ

odmishienのtechメモ

SpotifyWebAPIで遊んでみる

最近趣味としてのプログラミングというか創作みたいなものができていなかったので久しぶりに遊びました。

developer.spotify.com

SpotifyAPIは色々頑張っていて、特に audio-features という曲の特徴(BPMや調はもちろんenergyやdanceabilityなどSpotifyが独自に算出した値など)を取ってこれるのが個人的には面白いな〜と思いました。この曲の特徴を使えば、ちょっとした作業の時やBGMが欲しい時にざっくりと今のフィーリングを指定したら勝手にプレイリスト作ってくれる君ができそう!というのを思い付いたので手を動かしました。

できたもの

diggy-odmishien.herokuapp.com

"音楽を探す" ことを"dig" という風に言うのでこんな名前です。

f:id:odmishien:20191209113220p:plain
Spotifyのアカウントを使ってログイン

f:id:odmishien:20191209113223p:plain
今の気分をスライダーで教えてください

f:id:odmishien:20191209113226p:plain
プレイリストが作成され、リダイレクトされます

技術的な話

使ったフレームワークなど

APIにリクエストを投げたり、ユーザの認証のためにリダイレクトしたりするのでWebサーバが必要。今回はHerokuにデプロイして楽するつもりだったので、WebアプリのフレームワークにはHerokuとの親和性の高いExpressを選びました。Procfileちょろっと書けば、 npm install とかを勝手にHeroku側でやってくれてとても楽だった。

ユーザ認可(OAuth2.0)

検索やプレイリストを作るためにユーザーのリソースにアクセスする必要があるのでSpotify側で認可が必要です。 この記事を読んでアクセストークンのことを理解した気になったりしました。SpotifyAPIAuthorization Scopesを指定することでアクセスできるリソースを絞ることができます。取ってきたアクセストークンをどこに置いておくべきかみたいな話は(きっとDBとかサーバ側に置いておいたほうがいいんだろうけど)あまりよく分かっていないのでまた調べよう....。

今回はCookieに持たせていて、その理由としては

  • DB立てるのが面倒
  • Scopeを絞っていて、せいぜいプレイリストの作成や追加しかできないトークンなので漏れても破壊的なことはできない
  • Spotify側がtokenがexpireする時間を6000秒にしている

というあたりです。Cookie抜いてくるサイトにユーザーが行っちゃたりしたらちょっとアレな実装になっています。これ追々対応すべきかな〜...。

プレイリストを作っていく

何はともあれ、まずは楽曲を探します。Node向けにAPIアクセスのヘルパーパッケージがあったのでそれを使います。

www.npmjs.com

APIには「無作為に曲を取ってくる」というエンドポイントが提供されていないのでちょっと頑張ります。楽曲にはID(22桁のbase-62値)が割り振られていますので、これをランダムに生成して200番が返ってくれば続けて audio-features を見る、という作戦を考えますた。しかしあまりにも404ばかり返ってくる。まあそれはそうだなということで GET /search を使うことに。

// 検索クエリに使う文字列候補/
const queryStrs =
  "abcdefghijklmnopqrstuvwxyzあいうえおかきくけこさしすせそたちってとなにぬねのはひふへほまみむめもやゆよわをん";

let q = queryStrs[getRandomInt(0, queryStrs.length)]; // 今回は1文字だけで検索している
spotifyApi
  .searchTracks(q, {
        market: "JP",
        limit: 5,
        offset: getRandomInt(1, 1000) // offsetをランダムにすることで曲がなるべくかぶらないように
      })

// 適当な整数を返す
function getRandomInt(min, max) {
      min = Math.ceil(min);
      max = Math.floor(max);
      return Math.floor(Math.random() * (max - min)) + min;
    }

audio-features はあまりにもデータの種類が多いので今回は3つだけ使うことに。

  • valence : 楽曲の明るさ/高ければ高いほど明るく低いほど悲しい雰囲気
  • tempo : 曲のBPM
  • energy : 曲のエナジー(?) fast, loud, and noisy だとこの値が高くなるとのこと

なんかちょっとずつ各要素がかぶってる気もしなくないので、これはちゃんと考えるともっと自分のフィーリングに合った楽曲をお届けできそう。 フォームから受け取った3つの値から許容する誤差範囲を決めて、それに一致する値を持っていればプレイリストに追加します。プレイリストに追加するかどうか判定しているコードがこんな感じ。

function isAddableTrack(feature) {
      // 許容する誤差の範囲
      const tolerance = 0.1; 
      const tempoTolerance = 20;

      // かなりかわいい実装になってるが、3つの値のうち2つが許容範囲内ならOK
      let judgeCount = 0;
      if (
        happinessVal / 100 - tolerance < feature.valence &&
        feature.valence < happinessVal / 100 + tolerance
      ) {
        judgeCount += 1;
      }

      if (
        energyVal / 100 - tolerance < feature.energy &&
        feature.energy < energyVal / 100 + tolerance
      ) {
        judgeCount += 1;
      }

      if (
        tempoVal - tempoTolerance < feature.tempo &&
        feature.tempo < tempoVal + tempoTolerance
      ) {
        judgeCount += 1;
      }

      if (judgeCount >= 2) {
        return true;
      } else {
        return false;
      }
    }
  }

プレイリストに追加する楽曲数はユーザーが指定できたらいいなと思っていたが、あまりに探すのに時間がかかるとHerokuのタイムアウトに引っかかってしまうので今回は6曲以上見つけたらお終いにします。

if (tracks.length > 6) {
          // playlistの名前をとりあえずタイムスタンプにしている
          // ゆくゆくはちゃんとフィーリングに沿った名前を自動生成したい
          let now = new Date();
          let ts = dateformat(now, "yyyy/mm/dd HH:MM:ss");
          spotifyApi
            .createPlaylist(userId, ts)
            .then(data => {
              let playlistId = data.body.id;
              let playlistUri = data.body.external_urls.spotify;
              spotifyApi
                .addTracksToPlaylist(playlistId, properTracks)
                .then(data => {
                  // Playlistが正しく作られたらそこに直接リダイレクトする
                  res.redirect(playlistUri);
                })
                .catch(err => {
                  console.log(err);
                  res.send('something wrong!!');
                });
            })
            .catch(err => {
              console.log(err);
              res.send('something wrong!!');
            });

エラーハンドリングとかがかなり甘めですが、こんな感じです。

もっとやれそうなこと

  • 値を極端に静かめにすると「ヒーリングミュージック」ばかりになる
    • こんな感じ
  • これはこれで面白い
  • 歌詞があるかどうか(instrumentalness)とかいう項目もあるのでうまく使えたら良さそう
  • ロード画面がしょぼい&ロードが長くて不安になる
    • プレイリスト作るのが長いのはHerokuでは限界がある気がする
  • 直接プレイリストにリダイレクトしなくてもいい気がする
    • SpotifyはWebページにプレイリストを埋め込むViewがあるのでそれを使う
    • Twitterで呟くボタンやもう一回プレイリスト作る画面に戻る導線を設置できる

最後に

SpotifyWebAPI、まだまだ遊べそうという気持ちになりました。その日の天候とか自分の書いた文章とか自撮りした表情とかから感情分析して、プレイリスト作ってみるとかも面白そう。 作業する際やパーティの際にさっとBGM流したいけど、いつも同じ曲ばかり聴いてしまう...何流したらいいか分からない....みたいな方にはオヌヌメです!!随時ブラッシュアップしていくのでよかったら遊んでみてください。 最後に私のハイになった気分によって生成された元気の出るプレイリストを置いておきます!!!

参考にしたサイト

2019.12.10 追記

触ってみたよ!みたいな声いただきました。ありがとうございます。その中でいくつか「エラーになっちゃう」と教えていただきました…ちゃんと見れてないのですが多分Herokuのタイムアウトが原因だろうなと思っています。(プレイリストを作る処理が、条件にマッチする曲がない限り探し続けてしまうのでそこが遅い気がする)その場合裏でプレイリストを作る処理は行われているのでSpotifyのマイページにはプレイリスト追加されていると思います…。ちょっとHerokuに関しては課金するか、別のサーバに移すかどうか考えます!!よろしくお願いします!!

2019.12.10 さらに追記

タイムアウト対策として色々いじってみました。

  • APIの制限にかかると思って設定していたsleep処理をギリギリまで短くした
  • 最悪の場合1曲でもいいからタイムアウトする前にプレイリストを出力するようにした(setTimeOutで30秒計測している)

これでとりあえずタイムアウトのエラーは減って欲しいな....?