ネットラジオを自動で録画して自動でGoogle Play Musicにアップロードする方法

常体で文章書くの疲れたので、これからは敬体にします。

やったこと

  • ネット配信(今回は文化放送の超A&G+)のラジオを自動で録画
  • 録画が終わったら、自動でGoogle Play Musicにアップロード

以上により、深夜にリアルタイムで放送を聞けなくても、翌日にGoogle Play Musicで録画を聞きながら通学・通勤できるようになります。
最近はニコニコ動画アーカイブをアップロードしてくれる番組も増えましたが、いちいちニコニコ動画にアクセスするのは面倒だし、普段使いのプレイヤーで聴けるととても便利です。
実は録画・アップロードの仕組みはずいぶん前に作ったものなのですが、自宅サーバからAWSに載せ替えるついでに記事にしておこうと思い立ち、今回のエントリを書きました。

準備

今回は以下の環境で実現しました。

手順

EC2サーバ、Python/pip、Ruby環境の構築手順は割愛します。
各バージョンが合っていれば、問題なく動作すると思います。

今回の流れとしては、Rubyスクリプトによってrtmpdumpでダウンロードしたコンテンツをffmpegエンコードし、PythonスクリプトGoogle Play Musicへアップロード、という感じです。

rtmpdump/ffmpeg インストール

rtmpdumpは、ストリーミング配信されているコンテンツをダウンロードするコマンドです。
また、ffmpegは、動画・音声・画像等をエンコードするツールです。

rtmpdumpaptでインストールできます。

$ sudo apt install -y rtmpdump

ffmpegaptからインストールできないので、自前でビルドします。

参考: Ubuntu 14.04 で最新の ffmpeg を簡単かつクリーンにインストールする方法

録画スクリプト(Ruby)

Gistでこんなスクリプトを見つけたので、自分用に修正して使っています。

save AGQR radio programs. · GitHub

これをサーバ内の任意の場所にcloneします。

$ git clone https://gist.github.com/fcf7d283b9aa641694bed757b7e1b3cf.git ~/record_agqr

schedule.yaml

schedule.yamlで録画したい番組を指定できます。
フォーマットは以下のとおりです。

- title: [番組タイトル(任意)]
  wday: [曜日]
  time: [開始時間]
  length: [放送時間]

火曜日 25:00~25:30 放送の『洲崎西』を録画したければ、

- title: suzakinishi
  wday:time: '25:00'
  length: 30

という感じです。

agqr.rb

スクリプト内で2箇所だけ、環境によって書き換える必要がある(かもしれない)箇所があります。

10~11行
rtmpdump = '/usr/bin/rtmpdump'
ffmpeg = '/usr/bin/ffmpeg'

インストールしたrtmpdumpffmpegのPATHを指定しています。PATHはwhichコマンドで調べることができます。

$ which rtmpdump
$ which ffmpeg

出力されたPATHがスクリプトと違うものであれば、適宜書き換えます。

このスクリプトcronで叩きます。

$ crontab -e

29,59 * * * * ruby /home/ubuntu/record-agqr/agqr.rb

スクリプトは録画1分前に起動し待機させておかなければならないので、番組が始まる0分/30分から録画をするために、毎時間29分/59分に起動させます。

スクリプトが起動した時間がschedule.yamlで指定した番組が始まる1分前だった場合、録画を始める仕組みになっています。

録画が成功すると、スクリプトと同階層のflvmp3というフォルダに、動画と音声ファイルがそれぞれ保存されます。

gmusicapiインストール/セットアップ

gmusicapiは、PythonからGoogle Play MusicAPIを叩くライブラリです。

開発は数年前に止まっていますが、問題なく動きます。

pipをつかってインストールします。

$ sudo pip install gmusicapi

次は、Google Play Musicにアクセスするためのセットアップ作業です。

Pythonインタプリタを起動し、以下の通り入力します。

>>> from gmusicapi import Musicmanager
>>> mm = Musicmanager()
>>> mm.perform_oauth()

以下のようなメッセージが出力されるので、

Visit the following url:
 https://accounts.google.com/o/oauth2/v2/auth?scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fmusicmanager......
Follow the prompts, then paste the auth code here and hit enter: 

