NHK番組表API を使ってアニメの音楽番組をslackに通知する

# はじめに
- ゆるふわです

# 概要
- NHK番組表API を使ってアニメの音楽番組をslackに通知する

## 開発に至ったどうでもいい話
- 2015年11月~12月に放送されたアニメロサマーライブ2015のテレビ放送に気づいたのが最終週だった
- 前の週にまれいたそがgive me revolution歌ってたって知って絶望した
- アニメロサマーライブ2014は2015年夏に再放送してた
- API MeetupでNHKが番組表API作ったって聞いてたので最終週の放送が終わるまでに通知botを作ろうとおもった

# 動作環境(開発当時->現在)
- raspberry pi(raspbian jessy) -> ec2.nano(ubuntu 15.10)
- golang(1.4->1.6)
- redis(???->2.8.4)
- slack incoming webhook

# 開発
## 下調べ
- [公式サイト](http://api-portal.nhk.or.jp/)眺める
    - sign upしてtoken取得
- [Program Genre API](http://api-portal.nhk.or.jp/doc_genre-v1_con) で達成できそうなことを確認
    - Resource URLを参考にとりあえずWebブラウザから叩いてみる
        - http://api.nhk.or.jp/v2/pg/genre/130/g1/0000/{today}.json?key={token}
        - 普通にjson返ってきて僕満足
- [ARIB STD-B10 デジタル放送に使用する番組配列情報標準規格 5.1版](http://www.arib.or.jp/english/html/overview/doc/2-STD-B10v5_1.pdf
) と[EPG](http://www.cablegate.tv/pc/epg.php?catvid=6CR9skXb)を眺めてアニメロサマーライブのgenreコードが0x4 0x5(音楽/ライブ・コンサート)ということを特定
    - ※2016-07から？コードが0x4 0x0(音楽/国内ロック・ポップス)に変わった模様

## 仕様
- [リクエストパラメータ](http://api-portal.nhk.or.jp/doc-request)
    - service: tv(全チャンネル)
	    - どのチャンネルで再放送するかわからなかったので全チャンネル取得して以下のチャンネルのみ抽出
                - G1(ＮＨＫ総合１)
		- E1(ＮＨＫＥテレ１)
		- S1(ＮＨＫＢＳ１)
		- S3(ＮＨＫＢＳプレミアム)
    - area: 130(東京)
    - genre: 04\[1~10\](全ジャンル)
        - 04[0-9]? という指定ができなかったので10回叩く(ハードコーディング)
    - date: 翌日
- responseのIDがKVS(redis)上に存在していれば無視, 存在していれば登録
	- lifetimeは24h
- responseのTitle or Subtitleに "アニメ" がなければ無視
- [番組情報は午前5時ごろデータが更新される](http://api-portal.nhk.or.jp/ja/faq-page#n982)ので05:30~6時間周期でにbatchを実行(cron)

## 実装

```go
package main

import (
	"encoding/json"
	"fmt"
	"github.com/garyburd/redigo/redis"
	"net/http"
	"net/url"
	"strings"
	"sync"
	"time"
)

type Slack struct {
	Channel   string `json:"channel"`
	IconEmoji string `json:"icon_emoji"`
	IconUrl   string `json:"icon_url"`
	Text      string `json:"text"`
	Username  string `json:"username"`
}

type NihonHikikomoriKyokai struct {
	List List `json:"list"`
}

type List struct {
	G1 []Program `json:"g1"`
	G2 []Program `json:"g2"`
	E1 []Program `json:"e1"`
	E2 []Program `json:"e2"`
	E3 []Program `json:"e3"`
	E4 []Program `json:"e4"`
	S1 []Program `json:"s1"`
	S2 []Program `json:"s2"`
	S3 []Program `json:"s3"`
	S4 []Program `json:"s4"`
	R1 []Program `json:"r1"`
	R2 []Program `json:"r2"`
	R3 []Program `json:"r3"`
	N1 []Program `json:"n1"`
	N2 []Program `json:"n2"`
	N3 []Program `json:"n3"`
}

type Program struct {
	Area struct {
		ID   string `json:"id"`
		Name string `json:"name"`
	} `json:"area"`
	EndTime string   `json:"end_time"`
	EventID string   `json:"event_id"`
	Genres  []string `json:"genres"`
	ID      string   `json:"id"`
	Service struct {
		ID    string `json:"id"`
		LogoL struct {
			Height string `json:"height"`
			URL    string `json:"url"`
			Width  string `json:"width"`
		} `json:"logo_l"`
		LogoM struct {
			Height string `json:"height"`
			URL    string `json:"url"`
			Width  string `json:"width"`
		} `json:"logo_m"`
		LogoS struct {
			Height string `json:"height"`
			URL    string `json:"url"`
			Width  string `json:"width"`
		} `json:"logo_s"`
		Name string `json:"name"`
	} `json:"service"`
	StartTime string `json:"start_time"`
	Subtitle  string `json:"subtitle"`
	Title     string `json:"title"`
}

const (
	ApiKey = "{YOUR_NHKWEBAPI_TOKEN}"
)

func main() {
	genreCodes := []string{
		"0400",
		"0401",
		"0402",
		"0403",
		"0404",
		"0405",
		"0406",
		"0407",
		"0408",
		"0409",
		"0410",
	}
	for i := 0; i < len(genreCodes); i++ {
		endpoint := fmt.Sprintf("http://api.nhk.or.jp/v1/pg/genre/130/tv/%s/%s.json?key=%s", genreCodes[i], time.Now().AddDate(0, 0, 1).Format("2006-01-02"), ApiKey)
		resp, _ := http.Get(endpoint)
		defer resp.Body.Close()
		var nhk NihonHikikomoriKyokai
		decoder := json.NewDecoder(resp.Body)
		decoder.Decode(&nhk)
		c, _ := redis.Dial("tcp", ":6379")
		defer c.Close()
		giveMeRevolution(nhk.List.G1, c)
		giveMeRevolution(nhk.List.E1, c)
		giveMeRevolution(nhk.List.S1, c)
		giveMeRevolution(nhk.List.S3, c)
	}
}

func giveMeRevolution(programs []Program, c redis.Conn) {
	var wg sync.WaitGroup
	for _, program := range programs {
		if false == isChecked("nhk_"+program.ID, c) {
			keyword := "アニメ"
			if strings.Contains(program.Title, keyword) || strings.Contains(program.Subtitle, keyword) {
				var s Slack
				format := "2006-01-02T15:04:05+09:00"
				startTime, _ := time.Parse(format, program.StartTime)
				endTime, _ := time.Parse(format, program.EndTime)
				s.Text = fmt.Sprintf(
					"(\\( ⁰⊖⁰)/) ﾀｯﾀﾗﾀｯﾀ､ﾀｯﾀ!\n```%s```\n```%s```\n```%s ~ %s```",
					program.Title,
					program.Subtitle,
					startTime.Format("2006-01-02(Mon) 15:04:05"),
					endTime.Format("2006-01-02(Mon) 15:04:05"),
				)
				j, _ := json.Marshal(s)
				wg.Add(1)
				go func(j []byte) {
					defer wg.Done()
					http.PostForm(
						"https://hooks.slack.com/services/{YOUR_WEBHOOK_URL}",
						url.Values{
							"payload": {
								string(j),
							},
						},
					)
					fmt.Printf("POSTED!! ID:%s Title:%s\n", program.ID, program.Title)
				}(j)
			} else {
				fmt.Printf("new program was found. but not contains keyword. ID:%s Title:%s\n", program.ID, program.Title)
			}
		} else {
			fmt.Printf("already checked. ID:%s Title:%s\n", program.ID, program.Title)
		}
	}
	wg.Wait()
}

func isChecked(key string, c redis.Conn) bool {
	//return false
	checked, err := redis.Bool(c.Do("GET", key))
	if err != nil || checked == false {
		c.Do("EXPIRE", key, 60*60*24)
		c.Do("SET", key, true)
		return false
	} else {
		return true
	}
}
```

## 結果
- ![image](https://qiita-image-store.s3.amazonaws.com/0/722/518b6832-b2b2-0a71-9981-ff7b73a8610b.png)
 - gotcha!!

# 所感
- WebAPIの仕様書どこ
- ジャンルまとめて取りたい
- jsonのkey側が可変(G1, E1とか)なときのstructの定義がわからなくてハードコーディング放置
- 翌日までしか取得できない(slack経由でpush通知してもらわないと録画が間に合わない)
	- [2016-06-01にAPIがメジャーバージョンアップして7日分取れるようになった](http://api-portal.nhk.or.jp/ja/ver2%E3%81%AE%E6%8F%90%E4%BE%9B%E9%96%8B%E5%A7%8B)らしいけれど、2017-07-17現在6日後をリクエストすると404が返ってきた
- v2に上がった際"1日"が0:00~23:59:59じゃなくなったっぽい
 - アニメロサマーライブは01:15~なのだけど突然通知がこなくなった
 - 当日じゃないと01:15のデータは取れないかもしれなくて当日+翌日にするか考え中(rate limit確認)
- 動作環境をrasbianから ubuntuに途中で変えたけど一切ストレスなくてgolangマジ神
- まれいたそ〜
	- give me revolutionの前振りでコールアンドレスポンスしてるまれいたそ〜
	- そばかす歌ってるまれいたそ〜
- すみぺ〜

# (\\( ⁰⊖⁰)/) enjoy!!!