ペアリングしてステレオ化したGoogle Home(Nest)に、ボイスコマンドでPodcastを流してもらう

２台をペアリングしてステレオスピーカー化したGoogle Nest mini（Google Home miniの新型）にボイスコマンドでPodcastをしゃべってもらおうと思ったら、意外とハマりどころが多く、結構な苦戦を強いられました。
苦労の末、どうにかやりたいことはできるようになったので、今回はその辺の話を書きたいと思います。
実現のためにいくつかのクラウドサービスを利用していますが、無料枠の範囲内で大丈夫です。

この辺の記事を参考にしています。

[Google Homeで好きなポッドキャストをスマートに再生する](https://qiita.com/Yuiki/items/dce48f319adf3613dcd8)
[google-home-notifier で スピーカーグループを喋らせる](https://qiita.com/yomori/items/288f0d8d7418f41b6dd1)


# やりたいこと
google homeに「ok google、～～～を流して」とお願いすると、あらかじめ指定しておいたpodcastの最新話を再生してくれる。

# 必要なもの

* IFTTTのアカウント
* firebaseのプロジェクト（無料プランで大丈夫です）
* 自宅のLAN内で常時インターネットに接続されていてnodejsが動作するPCなど
  * 僕はRaspberry pi を使っています 

# 動作させる仕組み

* Google home(Google Assistant)へのボイスコマンド入力を、IFTTTでフックする。
* IFTTTのアクションでfirebaseのRealtime Databaseを更新する(webhookを利用)。
* 自宅LAN環境内に配置したnodejs製スクリプトでfirebase realtime databaseの更新を監視しておき、更新を検知したらPodcastのRSSを取得＆パースし、podcast最新話のMP3URLをGoogle Homeに渡す。
* Google Homeが、受け取ったMP3を再生してくれる

# 実現手順

大まかな手順は「[Google Homeで好きなポッドキャストをスマートに再生する](https://qiita.com/Yuiki/items/dce48f319adf3613dcd8)」に書かれている通りですが、いくつか修正した方がよい箇所があります。

# 元記事からの変更点

環境によっては、いくつかの修正が必要です。

## mdnsモジュールの一部機能がRaspberryPiで動かない問題の修正

依存ライブラリのgoogle-home-notifierの内部で利用されている「mdns」モジュールがRaspberrypiでは正しく動作しないため、修正が必要です。

　```~/node_modules/mdns/lib/browser.js```の１２１行目を以下のように修正します

__変更前__

```~/node_modules/mdns/lib/browser.js
, 'DNSServiceGetAddrInfo' in dns_sd ? rst.DNSServiceGetAddrInfo() : rst.getaddrinfo()
```

__変更後__

```~/node_modules/mdns/lib/browser.js
, 'DNSServiceGetAddrInfo' in dns_sd ? rst.DNSServiceGetAddrInfo() : rst.getaddrinfo({families:[4]})
```

※　引数に　```{families:[4]}```　を追加しています。


## 元記事に掲載されているRaspberrypiで動かすnodejsスクリプトの修正

元記事に掲載されているスクリプトでは、podcastを再生するGoogle HomeをIP Addressで指定していますが、この方法だとスピーカーのペアリングやグループ化をしているとうまく動きません。

google-home-notifierは、対象のgoogle homeデバイスをIPアドレス指定ではなく内部名で指定することもできるようになっているため、再生するデバイスを内部名で指定するようにスクリプトを変更します。

それには、対象のgoogle homeの内部名を調べなければなりません。

下記のようなスクリプトを用意し、nodeで動作させます。

```internal_name.js
var mdns = require('mdns');
var browser = mdns.createBrowser(mdns.tcp('googlecast'));
browser.start();
browser.on('serviceUp', function(service) {
  console.log('Device "%s" at %s:%d', service.name, service.addresses[0], service.port);
});
```

実行結果は以下のようになります（デバイスの内部名の一部を伏字にしています）。

```コマンド出力
*** WARNING *** The program 'node' uses the Apple Bonjour compatibility layer of Avahi.
*** WARNING *** Please fix your application to use the native API of Avahi!
*** WARNING *** For more information see <http://0pointer.de/blog/projects/avahi-compat.html>
*** WARNING *** The program 'node' called 'DNSServiceRegister()' which is not supported (or only supported partially) in the Apple Bonjour compatibility layer of Avahi.
*** WARNING *** Please fix your application to use the native API of Avahi!
*** WARNING *** For more information see <http://0pointer.de/blog/projects/avahi-compat.html>
Device "AQUOS-TV***************" at 192.168.0.111:8009
Device "Google-Home-Mini***************" at 192.168.0.22:8009
Device "Google-Nest-Mini***************" at 192.168.0.20:8009
Device "Google-Cast-Group***************" at 192.168.0.20:32000
Device "Google-Nest-Mini***************" at 192.168.0.21:8009

出力が終わったらCtrl+Cで終了させてください
```

（グループ化・ペアリングしたスピーカーの場合には、Google-Cast-Group-**** のような名前になります）

上で取得した情報をもとに、元記事で書かれているraspberrypiで動かすスクリプトを以下のように修正＆保存します（nodeのバージョンにもよるかもしれませんが、実行時エラーが出たのでそこも修正しています）。

```raspberrypi.js
var FeedParser = require('feedparser');
var firebase = require('firebase');
var googleHome = require('google-home-notifier');
var request = require('request');

const lang = 'ja';

// const ip = '192.168.0.20'; //再生したいGoogle HomeのIPアドレス
// googleHome.ip(ip, lang);
const deviceName = 'Google-Cast-Group*************';
googleHome.device(deviceName, lang);

const config = {
  apiKey: 'hoge',
  authDomain: 'fuga.firebaseapp.com',
  databaseURL: 'https://**************.firebaseio.com',
  projectId: 'fuga',
  storageBucket: '',
  messagingSenderId: 'piyo'
};
firebase.initializeApp(config);

var db = firebase.database();
var ref = db.ref('/');
ref.on('child_changed', function(snapshot) {
  // var url = ref.child('url').val();
  var url = snapshot.val();
  if (url) {
    playLatestPodcast(url);
  }
  ref.update({'podcast_url': ''}); // 変更をリセット
});

function playLatestPodcast(url) {
  var req = request(url);
  var parser = new FeedParser();
  var items = [];

  req.on('response', function(res) {
    this.pipe(parser);
  });

  parser.on('readable', function() {
    while (item = this.read()) {
      items.push(item);
    }
  });

  parser.on('end', function() {
    googleHome.play(getLatestPodcastUrl(items), function(notifyRes) {});
  });
}

function getLatestPodcastUrl(items) {
    for (item of items) {
      for (enclosure of item.enclosures) {
        var url = enclosure['url'];
        if (url) {
            return url;
        }
      }
    }
    return "";
}
```

## グループ化したスピーカーに対応させる。

再生するデバイスが１台のgoogle homeの場合には、これでうまくいきますが、Google homeがグループ化されていたり、ペアリングしているステレオ化されたスピーカーで再生しようとしている場合はうまく動きません。

そこで、「[google-home-notifier で スピーカーグループを喋らせる](https://qiita.com/yomori/items/288f0d8d7418f41b6dd1)」の記事を参考に、```~/node_modules/google-home-notifier/google-home-notifier.js``` を修正します。

__変更前__

```~/node_modules/google-home-notifier/google-home-notifier.js
deviceAddress = service.addresses[0];
```

__変更後__

```~/node_modules/google-home-notifier/google-home-notifier.js 
deviceAddress = {};
deviceAddress.host = service.addresses[0];
deviceAddress.port = service.port;
```

※　修正が必要な個所は２か所あります。


これで、作業はすべて完了です。

IFTTTで設定した「ok google、～～～～を流して」というフレーズを言えば、目的のpodcastの最新話が自動で再生されるかと思います。

このスクリプトを常時動かしておけば、google homeに「ok google ～～を流して」とお願いするだけで、目的のpodcastの最新話を聞くことができるようになります。

同じ仕組みを利用して、miniDLNAなどで組んだDLNAサーバのプレイリストＵＲＬをgoogle homeに渡してあげれば、「ok google、NASの音楽を流して」という音声コマンドでNASに保存した音楽をシャッフル再生してくれるような仕組みなども作れそうです。

機会があればやってみたいと思います。