2行目のURLにブラウザからアクセスすると、認証画面が表示されます。
ログイン後に「このコードをコピーし、アプリケーションに切り替えて貼り付けてください。」という文言とともに文字列が表示されるので、上記メッセージの3行目末尾にそのまま貼り付けると、認証が完了します。

そのままログインしてみます。

>>> mm.login()
True

Trueと表示されたら、認証は成功しています。

ちなみに、認証情報は$HOME/.local/share/gmusicapi/oauth.credに保存されています。
テストとして、ここにあったテスト用MP3ファイルをアップロードしてみます。
wgetでファイルをダウンロードし、

$ wget https://archive.org/download/testmp3testfile/mpthreetest.mp3

再度ログインした後、upload()メソッドでアップロードします。

>>> from gmusicapi import Musicmanager
>>> mm.login()
>>> mm.upload("mpthreetest.mp3")

ブラウザやスマホのクライアントからライブラリを見ると、タイトル"Test of MP3 File"というファイルがアップロードされていると思います。

以上でgmusicapiの準備は完了です。

Python スクリプトでmp3ファイルをアップロードするテスト

upload-gmusic.py · GitHub

こちらのファイルを適当な場所にcloneします。

$ git clone https://gist.github.com/29727675aec6ea1e1251a93d16179b7b.git

パラメータに渡されたファイルをアップロードするだけの、簡単なプログラムです。
使い方はこんな感じです。

$ python upload-music.py mpthreetest.mp3

スクリプトを見れば分かりますが、mp3, m4a, wma, flac, ogg, m4p形式の音声ファイルに対応しています。

Watcherでファイル作成を監視

Watcherは、ファイルの変更や移動をトリガーに、コマンドを実行してくれるPythonスクリプトです。

github.com

incronというツールも似たような動作をするのですが、こちらはディレクトリを再帰的に監視できないので、それが可能なWatcherを使うことにしました。

python-pyinotifyが必要なので、pipでインストールします。

$ sudo pip install python-pyinotify

スクリプトを適当な場所にcloneし、watcher.iniを編集します。

$ git clone https://github.com/szepeviktor/Watcher-splitbrain.git ~/Watcher

watcher.ini

[job1]
watch=/home/ubuntu/record-agqr/mp3
events=move_to
recursive=true
autoadd=true
command=python /home/ubuntu/upload-music.py

watchに監視するディレクトリを指定します。
eventsで、どういうアクションの際にコマンドを実行するかを指定します。
今回はmp3フォルダにファイルが追加されたときにアップロードスクリプトを走らせたいので、上記のような設定になります。

また、commandには、どんなコマンドを実行するかを指定します。
先ほどcloneしてきたupload-music.pyを叩いています。

そして、Watcherを起動します。

$ python watcher.py -c watcher.ini start

これでディレクトリの監視を始めてくれます。 デーモンみたいな感じで、あとは放置していればよいと思ったのですが、しばらくすると勝手に停止しているようなので、こちらもcronで一定時間ごとに起動し直すよう指定しました。

$ crontab -e

0 0 * * * python /home/ubuntu/Watcher/watcher.py -c /home/ubuntu/Watcher/watcher.ini start

おわりに

以上の作業で、最初に書いた録画 → エンコード → アップロードの流れを自動化できました。

ほとんどありものを流用したので、あまり苦労したところはありませんでした。

調べたところ、RubyにもGoogle Play Musicを叩けるGemがあるようなので、いつか全てRubyで書き直せたらいいなと思います。

CygwinでGitを使うと.gitconfigが効かない問題

Windows機にCygwinを入れ、その上でGitを使おうとして少しハマったのでメモ。

現象

  • git cloneしたリポジトリのファイルフォーマットがDOS形式になっている

僕はGitHubでdotfilesを管理し、Zsh / Tmux / Vimの設定ファイルをほぼ自動でデプロイできるようにしている。

以前にCygwin環境を構築したWin機では、特に問題なく設定ファイルを使用できていたので、 今回も同じようにリポジトリをcloneし、インストール用のスクリプトを動かそうとすると、 下記のように怒られた。

'r\' command not found

これはWindowsLinux系で改行コードが違うことから発生する問題で、cloneしたリポジトリ内のファイルの fileformatをいくつか見てみると、確かにすべてDOS形式になっていた。


