23
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-07-29

#はじめに
あなたの人生を変えたWebサービスを教えてください
と問われたらこの三つを答えます。
メルカリ
Twitter
###そしてSpotifyです。

#Spotifyとは
Spotify_Logo_RGB_Green.png

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

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

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

【Spotify Connectのイメージ】
spotify_connect.jpg
【iPadからPCのSpotifyを操作する】

デバイスをリモコン化するイメージだとお考えください。 #obnizとは

ダウンロード.jpeg
obnizとは
マイコンボードの一種でJavaScriptで動かすことができます。
LEDライトやスピーカー、温度センサーといったパーツと組み合わせて簡単にIoTデバイスを
作ることができます。

#構成図
Untitled Diagram.jpg
obnizのスイッチングのイベントを受けてnode.jsからSpotifyのAPIを叩いてプレーヤーを操作をします。
Requestで曲の再生/停止、巻き戻し/早送り、Responseで曲の情報を取得してobnizの画面に
曲の情報を表示させます。

#機能
まずこのリモコンでは曲の選択機能はないのであらかじめスマホやPCのプレーヤーで曲を選んでおいてください。
プレイリストを作成しておくのがいいですね。
###再生/停止

###再開 ###巻き戻し/早送り ###曲情報の切り替え ###今日も生きたね ###楽観の深奥で燻る魔(ry

#実装

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

【コード】

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ライフの質を向上させることができました。
クックパッドを見て料理を作るようにドキュメントを読んで楽しく課題ができたと思います。

23
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?