私が歌川です

@utgwkk が書いている

#nowplaying をリアルタイムに配信したい

KMCM(1)

はじめに

これは KMCアドベントカレンダー2016 1日目の記事です。

kmc.hatenablog.jp

www.adventar.org

こんにちは

KMC2回生で副広報*1id:utgwkk です。KMC では utgw という ID で活動しています。

さて、アドベントカレンダー1日目ということなので、ゆるーく進行していきましょう。

#nowplaying

みなさんは、#nowplaying をシェアしていますか?

#nowplaying とは、このように自分が今聴いている楽曲をシェアすることを表しています。起源は不明ですが、おそらくリアルタイムな SNS 文化の浸透とともに広まったものと推測されます。

Subsonic

Subsonic は、音楽をストリーミング配信するサーバーです。自分のサーバーに簡単に設置でき、インターネット一つでどこにでも音楽を持って行くことができます。また、API も用意されているので、必要なデータを簡単に取得することができます。

Subsonic API を叩く

以前のバージョンでは username と password をクエリに入れて叩いていたのですが、現在は username と salt と token (password の後に salt をつなげた文字列の md5sum) で叩くことができます。

どの API を叩くのにも、次のクエリが必須とされています。

名前 説明
u ユーザー名
s md5sum を取る際にパスワードに付ける salt
t トークン。md5sum(password の後に salt をつなげた文字列)
v Subsonic REST API のバージョン。通常は 1.14.0 など、最新バージョンでよい
c API を叩くアプリの識別子

次のクエリは必須ではありませんが、指定しないとデータが XML で返ってきます。

名前 説明
f データのフォーマット。xml json jsonp のいずれかが指定できる。普通は json でよさそう

これらに従って、http://example.com/subsonic/rest/ping.view?u=utgwkk&t=13fbc5dddddddaaaa&s=aaabb&v=1.14.0&f=json&c=myapp などの URL に対して GET リクエストを送ることによって API を叩くことができます。

より詳しい情報は、公式の API ドキュメントを閲覧してください。

SSE (Server-Sent Events)

SSE (Server-Sent Events) とは、ざっくり言うとサーバーからクライアントにデータを push する方式の1つです。詳しくは MDN の記事を参照してください。

developer.mozilla.org

event: ping
data: {"name": "utgwkk", "message": "Yo"}

data: {"name": "kkutgw", "message": "Yo"}

...

こういった行ベースのフォーマットで、手軽にデータを push することができます。

普通の HTTP ストリーミングとは違い、

  • EventSource のインタフェースに沿って直感的にクライアントを実装できる
  • ブラウザで動く
  • event の種類を付けることができる

などの利点があります。

SSE で配信する

MDN の記事に、サーバーサイドの PHP 実装の例があります。要するに、1つのリクエストに対してレスポンスを返してそのまま終了ではなく、レスポンスを生かしたまま次々にデータを流していく、ということをすればよいです。

単純に CGI 等でやる場合はこのようにすればよいですが、最近は WAF を用いて web ページを作ることが多いので、それぞれのフレームワークのやり方に従っていく必要があります。

たとえば、Python 製の Web アプリケーションフレームワークである Flask では、ジェネレーターを用いてこのようなデータの push を実現できるように実装されています。

次の例では、GET /stream すると1秒おきに ping イベントを push します。

def do_stream():
    while True:
        yield 'event: ping\n\n'
        time.sleep(1)


@app.route('/stream')
def streaming():
    return Response(do_stream(), mimetype="text/event-stream")


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, threaded=True)

このようにジェネレーターで直感的に書けるのはよいですね。

SSE で流れてくるものを見る

さて、手元に UNIX 端末がおありの方は、

$ curl https://utgw.net/nowplaying/stream

を実行してみてください。結果はたいてい次のようになるかと思われます。

$ curl https://utgw.net/nowplaying/stream

event: ping
data: {"track": 2, "title": "Nebula Sky", "created": "2016-11-15T08:37:27.000Z", "type": "music", "playCount": 39, "albumId": "70", "duration": 280, "transcodedSuffix": "mp3", "discNumber": 1, "transcodedContentType": "audio/mpeg", "id": "1010", "isDir": false, "genre": "Soundtrack", "year": 2015, "album": "THE IDOLM@STER CINDERELLA GIRLS ANIMATION PROJECT 2nd Season 05", "suffix": "m4a", "username": "utgw", "parent": "735", "contentType": "audio/mp4", "playerId": 4, "minutesAgo": 3, "path": "utgw/THE IDOLM@STER CINDERELLA GIRLS/THE IDOLM@STER CINDERELLA GIRLS ANIMATIO/02 Nebula Sky.m4a", "size": 9146912, "artist": "\u30a2\u30ca\u30b9\u30bf\u30b7\u30a2 (\u4e0a\u5742\u3059\u307f\u308c)", "bitRate": 256}