試したこと

適当にググって、以下のことを試した。

clone時の改行コードの変換設定を変更

Cygwin内で以下のコマンドを実行。

> git config --global core.autocrlf input

~/.gitconfigに以下の設定が追加されていることを確認。

……
[core]
    autocrlf = input
……

この状態でリポジトリを再cloneするも、解決せず。

どうやら.gitconfigが効いていない様子。

GitHubから直接ZIPファイルを落とす

さすがにファイルフォーマットはunixになっていて、デプロイも問題無く行うことができた。

しかし、ZIPファイルでダウンロードしてきたリポジトリには.gitフォルダが作成されないので、 リポジトリに変更があった際にgit pullできず、その都度最新のZIPを落として展開……などという 無駄につらい作業が発生してしまう。

また、この手段では今回の問題の本質的なところは解決せず、別のリポジトリを取得して実際に開発を行う場合に 結局同じ問題が発生することになるので、この案は最悪の手段として一旦保留にした。


解決

何気なくwhich gitしてみたら、なんと以下のように表示された。

> which git
/cygdrive/c/Program Files/Git/cmd/git

以前同機にインストールした、Git for Windowsのgitコマンドのパスだ。 原因はこれだった。

Git for Windows~/.gitconfigではなくWindowsユーザーフォルダ直下の.gitconfigを見に行くため、 ~/.gitconfigをいくら編集しても意味が無かったということだった。

Git for Windowsはもう使っていなかったのでアンインストールし、Cygwinのsetup.exeでgitをインストールすると問題が解決した。

setup.exeでgitをインストールするのを忘れていて、かつGit for Windowsのおかげでgitコマンドが使えてしまっていたせいで時間をとられてしまった。

MacBook Pro に Hammperspoon を導入した

マカーの仲間入り

先日、ついにMacBook Proを購入し、僕も無事マカーの仲間入りを果たした。
購入したのは、MacBook Pro 13インチ(Touch Barなし)だ。
Touch Barについては、現時点ではまだ必要性が感じられないことと、Escキーを多用する自分にとってはハードキーがなくなると精神的につらいこともあり、今回は購入を見送った。

MacBook のいいところ

なんといっても、デフォルトでシェルが使える。
Windows時代から、CygwinだのBabunだのを入れたり、自宅鯖を立ててSSHしていた自分にとっては、何の設定もせずにシェル環境が整っているmacOSは非常に良い物だ。
さっそくiTerm2を導入し、自分用の環境を整えた。
今まで育ててきたDOTFILESも、概ね良好に動作している。

不満点

しばらく使用してみて思ったのは、macOSのショートカットによるウィンドウ操作が、Windowsよりも不便だということ。

例えば、ウィンドウの切り替えはWindowsではAlt + TabmacOSだとCommand + Tabだが、macOSでは切り換えの対象がウィンドウ単位でなくアプリケーション単位になっている。

つまり、FinderやChromeなどを複数ウィンドウ開いている場合、それらをまとめて切り替えてしまう。同一アプリ内でウィンドウ切り替えを行うCommand + F1というショートカットキーもあるが、WindowsAlt + Tabに慣れきった身としては、アプリなんか関係なくウィンドウの切り替えを行いたいものだ。

また、Win + →Win + ←のような、ウィンドウを画面の左右半面に展開するといった動作も実現できない。

さらに、macOSの「ウィンドウ最大化」は言うなれば「ウィンドウ最適化」であり、画面全体にウィンドウが広がらない。Ctrl + Command + Fでフルスクリーンにはなるものの、今度はメニューバーが隠れてしまうなど、どうも思ったような挙動をしてくれない。

Hammerspoon を導入

上記のような問題を解決するために、Hammerspoonというアプリを導入した。
Hammerspoonは、設定をLuaによって記述できる、macOSの動作を自動化するためのツール。
アプリケーションやウィンドウ、ファイル、オーディオデバイス、クリップボードなど様々なものを制御できるらしいが、今回はショートカットキーによってウィンドウを操作するスクリプトを作った。

local prefix = {"cmd", "ctrl"}

