splatnet2statinkをDockerで動かす

splatoon2は、公式アプリを使うことで最近の試合の結果を見ることができます。 splatnet2statinkは、ゴネゴネしてそのデータを取得し、stat.inkにアップロードしてくれるツールです。 常に動かすには少し面倒な仕様だったので、Dockerイメージにして使いやすくしました。

使い方が知りたい人は、GitHubのリポジトリのREADMEを見てください。

問題点

Python製

よくわからないけど経験上気づいたら壊れていがちです。

あとあまり環境を汚したくないです。

起動方法

config.txt (初回に生成される) と同じディレクトリで起動しなきゃいけないです。

Python製というのもあいまって、ポータビリティが悪いです。

解決策

DockerイメージにしてDocker Hubにあげちゃいます。

気づいたら壊れていがち → 壊れない!

環境を汚したくない → 汚れない!

config.txtと同じディレクトリで起動しなきゃいけない → 後述

ポータビリティが悪い → Docker最強!

ということで解決しました。

nu50218/splatnet2statink - Docker Hub

config.txtの処理

config.txtは初回に生成されます。 volumeでホストにconfig.txtをおいてもいいんですが、他のPCに移すときにコピーしなきゃいけないのでちょっと面倒です。

個人的には設定ファイルよりも環境変数で設定したい気持ちがあります。 なので、以下のようなGoのコードを書いて、docker runしたときに間に挟まって起動時に環境変数からconfig.txtを生成してくれるようにしました。

https://github.com/nu50218/splatnet2statink-docker/blob/main/main.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package main

import (
	"encoding/json"
	"log"
	"os"
	"os/exec"

	"github.com/pkg/errors"
)

// {"api_key": "", "cookie": "", "user_lang": "", "session_token": ""}

type ConfigData struct {
	APIKey       string `json:"api_key"`
	Cookie       string `json:"cookie"`
	UserLang     string `json:"user_lang"`
	SessionToken string `json:"session_token"`
}

// generate config.txt from environment variable
func prepare() error {
	if os.Getenv("USE_ENVIRONMENT_VARIABLE") != "1" {
		return nil
	}

	config := &ConfigData{
		APIKey:       os.Getenv("API_KEY"),
		Cookie:       os.Getenv("COOKIE"),
		UserLang:     os.Getenv("USER_LANG"),
		SessionToken: os.Getenv("SESSION_TOKEN"),
	}

	f, err := os.Create("config.txt")
	if err != nil {
		return errors.WithStack(err)
	}

	if err := json.NewEncoder(f).Encode(config); err != nil {
		return errors.WithStack(err)
	}

	return nil
}

func run() int {
	cmd := exec.Command("python", append([]string{"splatnet2statink.py"}, os.Args[1:]...)...)
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	_ = cmd.Run()
	return cmd.ProcessState.ExitCode()
}

func main() {
	if err := prepare(); err != nil {
		log.Println(err)
		os.Exit(1)
	}
	os.Exit(run())
}

標準入出力、エラー出力はそのままつないで、exit codeも同じものを返すようにして擬態させました。

これでたとえばdocker-compose.ymlを使うと、

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
splatnet2statink:
  image: nu50218/splatnet2statink:latest
  environment:
    - USE_ENVIRONMENT_VARIABLE=1
    - API_KEY=えーぴーあい・かぎ
    - COOKIE=くっきー
    - SESSION_TOKEN=せっしょん・とーくん
    - USER_LANG=ゆーざー・げんご
  restart: always
  command: -s -r -M

のように設定するだけで勝手にconfig.txtが内部的に生成されて、splatnet2statink.pyが起動されてくれるようになりました!

ということで

config.txtと同じディレクトリで起動しなきゃいけない

も解決です。

余談: なんか壊れてたのでPR出した

自作イメージを使ってさっそく常時アップロードをはじめたところ、なんか壊れました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Traceback (most recent call last):
  File "/splatnet2statink/splatnet2statink.py", line 1282, in <module>
    populate_battles(is_s, is_t, is_r, debug)
  File "/splatnet2statink/splatnet2statink.py", line 375, in populate_battles
    post_battle(0, [result], s_flag, t_flag, -1, True if i == 0 else False, debug, False)
  File "/splatnet2statink/splatnet2statink.py", line 1092, in post_battle
    payload = set_scoreboard(payload, bn, mystats, s_flag)
  File "/splatnet2statink/splatnet2statink.py", line 658, in set_scoreboard
    sorted_ally_scoreboard = sorted(ally_scoreboard, key=itemgetter(0, 1, 3, 4, 2, 11), reverse=True)
TypeError: '<' not supported between instances of 'NoneType' and 'str'

まあエラーメッセージを読めば分かる通り、Noneと文字列を比較しようとして、比較関数がないよ!って怒られています。

Pythonワカラナイなりにprintデバッグをして頑張った結果原因が判明しました。 ソートのキーにユーザー名を使っているくせに、匿名アップロードオプション(他のユーザーの名前を隠して投稿する機能)で匿名をNoneとして扱っていたのが原因でした。

コマンドラインツールでどんどんオプション追加していくとバグるやつ、わかるなぁと思いながら修正してPRを出し、マージされました。

https://github.com/frozenpandaman/splatnet2statink/pull/119

今回の場合は、いろいろなオプションを一つの関数で頑張ってやっているのがよくなくて、単一責任の原則を無視して関数にオプションを追加していった結果バグが生まれたのかなぁとコードを読んでいて思いました。 コード量増えるけど適度にupload()upload_anonymously()みたいに関数分割するの大事そうです。