【obniz】IoTでSpotifyライフを充実させよう【node.js】

#はじめに
__あなたの人生を変えたWebサービスを教えてください__
と問われたらこの三つを答えます。
メルカリ
Twitter
###__そして[Spotify](https://www.spotify.com/jp/)です。__

#Spotifyとは
![Spotify_Logo_RGB_Green.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164405/01e379cb-4554-b222-b78a-19ad667ea385.png)

スウェーデン発のサブスクリプション型音楽ストリーミングサービスです。
有料会員数1億800万人、配信曲5000万曲以上、無料〜1480円のプランが用意されていています。

ランニングやジムなど運動中のBGMにしたり、近所の肉のハナマサ（髭男率高め）
でかかっていた印象的な曲を調べたり、レコメンドででた曲が良かったのでライブに足を運んだり、学生時代にCDで聴いていた曲を見つけて胸が熱くなったり、
__私の音楽生活を一つ上のステージに連れて行ってくれた素敵なサービスです。__

#Spotify Connect
曲数やプランそれぞれのメリットはAppleMusicやLINE MUSICなど様々あるとおもいますがSpotifyで特徴的なのがSpotify Connectです。
これは例えばスマートフォンからPCなど他端末のSpotifyを操作することできます。

【Spotify Connectのイメージ】
![spotify_connect.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164405/dd46c25a-aedb-61cc-824d-ddf70db90d12.jpeg)
【iPadからPCのSpotifyを操作する】
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">【iPad からPCのSpotifyを操作する】 <a href="https://t.co/DjlRZOSDvD">pic.twitter.com/DjlRZOSDvD</a></p>&mdash; ウーリズム (@Uh_rhythm) <a href="https://twitter.com/Uh_rhythm/status/1288424982721081344?ref_src=twsrc%5Etfw">July 29, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
デバイスをリモコン化するイメージだとお考えください。
#obnizとは