-- Command + Ctrl + ↑ : フルスクリーン
hs.hotkey.bind(prefix, "Up", function()
    local win = hs.window.focusedWindow()
    local f = win:frame()
    local screen = win:screen()
    local max = screen:frame()

    f.x = max.x
    f.y = max.y
    f.w = max.w
    f.h = max.h

    win:setFrame(f)
end)

-- Command + Ctrl + ← : ウィンドウ左寄せ
hs.hotkey.bind(prefix, "Left", function()
    local win = hs.window.focusedWindow()
    local f = win:frame()
    local screen = win:screen()
    local max = screen:frame()

    f.x = max.x
    f.y = max.y
    f.w = max.w / 2
    f.h = max.h

    win:setFrame(f)
end)

-- Command + Ctrl + ← : ウィンドウ右寄せ
hs.hotkey.bind(prefix, "Right", function()
    local win = hs.window.focusedWindow()
    local f = win:frame()
    local screen = win:screen()
    local max = screen:frame()

    f.x = max.x + (max.w / 2)
    f.y = max.y
    f.w = max.w / 2
    f.h = max.h

    win:setFrame(f)
end)

-- Alt + Tab : ウィンドウ切り替え
hs.hotkey.bind("alt", "tab", function()
    hs.window.switcher.nextWindow()
end)

-- Alt + Tab : ウィンドウ切り替え(逆)
hs.hotkey.bind({"alt", "shift"}, "tab", function()
    hs.window.switcher.previousWindow()
end)

上記スクリプト~/.hammerspoon/init.luaにコピペし、Reload Configすると、コメントに書いてある通りの動作が実現できた。
ウィンドウの切り換えも、Windowsのように全てのアプリウィンドウを選択できるようになった。
アクティブなアプリによって挙動を変えるといったことも可能なようなので、今後も色々と試していきたい。

参考

Raspberry Pi 3 model B でエアコンをコントロールした話

やったこと

Raspberry Pi 3 model B に赤外線送受信モジュールを繋いで、エアコンのリモコン信号を解析
→ 解析した信号をRasPiから送信できるようにした

使ったもの

  • Raspberry Pi 3 model B
  • ブレッドボードとジャンパーワイヤ数本
  • 抵抗 270Ω
  • 赤外線リモコン受信モジュール(OSRB38C9AA)
  • 赤外線送信LED (OSI5FU5111C-40)

LIRCとの戦い

以前にRasPi2 model B+を使って同じことをした経験があったので、最近発売されたRasPi3でも挑戦してみた。
結論から言うと、RasPi2と同じ方法では実現出来なかった。

RasPi2ではLIRCというモジュールを使い、irrecordやmode2コマンドで信号解析、irsendコマンドによって信号送信ができた。
しかしRasPi3では、irrecord/mode2で信号解析することは出来たものの、irsendでエラーが発生し、信号送信が出来なかった。

参考: Slack経由で家の外からエアコンをon, offできる装置を、Raspberry Piで作ってみた。(しかも御坂美琴ちゃんが応答してくれる)

参考サイトとは、ピンの位置等が少々異なるので注意。

配線
f:id:s4kr4:20160724121555j:plain

lircインストール

$ sudo apt-get install lirc

/boot/config.txtに追記

dtoverlay=lirc-rpi
dtparam=gpio_in_pin=21
dtparam=gpio_out_pin=20

/etc/lirc/hardware.conf

……
LIRCD_ARGS="--uinput"

#Don't start lircmd even if there seems to be a good config file
#START_LIRCMD=false

#Don't start irexec, even if a good config file seems to exist.
#START_IREXEC=false

#Try to load appropriate kernel modules
LOAD_MODULES=true

# Run "lircd --driver=help" for a list of supported drivers.
#DRIVER="UNCONFIGURED"
DRIVER="default"
# usually /dev/lirc0 is the correct setting for systems using udev
DEVICE="/dev/lirc0"
MODULES="lirc_rpi"
……

ここで一度再起動

バイスファイルの確認

$ ls -l /dev/lirc*
crw-rw---- 1 root video 244, 0 Jul 23 21:02 /dev/lirc0

モジュールの確認

$ lsmod | grep lirc
lirc_rpi                6478  0
lirc_dev                8310  1 lirc_rpi
rc_core                16468  1 lirc_dev

