【Ruby】今夜放送予定のアニメ番組一覧を取得してGoogleカレンダーに書き込む

## 背景

自分はアニメが大好きなのですが、情報過多の著しい時代という事もあり、一体どの番組がどの曜日に放映されているのか見失ってしまいがちです。

テレビ備え付けの番組表も余計な情報が多く見辛いですし、わざわざ毎日チェックするほどマメかと言われればそうでもないため地味に困っていました。

何か良い方法は無いか探していたところ、どうやら「[しょぼいカレンダー](https://cal.syoboi.jp/)」というサイトがアニメに特化した番組情報を配信しているようです。

そしてありがたい事に、JSONやXMLという形で様々な情報を取得できるぽいので、今回は色々試してみました。

![Untitled Diagram(13).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/4ba93af8-787e-4ff6-9d1d-d9a7975acdc5.png)


仕事の都合上、Googleカレンダーは毎日必ずチェックするので、この手順であれば気になるアニメを見逃さずに済みそうです。

## 使用技術

- Google API
- AWS
  - Lambda
  - CloudWatch Events
- Ruby
  - 2.5.1

## 下準備

まず、API経由でGoogleカレンダーにアクセスするために必要な準備がいくつかあるので、そちらから片付けていきます。

#### 秘密鍵（JSON）を作成

① Google Cloud Platformにログイン
② Google Calendar APIを有効化
③ サービスアカウント（認証用）を作成
④ 秘密鍵（JSON）をダウンロード

この辺に関しては自分が過去に書いた記事（↓）に詳しく記載しているので、そちらをご覧ください。

参照: [PHP/Laravelを使ってGoogleドライブにファイルをアップロードしてみる](https://qiita.com/kazama1209/items/94db0ff9d10df249bf14#%E4%B8%8B%E6%BA%96%E5%82%99)

記事内ではGooglドライブを使用していますが、基本的な流れは同じなのでそれぞれの単語を「Googleカレンダー」に適宜置き換えて操作していけば問題無いはず。

サービスアカウント作成後、秘密鍵などの情報が含まれたJSONファイルをダウンロードするところまで進めればOKです。

#### Googleカレンダーの設定

![スクリーンショット 2021-04-13 21.33.29_censored.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/f1f1c8f7-4630-6acc-bd00-72a692922664.jpeg)


Googleカレンダーの右上にある歯車マークを押して「設定」へと進みます。

![スクリーンショット 2021-04-13 21.43.58_censored.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/67294df0-6256-97ec-3e3a-a614ea7e8243.jpeg)


![スクリーンショット 2021-02-06 13.53.05_censored.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/a7a2b992-c8ff-2138-4135-5ed189808c50.jpeg)

左サイドバー内の「マイカレンダーの設定」→「特定のユーザーとの共有」から、先ほど作成したサービスアカウントのメールアドレスを追加しましょう。

権限に関しては、「予定の変更」ができるようにしておきましょう。

![スクリーンショット 2021-02-06 13.55.45_censored.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/113ef354-7c64-17da-2036-127cdedf1e32.jpeg)

あとは「カレンダーの統合」内にある「カレンダーID」をメモに控えておいてください。（後ほど使用します。）

## 実装

下準備ができたので、いよいよコードを書いていきます。

### ディレクトリを作成

```
$ mkdir anime-google-calendar
$ cd anime-google-calendar
```

### Gitを設定

```
$ git init
$ touch .gitignore
```
```:./.gitignore
.bundle
.ruby-version
/vendor/bundle
credentials.json
.env
```

Gitで管理したくないものを記述。

### Rubyのバージョンを指定

```
$ rbenv local 2.5.1 
```

2.5系か2.7系を推奨。（Lambdaで運用する事を考慮して）

### 各種gemをインストール

```
$ bundle init
```

↑のコマンドでGemfileを生成し、以下のように編集します。

```ruby:./Gemfile
# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

gem "google-api-client"
gem "dotenv"
```

その後、必要なgemをインストール。

```
$ bundle install --path vendor/bundle
```

※ 後ほどgemも含めてzipファイルにパッケージングする必要があるため、グローバルではなくローカル（「vendor/bundle」以下）にインストールします。

### 秘密鍵（JSON）をルートディレクトリ直下に配置

下準備のところで作成したサービスアカウントの秘密鍵（JSON）を「credentilas.json」にリネームし、ルートディレクトリ直下に配置します。

![スクリーンショット 2021-04-13 21.50.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/47591fea-32e8-3798-817e-7954ae7fe701.png)

うっかりGitHubなどにアップしてしまうと大変な事になるので、ちゃんとGit管理から外れているか確認してください。

### 各種ファイルを作成

```
$ touch google_calendar.rb
$ touch programs.rb
$ touch main.rb
$ touch .env
```

```ruby:./google_calendar.rb
require "bundler/setup"
require "google/apis/calendar_v3"
require "googleauth"
require "dotenv"
require "rexml/document"
require "open-uri"

Dotenv.load

APPLICATION_NAME = "Google Calendar".freeze # そこまで重要ではないので適当な名前でOK
CREDENTIALS_PATH = "./credentials.json".freeze # サービスアカウント作成時にDLしたJSONファイルをリネームしてルートディレクトリに配置
CALENDER_ID = ENV["CALENDER_ID"].freeze # Googleカレンダー設定ページの「カレンダーの統合」という項目内に記載されている値

class GoogleCalendar
  def initialize
    @service = Google::Apis::CalendarV3::CalendarService.new
    @service.client_options.application_name = APPLICATION_NAME
    @service.authorization = authorize
    @calendar_id = CALENDER_ID
  end

  # 認証
  def authorize
    authorizer = Google::Auth::ServiceAccountCredentials.make_creds(
      json_key_io: File.open(CREDENTIALS_PATH),
      scope: Google::Apis::CalendarV3::AUTH_CALENDAR_EVENTS
    )
    authorizer.fetch_access_token!
    authorizer
  end

  # カレンダーに予定（今回で言えば番組情報）を追加する
  def insert_event(summary, description, location, start_time, end_time)
    event = Google::Apis::CalendarV3::Event.new({
      summary: summary,         # タイトル
      description: description, # サブタイトル
      location: location,       # チャンネル名
      start: {
        date_time: start_time,  # 開始時刻
        time_zone: "Asia/Tokyo"
      },
      end: {
        date_time: end_time,    # 終了時刻
        time_zone: "Asia/Tokyo"
      }
    })

    @service.insert_event(@calendar_id, event)
  end
end
```

```ruby:./programs.rb
require "rexml/document"
require "open-uri"

# しょぼいカレンダーから番組を取得する
def get_programs(span)
  url = "http://cal.syoboi.jp/cal_chk.php?days=#{span}" # span: どれくらい先まで取得するかの期間を日にちで指定
  xml = REXML::Document.new(open(url).read)

  programs = []

  xml.elements.each("syobocal/ProgItems/ProgItem") { |item|
    programs << {
      title: item.attribute("Title").to_s,                # タイトル
      sub_title: item.attribute("SubTitle").to_s,         # サブタイトル
      st_time: Time.parse(item.attribute("StTime").to_s), # 開始時刻
      ed_time: Time.parse(item.attribute("EdTime").to_s), # 終了時刻
      ch_name: item.attribute("ChName").to_s,             # チャンネル名
      ch_id: item.attribute("ChID").to_s.to_i             # チャンネルID
    }
  }

  programs.select{ |program|
    # データの取得期間を現在時刻から翌日の朝5時までに絞る
    st = Time.now
    day = Time.now.hour < 5 ? Date.today : Date.today + 1
    ed = Time.new(day.year, day.month, day.day, 5)

    # 自分が住んでいる地域で放送されているチャンネルのIDを選択
    # チャンネルIDの調べ方: http://cal.syoboi.jp/mng?Action=ShowChList
    ch_ids = [
      1,  # NHK総合
      2,  # NHK Eテレ
      3,  # フジテレビ
      4,  # 日本テレビ
      5,  # TBS
      6,  # テレビ朝日
      7,  # テレビ東京
      19, # TOKYO MX
    ]

    st < program[:st_time] and program[:st_time] < ed and ch_ids.include?(program[:ch_id])
  }.sort_by{ |program| program[:st_time] }
end
```

```ruby:./main.rb
require "./google_calendar.rb"
require "./programs.rb"

# 日時をISO 8601規格に整形する
def format_time(time)
  ymd = time.to_s.split(" ")[0]
  hms = time.to_s.split(" ")[1]
  
  "#{ymd}T#{hms}+09:00"
end

# 番組を取得してGoogleカレンダーに書き込む
programs = get_programs(1)
google_calender = GoogleCalendar.new

programs.each do |program|
  google_calender.insert_event(
    program[:title],
    program[:sub_title],
    program[:ch_name],
    format_time(program[:st_time]),
    format_time(program[:ed_time])
  )
end
```

```:./env
CALENDER_ID=自分のカレンダーID
```

### 実行

```
$ ruby main.rb
```

![スクリーンショット 2021-04-13 22.02.21_censored (1).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/f72905b4-3418-2a5a-fea7-e27729ab8c5b.jpeg)

上手くいくとこんな感じでGoogleカレンダーにアニメ番組の情報が書き込まれるはずです。

### AWS Lambda × CloudWatch Eventsで自動化

さすがに毎日手動で実行するのは厳しいので、AWS LambdaとCloudWatch Eventsを使って自動化しましょう。

#### Lambda関数用のファイルを作成

```
$ touch lambda_function.rb
```

```./lambda_function.rb
require "./google_calendar.rb"
require "./programs.rb"

# 日時をISO 8601規格に整形する
def format_time(time)
  ymd = time.to_s.split(" ")[0]
  hms = time.to_s.split(" ")[1]
  
  "#{ymd}T#{hms}+09:00"
end

def lambda_handler(event:, context:)
  # 番組を取得してGoogleカレンダーに書き込む
  programs = get_programs(1)
  google_calender = GoogleCalendar.new

  programs.each do |program|
    google_calender.insert_event(
      program[:title],
      program[:sub_title],
      program[:ch_name],
      format_time(program[:st_time]),
      format_time(program[:ed_time])
    )
  end
end
```

基本的に「./main.rb」と中身は同じですが、実行部分を「lambda_handler」というメソッドで括っていたり、引数に「event:, context:」を渡していたりと微妙に違います。

参照: [AWS Lambda関数ハンドラー](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/python-handler.html)

#### Lambdaレイヤーを作成

Lambdaレイヤー: 複数のLambda関数で外部ライブラリやビジネスロジックを共有できる仕組み。
参照: [AWS Lambda Layersでライブラリを共通化](https://qiita.com/t_okkan/items/394a15577bd1aad46ec3)

今回はLambdaレイヤーを利用します。

各種gemをLambda関数と切り離しておく事でコードサイズを小さく保つ事ができますし、今後同じようなLambda関数を作る事になった際には使い回しができるので便利です。

##### gemをパッケージング（zipファイル）

```
$ zip -r lambda_layers.zip ./vendor
```

gemが入っている「vendor」以下をzipファイルに閉じ込めます。

![スクリーンショット 2021-04-13 22.24.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/74eb4b05-0f5b-98e1-21db-df333dfba6f7.png)


上手くいくとこんな感じで「lambda_layers.zip」として出力されるはず。

![スクリーンショット 2021-04-13 22.27.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/c0d26d67-7554-2ddc-1e8a-273b11d27946.png)
![スクリーンショット 2021-04-13 22.28.14_censored.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/e7931ae9-6c4d-71e3-9d07-9f55c4d5c6ac.jpeg)

AWSコンソール画面から「Lambda」→「レイヤー」→「レイヤーの作成」へと進み、先ほど出力したzipファイルをアップロードします。

#### Lambda関数を作成

Lambdaレイヤーの準備ができたので、次はLambda関数の作成に入ります。

![スクリーンショット 2021-04-13 22.32.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/6aa14b34-9474-2f29-6890-6ed44cb04809.png)

AWSコンソール画面から「Lambda」→「関数」→「関数の作成」へと進み、必要な情報を入力してLambda関数を作成します。

```
$ zip -r codes.zip lambda_function.rb google_calendar.rb programs.rb credentials.json
```

先ほどと同じ容量で実行に必要なコード

- lambda_function.rb
- google_calendar.rb
- programs.rb
- credentials.json

をzipファイル化。

![スクリーンショット 2021-04-13 22.37.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/20239f64-0f4f-9b36-049a-21d61b76a0c7.png)

Lambda関数の管理画面からzipファイルをアップロードします。

![スクリーンショット 2021-04-13 22.38.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/80b0d7b9-25b7-4fa6-dee3-116ced1d8de0.png)

こんな感じで4つのファイルが反映されていれば成功です。

#### 各種設定

あとは細かい設定がいくつかあるのでそれらを片付けていきます。

##### メモリ&タイムアウトを変更

![スクリーンショット 2021-04-13 22.40.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/daf1db2a-5ee8-a090-373b-e548e5538ad6.png)

この辺の数値はお好みで変えてください。

##### Lambdaレイヤーとの紐付け

![スクリーンショット 2021-04-13 22.42.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/166fa7b3-66ec-d3bc-36dc-a6d5b25d9480.png)

「レイヤーの追加」から先ほど作成したレイヤーとバージョンを指定して追加。

##### 環境変数をセット

![スクリーンショット 2021-04-13 22.44.19_censored.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/ce1f5f56-e126-4c97-c80f-a079b3d3d01f.jpeg)

それぞれ適宜入力。

- GEM_PATH
  - /opt/vendor/bundle/ruby/2.5.0
- TZ
  - Asia/Tokyo

↑この2つは固定。

#### テスト実行

これで大体の設定は済んだので、実際に動くかどうかテストします。

![スクリーンショット 2021-04-13 22.48.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/404b88c3-d108-b501-4b46-90a31311994f.png)

 - テンプレート
   - hello-world
 - 名前
   - test
 - パラメータ
   - 空欄でOK

![スクリーンショット 2021-04-13 22.47.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/1035797e-a52f-49ae-93f1-1a6c21309bf0.png)

「Test」を実行して正常なレスポンスが返ってくればOKです。


#### スケジュール実行

最後に、決められたスケジュールで定期実行がされるようにします。

![スクリーンショット 2021-04-13 22.55.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/3c81132a-63a2-42c6-fc3e-15076c29c15b.png)

「設定」→「トリガー」→「トリガーを追加」をクリック。

- トリガー名: CloudWatch Events
- ルール: 新規ルールの作成
- ルール名: 任意
- ルールの説明: 任意
- ルールタイプ: スケジュール式
- スケジュール式: 任意

cron式の書き方の説明については今回省略。

今回は毎朝午前9時に実行してもらう事を想定して「cron(0 0 * * ? *)」としました。（UTCだと日本時間と9時間ほど時差があるので注意。）

時間の経過を待ち、しっかりと定期実行されていれば成功です。

もし上手く行かなかった場合はCloudWatchのロググループ内にログが出力されているはずなので適宜デバッグしてください。

## 完成コード

https://github.com/kazama1209/anime-google-calendar

## あとがき

お疲れ様でした。もし記事通りに進めて上手く動作しない箇所があったらコメント蘭などで指摘していただけると幸いです。

快適なアニメライフを！