Google HomeでiTunesの音楽ファイルを再生
pychromecastというPythonのライブラリを利用して、
ラズパイ経由でGoogle HomeからiTunesの音楽ファイルを再生する方式です。
こんな流れです。
Google Homeに音楽キャストするPythonアプリを用意
(※Pythonプログラミングも、Chromecastのアプリ作るのも初めてなので、
作法的に誤っているところはあるかもしれないです)
pychromecastのインストール
Google HomeはChromecastと同じプロトコルでキャスト可能。
pychromecastというライブラリを利用して、音楽キャストします。
ライブラリ詳細は以下。
GitHub - balloob/pychromecast: Library for Python 3 to communicate with the Google Chromecast.
For more options see the BaseController. For an example of a fully implemented controller see the MediaController.
と記載のあるとおり、メディアをキャストする関数仕様については、BaseControllerやMediaControllerのソース内のコメントを参照。
インストールは、以下コマンド。
pip3 install pychromecast
pip3(Python3のライブラリインストーラ)が未インストールの場合は、先にpip3をインストール。
(pychromecastはPython2は非対応)
sudo apt-get install python3-pip
冒頭処理
import time, shutil, os, sys, random, datetime, logging, re, threading import lxml.etree import urllib.parse import pychromecast # NASのiTunesファイルパス # -- iTunes Librari.xml内に記載されているパス NAS_PATH = 'file://localhost//xxxxx' # -- ラズパイのマウント先 NAS_MOUNT_PATH = '/mnt/music' # iTunes Library.xmlファイルパス ITUNES_LIBRARY_PATH = NAS_MOUNT_PATH + '/iTunes/iTunes Library.xml' # 音楽データ一時格納先 TEMP_DIR = '/home/pi/www/music/' LOCAL_URL = 'http://192.168.101.1/music/' GOOGLE_HOME_NAME = 'ぐーぐるほーむ'
LOCAL_URLのIPアドレスはローカルIPでOK。
GOOGLE_HOME_NAMEは、Google Homeアプリから見えるGoogle Homeの名前と同じものを指定すればOK。
起動関数
# 起動 # 引数:キーワード def play(keyword): # xml読み込み global _original_doc _original_doc = lxml.etree.parse(ITUNES_LIBRARY_PATH) # 英大文字、小文字のゆらぎの対応用 root = _original_doc.getroot() xmlStr = lxml.etree.tostring(root, encoding='utf-8') _original_doc = lxml.etree.fromstring(xmlStr) global _lower_doc _lower_doc = lxml.etree.fromstring(xmlStr.lower()) # Google Home接続 chromecasts = pychromecast.get_chromecasts() global _cast _cast = next(cc for cc in chromecasts if cc.device.friendly_name == GOOGLE_HOME_NAME) _cast.wait() global _md _md = _cast.media_controller if _cast.status.status_text.startswith('Now Casting'): _md.block_until_active() # 再生中の場合、ストップ if _md.status.player_is_playing: _logger.debug('再生中の曲を停止') _md.stop() _md.block_until_active() _playKeyword(keyword)
iTunes Library.xmlを読み込む。
Google Homeから渡されてくる英文字がどうなっているかわからないので、以下を用意。
- _original_doc:オリジナル
- _lower_doc:XML内の英文字を全て小文字にしたもの
その後、Google Homeに接続。
再生中の音楽があった場合は停止する。
ここで、停止しておかないと、このアプリが動いている最中に、新たに
「○○流して」
で割り込みがあった場合に、先に動いている方が割り込みをうまく検知できない。
(もしかしたら、判断条件が足りないだけかもしれない)
iTunes Library.xmlを検索して目当ての曲を探す
Google Homeへの接続が完了したら、
iTunes Library.xmlをXPath検索して、目当ての曲を探す。
def _playKeyword(keyword): # 英小文字に揃える keyword = keyword.lower() # 英単語間以外のスペースを除去 keyword = re.sub('(?<![A-Za-z])( )', '', keyword) keyword = re.sub('( )(?![A-Za-z])', '', keyword) # ゆらぎ変換 yuragi = {"こっこ": "cocco", "ガーネットクロウ": "garnet crow"} if keyword in yuragi: keyword = yuragi[keyword] dict_list = [] # タイトル検索 xpath = './/dict/key[text()="name"]/following-sibling::string[position()=1 and text()="' + keyword + '"]/..' dict_list.extend(_getDictList(xpath)) # アーティスト名検索 xpath = './/dict/key[text()="artist"]/following-sibling::string[position()=1 and text()="' + keyword + '"]/..' dict_list.extend(_getDictList(xpath)) # シャッフル random.shuffle(dict_list) # 再生 for dict in dict_list: ret = _playDict(dict) if ret == 1 : #割り込み終了の場合 return # 指定のXPathに該当する音楽のlower_dictを返す def _getDictList(xpath): dict_list = [] for dict in _lower_doc.xpath(xpath): dict_list.append(dict) return dict_list
まず、Google Homeから渡されてきたキーワードをiTunes Library.xmlに記載されているものに合わせて変換。
ゆらぎ辞書は、とりあえずベタ書きで書いたけど、
そのうち、スプレッドシートに定義して、それを読みにいく方式にしたら使い勝手良さそう。
次に、キーワードでXML内を検索。英小文字で探すので、_lower_docの方を検索。
とりあえず、タイトルとアーティスト名の検索に対応。
iTunes Library.xmlの構造が以下のようになっていて、キー、値、キー、値、の羅列になっているから、XPathが少しめんどい。
まぁでも、iTunes Library.xmlに記載されている情報なら、何でも検索できる。
- dict
- key
- dict
- key : 'Track ID'
- integer : トラックIDの値
- key : 'Name'
- string : タイトル
- key : 'Artist'
- string : アーティスト名
- etc..
そうして、音楽リストをシャッフルして、再生処理に再生する音楽のdictノードの情報を渡す。
割り込み終了だったら、繰り返さないようにreturnを組み入れておく。
音楽情報の取得
# dictの音楽を再生する def _playDict(dict): info = _getDictInfo(dict) if info is None: return 0 return _playMusic(info) # dictノードから、音楽ファイルの情報を返す # path: ファイルパス def _getDictInfo(dict): track_nodes = dict.xpath('./key[text()="track id"]/following-sibling::integer[1]') if len(track_nodes)==0 : return None track_id = track_nodes[0].text original_dict = _original_doc.xpath('.//key[text()="Track ID"]/following-sibling::integer[position()=1 and text()="' + track_id + '"]/..')[0] path = original_dict.xpath('./key[text()="Location"]/following-sibling::string')[0].text # パス文字列の置換 # --- URLエンコーディング path = urllib.parse.unquote(path) # --- マウント先のパスに変換 path = path.replace(NAS_PATH, NAS_MOUNT_PATH) # 楽曲情報取得 title = original_dict.xpath('./key[text()="Name"]/following-sibling::string')[0].text artist = original_dict.xpath('./key[text()="Artist"]/following-sibling::string')[0].text metadata = {'metadataType': 3, 'title': title, 'artist': artist} return {'path': path, 'metadata': metadata}
dictノードを解析して、再生する音楽の詳細情報を取得。
こちらは、iTunes Library.xmlに記載されている内容そのままを使うので、_original_docの方を参照。
音楽ファイルパスは、iTunes Library.xmlには以下のように定義されている。
<key>Location</key><string>file://localhost//LS-WSGL149/MyMusic/iTunes/iTunes%20Music/Every%20Little%20Thing/Time%20to%20Destination/All%20along.mp3</string>
これをファイル参照に使用できるように、URLデコーディングして、マウント先のパスに変換。
以下の仕様に合わせて、タイトルやアーティスト名を含んだメタデータを作成。
https://developers.google.com/cast/docs/reference/messages#MediaData
このメタデータを音楽再生するときにGoogle Homeに一緒にキャストすると、
Google Homeと連携しているAndroidデバイスとかから、こんな風にタイトルやアーティスト名が見れる。
音楽再生
# 音楽再生 # 戻り値:通常終了->0, 割り込み終了->1 def _playMusic(info): # 再生する曲をWebサーバにコピー # 拡張子取得 path = info['path'] ext=path[path.rfind('.')+1:] if ext == 'm4p': # DRMファイルは再生できない return 0 now = datetime.datetime.now() tempFileName = '%04d%02d%02d%02d%02d%02d.%s' % (now.year, now.month, now.day, now.hour, now.minute, now.second, ext) tempPath = TEMP_DIR + tempFileName shutil.copyfile(path, tempPath) _md.play_media(LOCAL_URL + tempFileName, 'audio/' + ext, metadata=info['metadata'], stream_type="BUFFERED") _md.block_until_active() # 曲の再生を待つ time.sleep(5) event = threading.Event() _md.register_status_listener(StatusListener(event)) event.wait() if _md.status.idle_reason == 'FINISHED': # 再生終了 os.remove(tempPath) return 0 else: # 割り込み終了 os.remove(tempPath) return 1 # 再生が終わるのを待つ # MediaPlayerのステータス変更検知 class StatusListener: def __init__(self, event): self._event = event def new_media_status(self, status): # player_is_idle : 再生中・一時停止中=FALSE, 終了=TRUE if status.player_is_idle or status.media_session_id is None: # 再生終了したら self._event.set()
Google HomeにURLを渡して音楽流してもらう形なので、
音楽ファイルをWebサーバ下にコピーしてくる。
temp.mp3とかの固定の名前にすると、Google Homeがキャッシュ参照して、
延々同じ音楽を流してしまうので、日時を使った名前にしておく。
play_mediaしてから少し待たないと、速攻終了判定に行ってしまうので、
とりあえず5秒待っている。
再生終了したら、Webサーバ下にコピーしたファイルを削除。
再生終了判定の
status.media_session_id is None
は、外からキャスト停止された場合用。
コマンドラインからも呼び出せるようにする
最後に、
if __name__ == '__main__': args=sys.argv play(args[1])
これで、
python3 play_music.py 'キーワード'
で、実行できる。
(pythonCGIから呼ぶなら不要だけど)
Webサーバ処理の用意
PHPでもPythonでも何でもいいのだけど、
POSTなりGETなりでパラメタ受け取って、
音楽キャストアプリを起動するWebサーバ処理を用意。
とりあえず、PHPで用意した。
<?php $keyword = $_POST['keyword']; header("Access-Control-Allow-Origin: *"); $command ='/usr/bin/python3 play_music.py "'.$keyword.'" > /dev/null &'; exec($command); ?>
/dev/null &
を付けて、突き放し処理。
はまったところ
- キャストしようとしたところでエラー。pychromecastが利用しているzeroconfライブラリでType Errorだか何だか。
- 「zoroconfに問題があって、新しいバージョン入れれば解決する」という情報が公式に載っていたので、ライブラリのバージョン問題かと思って、アップデートしたり再インストールしたり何やりしたけど、うまくいかず。
- 結局、pychromecastのページをよく読んだら、Python2は非対応になった、と書いてあるのに気づいて、Python3で動かしたらうまく動作した。
- Webサーバ経由で動かそうとしたところで、pychromecastがImport Error
- shutil.filecopyで文字コード関連のエラー
この後の拡張
以下ができると良い。
- スプレッドシートでゆらぎ管理
- アートワークの表示
- Chromecastとも連携して、歌詞表示できるようにしてもいいかも
- 起動時に曲を検索した結果をGoogle Homeに喋らせる。「見つかりませんでした」とか「何曲見つかりました」とか。