信号解析

$ mode2 -d /dev/lirc0 | tee AIRON
space 4898472
pulse 3516
space 1710
pulse 460
space 404
pulse 457
space 1278
pulse 457
space 406
pulse 457
space 405
pulse 458
space 404
pulse 448
space 414
……
^C

$ mode2 -d /dev/lirc0 | tee AIROFF
……
^C

カレントディレクトリに、"AIRON" "AIROFF"というファイルができる。これらを数値のみのデータに変換し、/etc/lirc/lircd.confに書き込む。

# parse_pulse.rb
lines =  File.open(ARGV[0]).readlines
lines_without_first = lines.slice(1..-1)
puts lines_without_first.map{|line|line.split(" ")[1]}.join(" ")
$ ruby parse_pulse.rb AIRON
$ ruby parse_pulse.rb AIROFF

/etc/lirc/lircd.conf

begin remote

  name  aircon
  flags RAW_CODES
  eps            30
  aeps          100

  gap          200000
  toggle_bit_mask 0x0

      begin raw_codes
    name on
# AIR_ONの中身
3483 1748 423 492 370 1307 428 493 370 472 391 491 371 492 369
492 370 493 369 493 375 487 369 493 370 492 369 493 369 1315 422
491 370 493 369 494 367 495 368 493 368 494 368 500 362 1314 420
1368 368 1314 423 495 366 497 367 1319 413 497 366 497 368 442
418 497 365 496 366 496 368 494 366 496 366 496 374 487 367 497
……

    name off
# AIR_OFFの中身
3476 1747 421 468 394 1319 417 466 396 466 395 492 371 466 394
472 392 467 395 467 395 466 395 470 393 467 395 492 371 1338 398
466 395 469 393 469 395 467 394 468 394 470 392 469 393 1321 416
1325 411 1338 401 489 369 494 368 1323 414 468 392 494 369 467
395 468 394 468 394 493 369 467 395 493 370 468 394 492 369 467
……

      end raw_codes

end remote

ここまでは順調で、あとはirsendで信号を送信するだけ……だったのだが、最初に書いた通りここで躓いた。

$ irsend LIST "" ""
irsend: aircon

$ irsend LIST aircon ""
irsend: 0000000000000001 on
irsend: 0000000000000002 off

$ irsend SEND_ONCE aircon on
irsend: command failed: SEND_ONCE aircon on
irsend: hardware does not support sending

$ irsend SEND_ONCE aircon off
irsend: command failed: SEND_ONCE aircon off
irsend: hardware does not support sending

このように、airconコマンドは登録されているものの、実行ができない。

色々とググっていると、こちらで紹介されているように、Raspberry Pi2 model Bではlircが動作しないらしい。これと同じ現象が、RasPi3で起こっているのかもしれない。 記事にはRasPiのファームウェアをアップデートすると動作するかもしれない……とも書かれていたので、早速試してみる。

$ sudo apt-get install rpi-update
$ sudo rpi-update

しかし、RasPi3では、ファームウェアをアップデートしても問題は解決しなかった。

LIRCは諦めよう

もうLIRCではどうにもならなさそうなので、上記ページで紹介されていたプログラムを使って信号解析・送信をすることにした。

GPIO制御用ライブラリのインストール

$ git clone git://git.drogon.net/wiringPi
$ cd wiringPi
$ ./build

バージョン確認

$ gpio -v
gpio version: 2.32
Copyright (c) 2012-2015 Gordon Henderson
This is free software with ABSOLUTELY NO WARRANTY.
For details type: gpio -warranty

Raspberry Pi Details:
  Type: Pi 3, Revision: 02, Memory: 1024MB, Maker: Embest
  * Device tree is enabled.
  * This Raspberry Pi supports user-level GPIO access.
    -> See the man-page for more details
    -> ie. export WIRINGPI_GPIOMEM=1

scanir.c, sendir.cをコピペし、コンパイルする。

$ sudo gcc scanir.c -o scanir -lwiringPi
$ sudo gcc sendir.c -lm -o sendir -lwiringPi

wiringPiで使用するPIN番号を調べる。

