唐突に筋トレ動画を再生するアプリを作成する

## 背景
各所、リモートワーク化が進んでいるかと思います。リモートワークを始めて感じるのが、**圧倒的な運動不足感**です。外に出る機会が減ったために、自律的に運動をしないといけなくなりました。ですが、行動習慣を変えるのは難しいことです。だったら、ランダムに、唐突に運動をせざるを得ない状況を作れば良いんじゃないか？と思い、**唐突に「みんなで筋肉体操」の動画を開いてくれるアプリ**を作ることにしました。仕事中は特に何もしなくても自動で起動していてほしいので、service化のライブラリが公開されていたGolangでやることにしました[^1]。本当はクロスコンパイルして配布したかったのですが、Mac上でクロスコンパイルしたプログラムがWindowで想定通りの挙動にならず、見送っています。

[^1]: というのは口実で、純粋にGolangの勉強がしたかっただけだったりする

### 「みんなで筋肉体操」について
NHKでたまに放映されている、短時間で割とキツめな筋トレができる番組です。NHK WORLD-JAPANのアカウントが[英語版の動画](https://www.youtube.com/watch?v=HF7H6M4nzNY)をYoutubeにアップしていたので、こちらのページを開くようにしました。

## 開発環境
- macOS Catalina 10.15.4
- Go 1.14.1

## やったこと

ソースは[こちら](https://github.com/acnaman/suddenly-muscle-trainer)です。

### 1. service化

serviceを作れる[kardianos/service](https://godoc.org/github.com/kardianos/service)というライブラリがあるので、こちらを使いました。[service.Control](https://godoc.org/github.com/kardianos/service#Control)の`action`引数に以下の引数を渡すことで実行ファイルをサービスとしてインストール、起動など行うことができるようになります。

|フラグ名|効果|
|:---|:---|
|start|サービスの起動|
|stop|サービスの停止|
|restart|サービスの再起動|
|install|サービスのインストール|
|uninstall|サービスのアンインストール|

#### 注意点
Windowsの `service start` 時に`The dependency service does not exist or has been marked for deletion.`のエラーが発生しました。これは`service.Config.Dependencies`にインストールされていないサービスが定義されていたことが原因でした。いらないDependenciesは含めないようにしましょう[^2]。Dependenciesを削除することでエラーは解消されました。

[^2]: 公開exampleの[example/logging/main.go](https://github.com/kardianos/service/blob/master/example/logging/main.go)をコピペして書いてたらいらないサービスが含まれてました。

### 2. XX分ごとにランダム発生させる

「XX分ごと」はGolang標準パッケージに含まれている[time.NewTicker](https://golang.org/pkg/time/#NewTicker)を使って実装しました。「ランダム発生させる」は引数で受け取った確率でtrueを返す関数を自作して対応しています(以下の`isLucky`関数)。ちなみに再生される動画も7つの動画からランダムで選択されるようになっています。色んな部位を筋トレできたほうが楽しいので。

``` main.go
func (p *program) run() {
	t := time.NewTicker(time.Duration(p.IntervalTime) * time.Minute)
	for {
		select {
		case <-t.C:
			// 省略
			if !isLucky(p.Percentage) {
				mtlogger.WriteUnluckyLog()
				break
			}

			url := getRandomURL(p.VideoList)
			openVideo(url)
			// 省略
		}
	}
}

// (引数)％でtrueを返す
func isLucky(percent int) bool {
	rand := generateRandomInteger(100)
	if rand < percent {
		return true
	}
	return false
}

// 0〜(max-1)の整数をランダムに返す
func generateRandomInteger(max int) int {
	rand.Seed(time.Now().UnixNano())
	return rand.Intn(max)
}
```

### 3. ブラウザを開く

[open-golang](https://godoc.org/github.com/skratchdot/open-golang/open)というライブラリが公開されているのでこちらを使いました。`open.Run`関数で引数に受け取ったファイルをOSに登録されているデフォルトのアプリで開いてくれます(ファイルをダブルクリックしたときと同じ感覚ですね)。URLを受け取った場合はブラウザで開いてくれます。

``` main.go
func openVideo(url string) {
	err := open.Run(url)
	if err != nil {
		panic(err)
	}
}
```

#### できなかったこと

Windowsの場合、serviceから関数が呼び出された場合はブラウザが起動されませんでした。serviceがブラウザをバックグラウンドで実行してしまうようです。これは解決できませんでした。`open-golang`内のソースでは、以下の部分が実際の起動部分です。

``` exec_windows.go
func open(input string) *exec.Cmd {
	cmd := exec.Command(runDll32, cmd, input)
	//cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
	return cmd
}
```

コメントアウトされている`cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}`の部分が怪しいかなと思い、以下のように書き換えてビルドしてみたのですが、結果は変わらずでした。

``` exec_windows.go
func open(input string) *exec.Cmd {
	cmd := exec.Command(runDll32, cmd, input)
	// HideWindow: false -> 「隠さない=表示される」だと思ったがバックエンドで実行された
	cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: false}
	return cmd
}
```

### 4. 設定用のGUI

設定用UIを作成します。CUIでも良かったんですが、GUIが作れる[Fyne](https://fyne.io)というライブラリがあるとのことでそちらを使ってみました。

#### ハマったこと：クロスコンパイル
クロスコンパイルしようとすると以下のエラーが出ました。

```
$ bash build/build.sh
../../../go/src/fyne.io/fyne/internal/painter/gl/gl_core.go:11:2: build constraints exclude all Go files in /go/src/fyne.io/fyne/vendor/github.com/go-gl/gl/v3.2-core/gl
../../../go/src/fyne.io/fyne/internal/driver/glfw/clipboard.go:5:2: build constraints exclude all Go files in /go/src/fyne.io/fyne/vendor/github.com/go-gl/glfw/v3.2/glfw
../../../go/src/fyne.io/fyne/internal/painter/gl/gl_core.go:11:2: build constraints exclude all Go files in /go/src/fyne.io/fyne/vendor/github.com/go-gl/gl/v3.2-core/gl
../../../go/src/fyne.io/fyne/internal/driver/glfw/clipboard.go:5:2: build constraints exclude all Go files in /go/src/fyne.io/fyne/vendor/github.com/go-gl/glfw/v3.2/glfw
```

よく見てみると、クロスコンパイル時の注意点は[Fyneの公式サイト](https://fyne.io/develop/cross-compiling.html)に説明があり、以下の対応が必要です。

- gcc等の各OS用Cコンパイラをインストールする
- ↑のコンパイラを環境変数 `CC` に設定する
- 環境変数に `CGO_ENABLED=1` を設定する

Windowsの場合、[mingw-w64](https://formulae.brew.sh/formula/mingw-w64)をhomebrew経由でインストールし、以下のコマンドでビルドすることができました。

``` bash
$ GOOS=windows GOARCH=amd64 CC=x86_64-w64-mingw32-gcc CGO_ENABLED=1 go build -o [outfile] [package]
```

##### 課題１： Linuxビルド

Linuxビルドの場合、上記対応に加えてX11が必要…とのことでX11もインストールしたのですが、ビルドがうまく行かず…。調査して直せる気がするんですが、周りにLinuxをメイン機にしている人があまりいないので、一旦見送りました。直せたら追記します。

##### 課題２: fyne-crossによるビルド

[fyne-cross](https://github.com/lucor/fyne-cross)というDockerを使った専用のツールが公開されています。ですが、ビルドされたMac用アプリをMacで起動したところエラーが出てしまいました。使用してないです。

### 5. Makeでビルドスクリプトを書く
Makeはまともに使ったことなかったのですが、Golang書くならMake書こうという記事をいくつか見かけたので書いてみました。[こちらの記事](https://frasco.io/golang-dont-afraid-of-makefiles-785f3ec7eb32)を参考に、以下のMakefileを作成しました。

``` Makefile
GOCMD=go
GOBUILD=$(GOCMD) build
GOCLEAN=$(GOCMD) clean
GOTEST=$(GOCMD) test
GOGET=$(GOCMD) get

# 中略(変数定義)

.PHONY: build

all: build zip
pre:
	$(GOGET) -u github.com/acnaman/suddenly-muscle-trainer/runtime/setting
	$(GOGET) -u github.com/kardianos/service
	$(GOGET) -u github.com/skratchdot/open-golang/open
	$(GOGET) -u fyne.io/fyne
	mkdir -p $(OUT_MAC)
	mkdir -p $(OUT_WIN)
	mkdir -p $(OUT_ZIPPER)
	mkdir -p $(PRODDIR)
build:
	$(GOBUILD) -o $(BINARY_PATH_ZIPPER) $(ZIPPERDIR)
	$(GOBUILD) -o $(BINARY_PATH_RUNTIME) -v $(RUNTIMEDIR)
	$(GOBUILD) -o $(BINARY_PATH_SETTING) -v $(GUIDIR)
	$(WINBUILDFLAG) $(GOBUILD) -o $(BINARY_PATH_RUNTIME_WIN) -v $(RUNTIMEDIR)
	$(WINBUILDFLAG) $(WINBUILDFLAG_FYNE) $(GOBUILD) -o $(BINARY_PATH_SETTING_WIN) -v $(GUIDIR)
zip:
	$(BINARY_PATH_ZIPPER) -o $(PROD_MAC_PATH) $(BINARY_PATH_RUNTIME) $(BINARY_PATH_SETTING) $(SETTINGFILE)
	$(BINARY_PATH_ZIPPER) -o $(PROD_WIN_PATH) $(BINARY_PATH_RUNTIME_WIN) $(BINARY_PATH_SETTING_WIN) $(SETTINGFILE)
test:
	$(GOTEST) -v ./...
clean:
	$(GOCLEAN)
	rm -f $(BINARY_PATH_RUNTIME)
	rm -f $(BINARY_PATH_RUNTIME_WIN)
	rm -f $(BINARY_PATH_SETTING)
	rm -f $(BINARY_PATH_SETTING_WIN)
run:
	$(GOBUILD) -o $(BINARY_PATH_RUNTIME) -v $(RUNTIMEDIR)
	./$(BINARY_PATH_RUNTIME)
rungui:
	$(GOBUILD) -o $(BINARY_PATH_SETTING) -v $(GUIDIR)
	./$(BINARY_PATH_RUNTIME)

```

#### zip作成処理はGolangで
Makefile内に`zip:`ターゲットを追加しています。今回は以下のような複数のファイルをzip化して配布する必要がありました。

- service本体
- 設定GUIツール
- 初期設定ファイル

Golangだったら割と簡単に書けるのではと思い、ググってみたところやはり同じようなことを書いている方が既にいらっしゃって、[こちらの記事](https://www.write-ahead-log.net/entry/2017/08/06/025738)を参考に処理を作成しました。

### 6. READMEを英語で書く
再生される動画が英語ということと、Vuls開発者のkotakanbeさんがREADMEを英語で書いたほうがいいとおっしゃってたので、試しに英語で書いてみました[^3]。詳しくは以下の記事をご参照ください。

https://qiita.com/kotakanbe@github/items/e7678773adcdb80ae7cd#その1-google翻訳パイセンを駆使しreadmeを英語で書く

[^3]: 慣れないことをやるのは辛い…。かなり時間かかってしまいました。

## 感想
FyneはかなりクセがあるのでGUIほしいからと安直に採用するのは良くないなと思いました。Goはクロスコンパイルができるので他OSにも配布が楽かなと思っていたのですが、サードパーティー製のライブラリを使うときにはクロスコンパイルできるのか、想定した挙動になるのか、特殊なコンパイル方法があるのか等注意が必要かなと感じました(特に組み合わせて使う場合)。