![ダウンロード.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164405/280c0452-e3a1-e134-2555-f70f131869a7.jpeg)
[obnizとは](https://obniz.io/ja/)
マイコンボードの一種でJavaScriptで動かすことができます。
LEDライトやスピーカー、温度センサーといったパーツと組み合わせて簡単にIoTデバイスを
作ることができます。

#構成図
![Untitled Diagram.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164405/2cdc46ab-7f7d-b6c2-08dd-f9531a3c915b.jpeg)
obnizのスイッチングのイベントを受けてnode.jsからSpotifyのAPIを叩いてプレーヤーを操作をします。
Requestで曲の再生/停止、巻き戻し/早送り、Responseで曲の情報を取得してobnizの画面に
曲の情報を表示させます。

#機能
まずこのリモコンでは曲の選択機能はないのであらかじめスマホやPCのプレーヤーで曲を選んでおいてください。
プレイリストを作成しておくのがいいですね。
###再生/停止
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">【再生/停止】 <a href="https://t.co/5L7b0F3chG">pic.twitter.com/5L7b0F3chG</a></p>&mdash; ウーリズム (@Uh_rhythm) <a href="https://twitter.com/Uh_rhythm/status/1288448738348765184?ref_src=twsrc%5Etfw">July 29, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
###再開
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">【再開】 <a href="https://t.co/VG8FjKhZEB">pic.twitter.com/VG8FjKhZEB</a></p>&mdash; ウーリズム (@Uh_rhythm) <a href="https://twitter.com/Uh_rhythm/status/1288468748857257984?ref_src=twsrc%5Etfw">July 29, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
###巻き戻し/早送り
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">【巻き戻し/早送り】 <a href="https://t.co/BkiCyLYSo0">pic.twitter.com/BkiCyLYSo0</a></p>&mdash; ウーリズム (@Uh_rhythm) <a href="https://twitter.com/Uh_rhythm/status/1288469558089543681?ref_src=twsrc%5Etfw">July 29, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
###曲情報の切り替え
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">【曲情報の切り替え】 <a href="https://t.co/aQRTSrkTop">pic.twitter.com/aQRTSrkTop</a></p>&mdash; ウーリズム (@Uh_rhythm) <a href="https://twitter.com/Uh_rhythm/status/1288469879297728512?ref_src=twsrc%5Etfw">July 29, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
###今日も生きたね
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">【RGBライトでムーディーに】 <a href="https://t.co/YnfI6jAXsO">pic.twitter.com/YnfI6jAXsO</a></p>&mdash; ウーリズム (@Uh_rhythm) <a href="https://twitter.com/Uh_rhythm/status/1288470127046926337?ref_src=twsrc%5Etfw">July 29, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
###楽観の深奥で燻る魔(ry
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">【表示文字数の課題】<br>楽観の深奥で燻る魔は、万人が宿す普遍的無意識の『罪』の残滓。 <a href="https://t.co/9DOTzhoLY1">pic.twitter.com/9DOTzhoLY1</a></p>&mdash; ウーリズム (@Uh_rhythm) <a href="https://twitter.com/Uh_rhythm/status/1288478440413229056?ref_src=twsrc%5Etfw">July 29, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

#実装

https://gist.github.com/UhRhythm/5e208cce89f3e845ba942f5f081fc7f9
ものすごく単純です。
obnizのスイッチのイベントに合わせて各々のAPIを呼び出すだけです。
各動作に割り当てられたAPIは下記のようになっております。


| obniz| 動作|httpメソッド| エンドポイントURL |
| :--- | :--- | :--- | :--- |
| push| 再生 | PUT|https://api.spotify.com/v1/me/player/play|
| push| 停止| PUT|https://api.spotify.com/v1/me/player/pause|
| right| 早送り | POST |https://api.spotify.com/v1/me/player/next|
| left| 巻き戻し| POST|https://api.spotify.com/v1/me/player/previous|
|none|再生中の曲を取得|GET|https://api.spotify.com/v1/me/player/currently-playing|

[【参考】Spotify for Developers](https://developer.spotify.com/documentation/web-api/)


【コード】

```javascript:spobniz.js
'use strict'
require('dotenv').config();
const request = require('request');
const sync_request = require('sync-request');
// obniz認証情報
const obniz_id = process.env.OBNIZ_ID;
const Obniz = require('obniz');
const obniz = new Obniz(obniz_id);
// spotify認証情報(.envから読み込む)
const access_token = process.env.ACCESS_TOKEN;
const client_id = process.env.CLIENT_ID; // Your client id
const client_secret = process.env.CLIENT_SECRET; // Your secret
const redirect_uri = process.env.REDIRECT_URI; // Your redirect urico
const { createCanvas } = require('canvas');
// ヘッダー情報
var headers = {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${access_token}`
};

obniz.onconnect = async function () {
    // RGB LEDを呼び出す
    var rgbled = obniz.wired('WS2811', { gnd: 0, vcc: 1, din: 2 });
    // ディスプレイ処理
    obniz.display.clear();  // 一旦クリアする
    obniz.display.print('Hello obniz-spotify!');
    var state = await obniz.switch.getWait();
    // スイッチの反応を常時監視
    obniz.switch.onchange = function (state) {
        var result = getTrackInfo();
        if (state === 'push') {
            var is_playing = result['is_playing'];
            // 再生
            if (!is_playing) {
                console.log('play');
                playTrack();
                // 停止
            } else {
                console.log('stop');
                pauseTrack();
            }
            // ディスプレイ処理
            obniz.display.clear();
            const r = random(0, 255);
            const g = random(0, 255);
            const b = random(0, 255);
            console.log(r, g, b);
            rgbled.rgb(r, g, b);
        } else if (state == 'none') {
            var info = getDispInfo(result);
            obniz.display.clear();
        } else if (state === 'right') {
            // 右にスイッチを倒したとき
            console.log('skip');
            rgbled.rgb(0, 255, 0);
            skipTrack();
            // ディスプレイ処理
            obniz.display.clear();  // 一旦クリアする
            obniz.display.print('skip');  // pressed right という文字を出す       
        } else if (state === 'left') {
            // 左にスイッチを倒したとき（やさしく）
            console.log('back');
            rgbled.rgb(255, 0, 255);
            backTrack();
            // ディスプレイ処理
            obniz.display.clear();  // 一旦クリアする
            obniz.display.print('back');
        }
    }
    setInterval(async function () {
        var result = getTrackInfo();
        if (state == 'none' && result['is_playing']) {
            rgbled.rgb(random(0, 255), random(0, 255), random(0, 255));
        } else {
            rgbled.rgb(0, 0, 0);
        }
    }, 1200);
    setInterval(async function () {
        if (state == 'none') {
            var result = getTrackInfo();
            var info = getDispInfo(result);
            console.log(info);
            const canvas = createCanvas(128, 64);
            const ctx = canvas.getContext('2d');
            ctx.fillStyle = "white";
            ctx.font = "15px Avenir";
            ctx.fillText(info, 0, 40);
            obniz.display.draw(ctx);
        }
    }, 3000);
}
// RGBをランダムで
var random = function (min, max) {
    min = Math.ceil(min);
    max = Math.floor(max);
    return Math.floor(Math.random() * (max - min)) + min; //The maximum is exclusive and the minimum is inclusive
}
// 再生
function playTrack() {
    console.log("play func");
    var options = {
        url: 'https://api.spotify.com/v1/me/player/play',
        method: 'PUT',
        headers: headers,
    };

    function callback(error, response, body) {
        if (!error && response.statusCode == 200) {
            console.log("success");
            return body;
        }
    }
    request(options, callback);
}

// 巻き戻し
function backTrack() {
    console.log("back func");
    var options = {
        url: 'https://api.spotify.com/v1/me/player/previous',
        method: 'POST',
        headers: headers
    };

    function callback(error, response, body) {
        if (!error && response.statusCode == 200) {
            console.log(body);
        }
    }
    request(options, callback);
}

// 早送り
function skipTrack() {
    console.log("skip func");
    var options = {
        url: 'https://api.spotify.com/v1/me/player/next',
        method: 'POST',
        headers: headers
    };

    function callback(error, response, body) {
        if (!error && response.statusCode == 200) {
            console.log(body);
        }
    }
    request(options, callback);
}

//  再生中の曲
function getTrackInfo() {
    var response = sync_request(
        'GET',
        'https://api.spotify.com/v1/me/player/currently-playing', {
        headers: headers,
    });
    return JSON.parse(response.getBody('utf8'));
}

// ディスプレイ表示用の曲情報
function getDispInfo(data) {
    var title = data["item"]["name"];
    var name = data["item"]["artists"]["0"]["name"];
    return title + "\n" + name;
}

// 停止
function pauseTrack() {
    console.log("stop func");
    var options = {
        url: 'https://api.spotify.com/v1/me/player/pause',
        method: 'PUT',
        headers: headers
    };

    function callback(error, response, body) {
        if (!error && response.statusCode == 200) {
            console.log(body);
        }
    }
    request(options, callback);
}

```

非同期処理の理解が甘いのと雑なコーディングなのは自覚しているので、これからリファクタリングしていく所存であります。
工夫したところはsetIntervalのところでライトがランダムに光るところです。
イケていないところは再生中にずっとライトが点滅していて結構目障りなところです。
暗闇で眺めている分にはすごく良いのですが。
さらイケてないところはsetIntervalのところで曲情報を取得するリクエストを三秒に一回
投げているところです。
もっと適した方法はあったはずです・・・。

##感想
APIドキュメントをじっくり読んだことがあまりなかったので、実現したい機能を実装するために
必要なパラメータだったり書き方だったりを英語と格闘しながら調べました。
おかげでAPIドキュメントの勘所を少しは抑えれるようになりました。
日本語表示も対応できたし、なにより大好きなサービスを使って物づくりができるというのは
ここまで楽しいことなのかと思いました。

前回の課題では本来の理想であったnode.jsでの実装をほぼほぼGASでしてしまった後悔があったのですが
今回はnode.jsでゴリゴリ書けたしobnizの関数も上手く盛り込むことができ、バランスよく作れたため達成感があります。

__人参は生産できないけど八百屋さんで購入して美味しいカレーがつくれるように、世の中に提供されている便利なAPIを使用してちょっとだけSpotifyライフの質を向上させることができました。
クックパッドを見て料理を作るようにドキュメントを読んで楽しく課題ができたと思います。__