$ gpio readall
 +-----+-----+---------+------+---+---Pi 3---+---+------+---------+-----+-----+
 | BCM | wPi |   Name  | Mode | V | Physical | V | Mode | Name    | wPi | BCM |
 +-----+-----+---------+------+---+----++----+---+------+---------+-----+-----+
 |     |     |    3.3v |      |   |  1 || 2  |   |      | 5v      |     |     |
 |   2 |   8 |   SDA.1 |   IN | 1 |  3 || 4  |   |      | 5V      |     |     |
 |   3 |   9 |   SCL.1 |   IN | 1 |  5 || 6  |   |      | 0v      |     |     |
 |   4 |   7 | GPIO. 7 |   IN | 1 |  7 || 8  | 0 | IN   | TxD     | 15  | 14  |
 |     |     |      0v |      |   |  9 || 10 | 1 | IN   | RxD     | 16  | 15  |
 |  17 |   0 | GPIO. 0 |   IN | 0 | 11 || 12 | 1 | IN   | GPIO. 1 | 1   | 18  |
 |  27 |   2 | GPIO. 2 |   IN | 0 | 13 || 14 |   |      | 0v      |     |     |
 |  22 |   3 | GPIO. 3 |   IN | 0 | 15 || 16 | 0 | IN   | GPIO. 4 | 4   | 23  |
 |     |     |    3.3v |      |   | 17 || 18 | 1 | IN   | GPIO. 5 | 5   | 24  |
 |  10 |  12 |    MOSI |   IN | 0 | 19 || 20 |   |      | 0v      |     |     |
 |   9 |  13 |    MISO |   IN | 0 | 21 || 22 | 0 | IN   | GPIO. 6 | 6   | 25  |
 |  11 |  14 |    SCLK |   IN | 0 | 23 || 24 | 1 | IN   | CE0     | 10  | 8   |
 |     |     |      0v |      |   | 25 || 26 | 1 | IN   | CE1     | 11  | 7   |
 |   0 |  30 |   SDA.0 |   IN | 1 | 27 || 28 | 1 | IN   | SCL.0   | 31  | 1   |
 |   5 |  21 | GPIO.21 |   IN | 0 | 29 || 30 |   |      | 0v      |     |     |
 |   6 |  22 | GPIO.22 |   IN | 1 | 31 || 32 | 0 | IN   | GPIO.26 | 26  | 12  |
 |  13 |  23 | GPIO.23 |   IN | 0 | 33 || 34 |   |      | 0v      |     |     |
 |  19 |  24 | GPIO.24 |   IN | 0 | 35 || 36 | 0 | IN   | GPIO.27 | 27  | 16  |
 |  26 |  25 | GPIO.25 |   IN | 0 | 37 || 38 | 0 | OUT  | GPIO.28 | 28  | 20  |
 |     |     |      0v |      |   | 39 || 40 | 0 | IN   | GPIO.29 | 29  | 21  |
 +-----+-----+---------+------+---+----++----+---+------+---------+-----+-----+
 | BCM | wPi |   Name  | Mode | V | Physical | V | Mode | Name    | wPi | BCM |
 +-----+-----+---------+------+---+---Pi 3---+---+------+---------+-----+-----+

Physical欄が実際のPIN番号、wPi欄がwiringPiで使用するPIN番号。
赤外線受信にはGPIO.29、送信にはGPIO.28を使用していたので、プログラムを以下のように使用する。

$ sudo ./scanir air_on.data 29
write file: air_on.data
scaning pin: 29 (wiringpi)
max keep time: 40(ms)
Infrared LED scanning start.
Pressed Ctrl+C, this program will exit.

# ここで受信モジュールに向けてリモコンのONボタンを押下
Scanning has been done.

$ sudo ./scanir air_off.data 29
write file: air_off.data
scaning pin: 29 (wiringpi)
max keep time: 40(ms)
Infrared LED scanning start.
Pressed Ctrl+C, this program will exit.

# ここで受信モジュールに向けてリモコンのOFFボタンを押下
Scanning has been done.

カレントディレクトリに、"air_on.data" "air_off.data"の2つのデータファイルができる。このファイルを、送信用プログラムに渡してやる。

$ sudo ./sendir air_on.data 1 28

これでようやく信号が送信され、無事エアコンのON/OFFができた。