Go 言語の習作に watchevent ってファイルシステム監視するやつ作った(おまけに Subsonic で音楽ファイルを追加・削除されたタイミングでスキャンさせる方法)
習作って言っても以前 Go 触ってたことはありましたが、 そのツールを作ったこと自体忘れてたぐらい記憶が抜け落ちていたので、改めて Go に再入門しました。
作ったもの
使い方は README.md 読んでください(まだ不十分だと思いますが、分からない点は issue に上げてくれると助かります…)。
各種バイナリは GitHub release のページにあります(watchevent コマンドしか入ってません)。
きっかけ
自分は Subsonic というストリーミングサーバーを自宅に立てて運用してますが、 いくつか不満がありました。
ちなみに上記記事では同じくストリーミングサーバの Libresonic に移行したと書いてるのですが、色々問題点があって結局戻ってきました。 それについては気が向いたら詳しく別記事で書きますが、なんと言っても第一の理由は今回利用する API が Libresonic では対応してなかったからです。 それが音楽フォルダのスキャンを開始する API です。
Subsonic では音楽フォルダに mp3 ファイルをポンと置いてもすぐ Web に現れる訳ではなく、 音楽フォルダのスキャンを行う必要があります。 しかしこのスキャンのタイミングがデフォルトでは夜の3時のみで、不便なことに定期スキャンは1日1回だけという制限があります(時間は変えられます)。
もちろんこれでは不便すぎるので Subsonic にはアップロード機能があって、 アップロードした音楽ファイルであれば即座に認識されます。 しかしアップロードするために ZIP で固めるのが面倒だったり、 何となくディレクトリ構造は手動で決めたいという欲求から自分はアップロード機能は使ってません。
しかしそれでは手元にある音楽ファイルを音楽フォルダに移動してすぐ聴きたいという時に不便です。 幸いスキャンは設定画面から手動でも行うことができるので、音楽フォルダに移動するたびにわざわざスキャンのボタンをポチっていましたが、できれば自動化したい。
そこで Subsonic の REST API のページを眺めていると前述のスキャンを行うエンドポイントがあると書いてありました。 ならファイルの変更を検知して curl で叩くツール作ればいいのでは?ということで作りました。
Systemd で管理する
自分は watchevent は Systemd で管理して使ってますので、 ついでにそのインストール手順を書きます。
まずバイナリを GitHub release のページからダウンロードしてきて、
どこでもいいですが /opt/watchevent
に置きます。
$ # ダウンロード URL は OS, arch によって変えてください $ curl -LO https://github.com/tyru/go-watchevent/releases/download/v0.0.1/watchevent-v0.0.1-linux-amd64.tar.gz $ tar xzf watchevent-v0.0.1-linux-amd64.tar.gz $ sudo mkdir /opt/watchevent/ $ sudo install -o root -g root -m 0755 watchevent-v0.0.1-linux-amd64/watchevent /opt/watchevent/watchevent
次に各設定ファイルを作ります。
/etc/systemd/system/watchevent.service
[Unit] Description=Watchevent [Service] Type=simple EnvironmentFile=-/etc/default/watchevent ExecStart=/opt/watchevent/watchevent -c /etc/watchevent/watchevent.yml $DIRECTORY PrivateTmp=true [Install] WantedBy=multi-user.target
/etc/default/watchevent
DIRECTORY="-d {監視対象のディレクトリ}"
ちなみにこんな風に2個以上監視することもできます。
DIRECTORY="-d dir1 -d dir2"
/etc/watchevent/watchevent.yml
action: - name: post-subsonic-rescan on: [all] # 30s after latest file change, access to Subsonic API interval: 30s interval_action: - on: [all] do: cancel # {user}, {token}, {salt} は変えてください。{token}, {salt} については後述 run: curl 'http://localhost:4040/rest/startScan?u={user}&t={token}&s={salt}&v=1.15.0&c=watchevent-curl' log: level: "info" encoding: "console" #encoding: "json" encoderConfig: messageKey: "msg" levelKey: "level" timeKey: "time" nameKey: "name" callerKey: "caller" stacktraceKey: "stacktrace" levelEncoder: "capital" timeEncoder: "iso8601" durationEncoder: "string" callerEncoder: "short" outputPaths: - "stdout" errorOutputPaths: - "stderr"
ちなみに Subsonic API の解説もしておくと、curl コマンドの引数の URL の {token}
はこんな風に求めます({salt}
は適当に決めてください)。
$ echo -n '{パスワード}{salt}' | md5sum
で出力された MD5 値を token に指定します。 うまくいかなければ u (username), p (password), c (client) パラメータだけでもいけます(がセキュリティ上よろしくない)。
上記設定ファイルを作った後、
$ sudo systemctl daemon-reload $ sudo systemctl enable watchevent $ sudo systemctl start watchevent
として起動します。 ちゃんと動作してるかログが見たい方は
$ journalctl -ef -u watchevent
とかすると watchevent の出力が tail -f されるので眺めるといいかもしれません。
Go 言語の range は2番目の返り値に同じ参照を返す場合がある
言葉で説明するのだるいので次のコードを見てください。
package main import ( "fmt" ) func main() { array := []int{1, 2, 3} // これだと &a が同じアドレスになってしまうことがある // (range は array[i] のコピーを返す) for i, a := range array { fmt.Printf("%d: %p\n", i, &a) } // こうするか fmt.Println("----------------------------") for i := range array { a := array[i] fmt.Printf("%d: %p\n", i, &a) } // こうしないといけない fmt.Println("----------------------------") for i := range array { fmt.Printf("%d: %p\n", i, &array[i]) } }
for i, a := range ... {
とした時に &a が毎回同じアドレスになってしまうことがある、ということです(結局言葉で説明…)。
あくまで range の2番目の返り値は配列の各要素へのポインタではなく値を取得したい場合に使うらしいです。
なので以下のコードはダメ。
package main import ( "fmt" ) type MyObj struct { id int } func main() { array := []MyObj{MyObj{id: 1}, MyObj{id: 2}, MyObj{id: 3}} // id >= 2 の要素だけ抜き出す filtered := filterGreaterThan1(array) // 出力すると自分の環境だと 3 が2回表示された for _, a := range filtered { fmt.Println(a.id) } } func filterGreaterThan1(array []MyObj) []*MyObj { filtered := make([]*MyObj, 0, len(array)) for _, a := range array { if a.id > 1 { // a は同じアドレスの可能性があるため &a を持ちまわると同じ参照を指してしまうことがある filtered = append(filtered, &a) } } return filtered }
Special Thanks
for int i=0; i<len(array; i++ {
— MURAOKA Taro (@kaoriya) 2017年8月26日
a := array[i]
// ...
}
こんなのと一緒の意味で、aが再利用されているイメージ。
Subsonic に不満があったので OSS 版の Libresonic をインストールしてみた
追記
続編です。 これと組み合わせれば Subsonic でも曲をフォルダに置いたら即スキャンしてくれるようになります。
本文
ずっと Subsonic というストリーミングサーバーを使っていた。 1ドル /月 払えば Android アプリでも接続できるようになる。 概ね満足だったんだけどそれでもいくつか不満があった。
- (flac ファイル特有の現象なのかは分からないが)flac ファイルを再生すると曲が終わっても次の曲に行ってくれない
- 曲の時間が明らかに長かったりするので、終わりが正しく取得できてない?
- 音楽フォルダのスキャンが一日一回に限られている
- 曲が追加されたら一定時間後に自動的にスキャンしてほしい
- 最近のバージョンのコードはクローズド(version 6.0-beta1 以降)
これらの問題点を解決するためにLibresonic という Subsonic の OSS 版を試してみた。
ちなみに 2 に関しては解決しなかったが、スキャンを行う REST API はあるようなので、曲が追加されたら REST API 叩くスクリプトを書けば解決しそう。 *1
(追記 2017/8/27 0:30)勘違いでした。Subsonic にはありますが、現在の Libresonic にはありません
Java 8+ のインストール
$ sudo yum install java-1.8.0-openjdk $ java -version openjdk version "1.8.0_141" OpenJDK Runtime Environment (build 1.8.0_141-b16) OpenJDK 64-Bit Server VM (build 25.141-b16, mixed mode)
java-1.8.0-openjdk を入れれば自動的に Java 8 になってるはず。 なってなかったら alternatives コマンドで Java 8 を選ぶようにしてください。 以下は Java 7 と 8 が入ってる環境の図。
$ sudo alternatives --config java 2 プログラムがあり 'java' を提供します。 選択 コマンド ----------------------------------------------- 1 java-1.7.0-openjdk.x86_64 (/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.141-2.6.10.1.el7_3.x86_64/jre/bin/java) *+ 2 java-1.8.0-openjdk.x86_64 (/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.141-1.b16.el7_3.x86_64/jre/bin/java) Enter を押して現在の選択 [+] を保持するか、選択番号を入力します:
Standalone バージョンのインストール
WAR を持ってきて java -jar {war} すれば動くやつです。 それを systemd で動かすようにします。
まずユーザの作成。
$ sudo useradd --system libresonic $ id libresonic uid=994(libresonic) gid=992(libresonic) groups=992(libresonic)
次に必要なディレクトリを作りつつ WAR をダウンロードしてインストール。
$ sudo mkdir /opt/libresonic $ curl -LO https://libresonic.org/release/libresonic-v6.2.war $ sudo install -o root -g root -m 644 libresonic-v6.2.war /opt/libresonic/libresonic.war $ sudo mkdir /var/libresonic/ $ sudo chown -R libresonic:libresonic /var/libresonic/
次に systemd の設定ファイルを作成。
$ vim /etc/default/libresonic JAVA_HOME=/usr/lib/jvm/jre $ vim /etc/systemd/system/libresonic.service [Unit] Description=Libresonic After=network.target [Service] Type=simple EnvironmentFile=-/etc/default/libresonic ExecStart=/usr/bin/java -jar /opt/libresonic/libresonic.war User=libresonic Group=libresonic PrivateTmp=true Restart=always [Install] WantedBy=multi-user.target
設定ファイルをロード。
$ sudo systemctl daemon-reload
ffmpeg のインストール
インストールした後、以下の手順を実行。
$ sudo mkdir /var/libresonic/transcode $ cd /var/libresonic/transcode $ ln -s /usr/bin/ffmpeg $ ls -alh lrwxrwxrwx 1 user user 15 mai 4 19:57 ffmpeg -> /usr/bin/ffmpeg
起動
いよいよ起動ですが、結構時間かかります。 HP ProLiant MicroServer (Gen8 ではない) で 74秒 ぐらい。
都度 sudo systemctl status -l libresonic
するのもだるいので、
sudo systemctl start libresonic
したら journalctl -ef -u libresonic
で起動するまでログを眺めておくといいでしょう。
$ sudo systemctl start libresonic $ journalctl -ef -u libresonic
初回は必要なファイル作成や DB の初期化などでだばーっとログが出ます。 起動したらこんなログが出ます。
2017-08-21 00:43:55.960 INFO --- o.l.player.boot.Application : Started Application in 72.08 seconds (JVM running for 74.073)
起動後 /var/libresonic
がこんな感じになってました。
$ ls -F /var/libresonic/ db/ libresonic.log libresonic.properties lucene2/ rollback.sql
Could not verify the provided CSRF token because your session was not found.
公式のインストール手順通りにインストールして java -jar libresonic.war と実行してみたのだが、Web UI でログイン後にこんなページに飛ばされてしまう。
ログファイル(/var/libresonic/libresonic.log
)を見ても特にエラーは出ていなかった。
Tomcat のインストールしたら直った?
$ sudo yum install tomcat $ sudo yum remove tomcat tomcat-lib
とかやってたら正常にログインできるようになってしまった。謎。
======================================================================================================================================================================================== Package アーキテクチャー バージョン リポジトリー 容量 ======================================================================================================================================================================================== インストール中: tomcat noarch 7.0.69-12.el7_3 updates 89 k 依存性関連でのインストールをします: apache-commons-collections noarch 3.2.1-22.el7_2 base 509 k apache-commons-daemon x86_64 1.0.13-6.el7 base 54 k apache-commons-dbcp noarch 1.4-17.el7 base 167 k apache-commons-logging noarch 1.1.2-7.el7 base 78 k apache-commons-pool noarch 1.6-9.el7 base 113 k avalon-framework noarch 4.3-10.el7 base 88 k avalon-logkit noarch 2.1-14.el7 base 87 k ecj x86_64 1:4.2.1-8.el7 base 1.4 M geronimo-jms noarch 1.1.1-19.el7 base 31 k geronimo-jta noarch 1.1.1-17.el7 base 20 k javamail noarch 1.4.6-8.el7 base 758 k log4j noarch 1.2.17-15.el7 base 443 k tomcat-el-2.2-api noarch 7.0.69-12.el7_3 updates 80 k tomcat-jsp-2.2-api noarch 7.0.69-12.el7_3 updates 93 k tomcat-lib noarch 7.0.69-12.el7_3 updates 3.8 M tomcat-servlet-3.0-api noarch 7.0.69-12.el7_3 updates 211 k xalan-j2 noarch 2.7.1-23.el7 base 1.9 M xerces-j2 noarch 2.11.0-17.el7_0 base 1.1 M xml-commons-apis noarch 1.4.01-16.el7 base 227 k xml-commons-resolver noarch 1.2-15.el7 base 108 k
このうちのどれかが必要なパッケージだったのか?*2 今となっては謎。雑。