カレーのライスを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秒計測している)

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

GAE/ Go1.9 から Go1.12 へ移行するためにやったこと

業務でGAEのGoアプリケーション(1.9)を1.12化をすることがあったので、作業ログをまとめておきます。

ドキュメント

やること

  • app.yaml の書き換え

    • runtime: go112
    • script: auto
    • skip_files.gcloudignore へ移行
    • login が非推奨になるので新しい認証を入れる
      • login | 非推奨 | Go 1.12 ラインタイムでは login がサポートされません。ユーザー認証に別の方法を使用する必要があります。

  • init.go & init_test.go の書き換え

    • ファイル名を main.go & main_test.go に変更
    • func init()func main() に変更
  • appengine パッケージからの脱出

    • appengine パッケージと google.golang.org/appengine パッケージはサポート対象外になりました。同等の Google Cloud Platform サービスにアクセスするには、Google Cloud クライアント ライブラリに移行する必要があります。

    • appengine.Main() とか appengine.NewContext がサポート対象外になるのでGoの標準packageを使って書き換えないといけないぽい
    • この サポート対象外 というのがよく分からなくて、結局 #AccessToken とか #AppID 関数とかは動いている

ハマったところ

go run main.go ができない

  • localで go run main.go してみるとすでにappengineパッケージ周りでエラーが出ていたのでサポート対象外ってAPIが廃止になったてことかな......
    • ちなみにdev_appserver.pyみたいなアレが1.12ランタイムから使えなくなったのでそれはそう、という感じ
    • 完全に appengine package から脱出しないとローカルで実行することができないのでちょっと面倒

go mod 使うとワーキングディレクトリが go.mod がある場所になる

internal.flushLog: Flush RPC: service bridge HTTP failed: Post http://appengine.googleapis.internal:10001/rpc_http: dial tcp 169.254.169.253:10001: i/o timeout というエラー

  • これは私の手元での問題なのだが、 go mod init するとGopkg.lockから依存を解決してくれていたのでしっかり以前のバージョンが更新されないまま入っていた

    • latestにアップデートしてみて、これで直るか!!と思ったけどダメ
    • 検索してみると 同じような症状の人を見つけた
    • 原因不明なのでgo1.11ランタイムを使っていますとのことぽい、そんな...
  • appengineパッケージを完全に脱出すると治った

10月には1.9ランタイムがdeprecatedになっているにも関わらずあまりインターネッツに情報がなかったので少し大変だった。とりあえず動いてはいるけど、これでいいのかな?という感じで自信がない...

参考にしたページ

Go で time.Format() する際に 2019 はダメだけど 2006 ならいけた

package main

import (
    "fmt"
    "time"
)

func main() {
    t := time.Now()
    fmt.Println(t.Format("20190709_151230"))  // 270790709_1372710
    fmt.Println(t.Format("20060709_151230"))  // 20190709_1372710
}

何だよ 27079 年て.................。

追記

そうなんだ〜〜〜

"ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/var/lib/mysql/mysql.sock' (2)" このエラー5万回見た

結局やらなくてはいけないことは

  • sudo touch /tmp/mysql.sock みたいな感じで sockファイルを作る
  • sudo chown mysql:mysql /tmp みたいな感じでmysqlにsockファイルの入っているディレクトリの権限を与える
  • そもそも sudo mysql.server start などしていないかも

この辺に知見がある。

qiita.com

qiita.com

macOS 10.15 Beta にしてからbrew upgrade が動かなかったりした

  • タイトルの通り、新しいもの好きの私はすぐアップデートとかしたがるんですが毎回こうやってBeta版の洗礼を受けます

  • 今回は brew upgrade が動かなくなった

==> Upgrading heroku/brew/heroku
Error: Your Xcode (10.2.1) is too outdated.
Please update to Xcode 11.0 (or delete it).
Xcode can be updated from:
  https://developer.apple.com/download/more/
  • Xcode のベータ版(11.0)をインストールしたが、まだ動かない

  • brewが前のバージョンのXcodeを見にいってたのでBeta版の方を指定してあげる

  • >> sudo xcode-select -switch /Applications/Xcode-beta.app/