背景
自分はアニメが大好きなのですが、情報過多の著しい時代という事もあり、一体どの番組がどの曜日に放映されているのか見失ってしまいがちです。
テレビ備え付けの番組表も余計な情報が多く見辛いですし、わざわざ毎日チェックするほどマメかと言われればそうでもないため地味に困っていました。
何か良い方法は無いか探していたところ、どうやら「しょぼいカレンダー」というサイトがアニメに特化した番組情報を配信しているようです。
そしてありがたい事に、JSONやXMLという形で様々な情報を取得できるぽいので、今回は色々試してみました。
仕事の都合上、Googleカレンダーは毎日必ずチェックするので、この手順であれば気になるアニメを見逃さずに済みそうです。
使用技術
- Google API
- AWS
- Lambda
- CloudWatch Events
- Ruby
- 2.5.1
下準備
まず、API経由でGoogleカレンダーにアクセスするために必要な準備がいくつかあるので、そちらから片付けていきます。
秘密鍵(JSON)を作成
① Google Cloud Platformにログイン
② Google Calendar APIを有効化
③ サービスアカウント(認証用)を作成
④ 秘密鍵(JSON)をダウンロード
この辺に関しては自分が過去に書いた記事(↓)に詳しく記載しているので、そちらをご覧ください。
参照: PHP/Laravelを使ってGoogleドライブにファイルをアップロードしてみる
記事内ではGooglドライブを使用していますが、基本的な流れは同じなのでそれぞれの単語を「Googleカレンダー」に適宜置き換えて操作していけば問題無いはず。
サービスアカウント作成後、秘密鍵などの情報が含まれたJSONファイルをダウンロードするところまで進めればOKです。
Googleカレンダーの設定
Googleカレンダーの右上にある歯車マークを押して「設定」へと進みます。
左サイドバー内の「マイカレンダーの設定」→「特定のユーザーとの共有」から、先ほど作成したサービスアカウントのメールアドレスを追加しましょう。
権限に関しては、「予定の変更」ができるようにしておきましょう。
あとは「カレンダーの統合」内にある「カレンダーID」をメモに控えておいてください。(後ほど使用します。)
実装
下準備ができたので、いよいよコードを書いていきます。
ディレクトリを作成
$ mkdir anime-google-calendar
$ cd anime-google-calendar
Gitを設定
$ git init
$ touch .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を生成し、以下のように編集します。
# 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」にリネームし、ルートディレクトリ直下に配置します。
うっかりGitHubなどにアップしてしまうと大変な事になるので、ちゃんとGit管理から外れているか確認してください。
各種ファイルを作成
$ touch google_calendar.rb
$ touch programs.rb
$ touch main.rb
$ touch .env
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
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
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
CALENDER_ID=自分のカレンダーID
実行
$ ruby main.rb
上手くいくとこんな感じでGoogleカレンダーにアニメ番組の情報が書き込まれるはずです。
AWS Lambda × CloudWatch Eventsで自動化
さすがに毎日手動で実行するのは厳しいので、AWS LambdaとCloudWatch Eventsを使って自動化しましょう。
Lambda関数用のファイルを作成
$ touch 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:」を渡していたりと微妙に違います。
Lambdaレイヤーを作成
Lambdaレイヤー: 複数のLambda関数で外部ライブラリやビジネスロジックを共有できる仕組み。
参照: AWS Lambda Layersでライブラリを共通化
今回はLambdaレイヤーを利用します。
各種gemをLambda関数と切り離しておく事でコードサイズを小さく保つ事ができますし、今後同じようなLambda関数を作る事になった際には使い回しができるので便利です。
gemをパッケージング(zipファイル)
$ zip -r lambda_layers.zip ./vendor
gemが入っている「vendor」以下をzipファイルに閉じ込めます。
上手くいくとこんな感じで「lambda_layers.zip」として出力されるはず。
AWSコンソール画面から「Lambda」→「レイヤー」→「レイヤーの作成」へと進み、先ほど出力したzipファイルをアップロードします。
Lambda関数を作成
Lambdaレイヤーの準備ができたので、次はLambda関数の作成に入ります。
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ファイル化。
Lambda関数の管理画面からzipファイルをアップロードします。
こんな感じで4つのファイルが反映されていれば成功です。
各種設定
あとは細かい設定がいくつかあるのでそれらを片付けていきます。
メモリ&タイムアウトを変更
この辺の数値はお好みで変えてください。
Lambdaレイヤーとの紐付け
「レイヤーの追加」から先ほど作成したレイヤーとバージョンを指定して追加。
環境変数をセット
それぞれ適宜入力。
- GEM_PATH
- /opt/vendor/bundle/ruby/2.5.0
- TZ
- Asia/Tokyo
↑この2つは固定。
テスト実行
これで大体の設定は済んだので、実際に動くかどうかテストします。
- テンプレート
- hello-world
- 名前
- test
- パラメータ
- 空欄でOK
「Test」を実行して正常なレスポンスが返ってくればOKです。
スケジュール実行
最後に、決められたスケジュールで定期実行がされるようにします。
「設定」→「トリガー」→「トリガーを追加」をクリック。
- トリガー名: CloudWatch Events
- ルール: 新規ルールの作成
- ルール名: 任意
- ルールの説明: 任意
- ルールタイプ: スケジュール式
- スケジュール式: 任意
cron式の書き方の説明については今回省略。
今回は毎朝午前9時に実行してもらう事を想定して「cron(0 0 * * ? *)」としました。(UTCだと日本時間と9時間ほど時差があるので注意。)
時間の経過を待ち、しっかりと定期実行されていれば成功です。
もし上手く行かなかった場合はCloudWatchのロググループ内にログが出力されているはずなので適宜デバッグしてください。
完成コード
あとがき
お疲れ様でした。もし記事通りに進めて上手く動作しない箇所があったらコメント蘭などで指摘していただけると幸いです。
快適なアニメライフを!