: no new data

: no new data

...(しばらくして)

: no new data

data: {"track": 3, "title": "PANDEMIC ALONE", "created": "2016-10-26T03:35:13.000Z", "type": "music", "playCount": 86, "albumId": "63", "duration": 243, "transcodedSuffix": "mp3", "discNumber": 1, "transcodedContentType": "audio/mpeg", "id": "978", "isDir": false, "genre": "Soundtrack", "year": 2016, "album": "THE IDOLM@STER CINDERELLA GIRLS STARLIGHT MASTER 06", "suffix": "m4a", "username": "utgw", "parent": "976", "contentType": "audio/mp4", "playerId": 4, "minutesAgo": 0, "path": "utgw/THE IDOLM@STER CINDERELLA GIRLS/THE IDOLM@STER CINDERELLA GIRLS STARLIGHT MASTER/06/03 PANDEMIC ALONE.m4a", "size": 7951890, "artist": "\u661f\u8f1d\u5b50 (\u677e\u7530\u98af\u6c34)", "bitRate": 256}

: no new data

...

接続に成功すると、ping イベントと共に #nowplaying を返す*2か、: no new data コメント*3を返すかします。

私が新しい曲を聴き始めたら新しい #nowplaying が、そうでなければ : no new data コメントが流れてきます。

データの配信(つまり、新しい曲を聴いているかどうかの判定)は、5秒おきに行われます。

SSE のデータを受けとる

先述した通り、EventSource に従って直感的にクライアントを実装することができます。

document.addEventListener('DOMContentLoaded', () => {
  const vm = new Vue({
    el: "#main",
    data: {
      title: "",
      artist: ""
    }
  });

  const renderData = (evt) => {
    console.log(evt);
    const data = JSON.parse(evt.data);
    vm.title = data.title;
    vm.artist = data.artist;
  }

  const evtSource = new EventSource("/nowplaying/stream");

  evtSource.addEventListener('ping', renderData);
  evtSource.onmessage = renderData;
});

https://sugarheart.utgw.net/labs/nowplaying.htmで #nowplaying が取得できるようにしているのですが、たったこれだけのコード*4でリアルタイムにデータを取得することができるので便利です。

また、データを取得して Slack に #nowplaying を投稿する BOT を作りました。NodeJS で書かれています。

BOT が動いている様子です。

gyazo.com

成果物

先述したように、https://sugarheart.utgw.net/labs/nowplaying.htmで、今私が Subsonic で聴いている曲の情報をリアルタイムに得ることができます。

また、配信サーバーと Slack BOTソースコードGitHub で公開しています。

github.com

curl https://sugarheart.utgw.net/nowplaying/ すると、私の #nowplaying を JSON で取得することができます。

おわりに

こうして #nowplaying を手軽に取得できる API を作ることによって、私たちは次の展望を考えることができるようになります。よかったですね。

次回予告

次は KMC-ID: kata さんの「スケベな絵を描くべきnの理由 - KMCアドベントカレンダー用のブログ」です! スケベな絵に関して一言ある先輩の記事が楽しみですね。

あわせて読みたい

www.adventar.org

KMCお絵かき Advent Calendar 2016は、KMCで主にお絵描き系のプロジェクトに参加している部員や、そうでない部員も1日1人1枚ずつ絵を描いていくというアドベントカレンダーです。こちらもどうぞ!

KMCM(2)

KMCこと京大マイコンクラブでは、#nowplaying をリアルタイムに配信したり、BOT を作りまくったりしたい部員を募集しています。KMCには入部制限はありません。年齢や学歴、人種、宗教、信条、性別、社会的身分、門地、国籍、経験などは不問です。また活動に関する制約もありません。Slackのチャット越しに会話に参加することだけでも大丈夫です。詳細は下記Webページを御覧ください。

www.kmc.gr.jp

*1:この役職も、もうじき「元」が付くようになります。

*2:返ってくる JSON については公式の API ドキュメントを参照してください。

*3:これは heartbeat といい、コネクションが生きていることを示すためのものです。

*4:データを HTML に反映するのに Vue.jsを使っています。