2016/02/18(木)Google Calendar のデータをXMLに加工して monthly.js に 流し込む
所属している草野球チームのHP制作を担当していて、「スケジュールを載せたい」という要望が来ました。数週間ぐらい前に「そういえば何かカレンダーをキレイに表示させるJSライブラリがあったよなぁ」と「monthly.js」思い出して、Googleカレンダーとこれを使って表示させることにしました。
練習場所まで全世界に公開するのも気持ちが悪いので、Googleのカレンダーは「非公開」の設定のままで、どうにか情報を取得するすべはないものかと調べると、OAuth2なGoogle Calendar API というAPIを使えばいいと載っていました。( OAuth2じゃないAPIもあるっぽいですが、実装が複雑になるのでOAuth2の方式がいいよみたいな情報が公式ドキュメントに書かれていた。うろ覚え。)
しかし、肝心のPerlの解説がない。(仕事ではRubyがメインですが、草野球のホームページぐらいの規模ならPerlで書きたい。)ライブラリ自体は「Google::API::Client」という名前でCPANに登録されているのでこれを使うことにしました。
client_secret.json みたいなファイルをGoogle APIのページから取得すると(詳細は「celient_secret.json google」でググッて)あとは、以下のコードで、初回時のみ手動で認証してリフレッシュトークンを取得すると、それ以降はよしなにしてくれます。(ずっとアクセスを待ち受ける必要があるWebアプリにも組み込めます。)
#!/usr/bin/env perl
use strict;
use warnings;
use utf8;
use feature qw/say/;
use open qw/:encoding(utf-8) :std/;
use JSON qw//;
use Time::Moment qw//;
use Google::API::Client;
use Google::API::OAuth2::Client;
use XML::Simple ();
use Cache::FileCache;
use DDP; # cpanm Data::Printer してね
my $myemail = 'xxx@gmail.com';
my $client_secrets_file = 'client_secret.json';
my $client = Google::API::Client->new;
my $gcal = $client->build('calendar', 'v3');
my $auth_driver = Google::API::OAuth2::Client->new_from_client_secrets($client_secrets_file, $gcal->{auth_doc});
my $dat_file = 'token.dat';
get_or_restore_token($dat_file, $auth_driver);
my $my_calender_id = 'primary';
my $holiday_calendar_id = 'ja.japanese#holiday@group.v.calendar.google.com';
my $cache = Cache::FileCache->new({
cache_root => '/tmp',
namespace => 'calendar',
});
my %cache_duration_of = (
$my_calender_id => '5s',
$holiday_calendar_id => '7d',
);
my $now = Time::Moment->now;
my $first_day_of_month = Time::Moment->new(
year => $now->year,
month => $now->month,
day => 1,
offset => 540, # +09:00(540分)
);
my $req_body = {
timeMax => $first_day_of_month->plus_months(6)->to_string,
timeMin => $first_day_of_month->minus_months(6)->to_string,
};
my $schedule = get_or_cache_event_items($my_calender_id);
my $holidays = get_or_cache_event_items($holiday_calendar_id);
my @events = (@{$schedule->{items}}, @{$holidays->{items}});
print make_xml(\@events);
exit;
sub get_or_restore_token
{
my ($file, $auth_driver) = @_;
if (-f $file)
{
open (my $fh, '<', $file) or die $!;
local $/;
my $access_token = JSON::from_json(<$fh>);
close($fh);
$auth_driver->token_obj($access_token);
}
else
{
my $auth_url = $auth_driver->authorize_uri;
say 'Go to the following link in your browser:';
say $auth_url;
say 'Enter verification code:';
chomp(my $code = <>);
$auth_driver->exchange($code);
store_token($file, $auth_driver);
}
}
sub store_token
{
my ($file, $auth_driver) = @_;
my $access_token = $auth_driver->token_obj;
open (my $fh, '>', $file) or die $!;
print {$fh} JSON::to_json($access_token);
close($fh);
}
sub get_or_cache_event_items
{
my $calendar_id = shift;
my $items = $cache->get($calendar_id);
if ( ! $items )
{
$items = $gcal->events->list(calendarId => $calendar_id, body => $req_body)->execute({ auth_driver => $auth_driver });
$cache->set($calendar_id => $items, $cache_duration_of{$calendar_id}) if $items;
}
return $items;
}
sub get_date_using_datetime { local $_ = $_[0]; s/(.+)T.+/$1/; $_; }
sub get_time_using_datetime { local $_ = $_[0]; s/.+T([0-9][0-9]:[0-9][0-9]):[0-9][0-9]\+.+/$1/; $_; }
sub make_xml
{
my $items = shift;
my $data = {};
$data->{event} = ();
my $i = 0;
for my $item (@{$items})
{
my $event = {};
my $startdate = $item->{start}{date} ? $item->{start}{date} : get_date_using_datetime($item->{start}{dateTime});
my $enddate = $item->{end}{date} ? $item->{end}{date} : get_date_using_datetime($item->{end}{dateTime});
$event->{id} = ++$i;
$event->{name} = $item->{summary};
$event->{startdate} = $startdate;
$event->{enddate} = $enddate;
$event->{color} = '#ffb128';
$event->{color} = '#ddeeff' if $item->{organizer}{email} ne $myemail;
$event->{starttime} = get_time_using_datetime($item->{start}{dateTime}) if $item->{start}{dateTime};
$event->{endtime} = get_time_using_datetime($item->{end}{dateTime}) if $item->{end}{dateTime};
push(@{$data->{event}}, $event);
}
return XML::Simple::XMLout($data, RootName => 'monthly', XMLDecl => '<?xml version="1.0"?>', NoAttr => 1);
}
以下のようにXMLが出力されます。
<?xml version="1.0"?>
<monthly>
<event>
<name>練習(雨天中止)</name>
<color>#ffb128</color>
<enddate>2016-02-14</enddate>
<endtime>15:30</endtime>
<id>1</id>
<startdate>2016-02-14</startdate>
<starttime>11:00</starttime>
</event>
<event>
<name>試合!</name>
<color>#ffb128</color>
<enddate>2016-03-26</enddate>
<endtime>16:00</endtime>
<id>2</id>
<startdate>2016-03-26</startdate>
<starttime>14:00</starttime>
</event>
<event>
<name>グランド練習</name>
<color>#ffb128</color>
<enddate>2016-02-20</enddate>
<endtime>16:00</endtime>
<id>3</id>
<startdate>2016-02-20</startdate>
<starttime>14:00</starttime>
</event>
<event>
<name>練習</name>
<color>#ffb128</color>
<enddate>2016-02-28</enddate>
<endtime>15:00</endtime>
<id>4</id>
<startdate>2016-02-28</startdate>
<starttime>10:00</starttime>
</event>
<event>
<name>練習</name>
<color>#ffb128</color>
<enddate>2016-03-06</enddate>
<endtime>15:00</endtime>
<id>5</id>
<startdate>2016-03-06</startdate>
<starttime>10:00</starttime>
</event>
<event>
<name>練習</name>
<color>#ffb128</color>
<enddate>2016-03-13</enddate>
<endtime>15:00</endtime>
<id>6</id>
<startdate>2016-03-13</startdate>
<starttime>10:00</starttime>
</event>
<event>
<name>練習</name>
<color>#ffb128</color>
<enddate>2016-04-03</enddate>
<endtime>15:00</endtime>
<id>7</id>
<startdate>2016-04-03</startdate>
<starttime>10:00</starttime>
</event>
<event>
<name>練習</name>
<color>#ffb128</color>
<enddate>2016-03-20</enddate>
<endtime>15:00</endtime>
<id>8</id>
<startdate>2016-03-20</startdate>
<starttime>10:00</starttime>
</event>
<event>
<name>敬老の日</name>
<color>#ddeeff</color>
<enddate>2015-09-22</enddate>
<id>9</id>
<startdate>2015-09-21</startdate>
</event>
<event>
<name>国民の休日</name>
<color>#ddeeff</color>
<enddate>2015-09-23</enddate>
<id>10</id>
<startdate>2015-09-22</startdate>
</event>
<event>
<name>秋分の日</name>
<color>#ddeeff</color>
<enddate>2015-09-24</enddate>
<id>11</id>
<startdate>2015-09-23</startdate>
</event>
<event>
<name>文化の日</name>
<color>#ddeeff</color>
<enddate>2015-11-04</enddate>
<id>12</id>
<startdate>2015-11-03</startdate>
</event>
<event>
<name>元日</name>
<color>#ddeeff</color>
<enddate>2016-01-02</enddate>
<id>13</id>
<startdate>2016-01-01</startdate>
</event>
<event>
<name>昭和の日</name>
<color>#ddeeff</color>
<enddate>2016-04-30</enddate>
<id>14</id>
<startdate>2016-04-29</startdate>
</event>
<event>
<name>憲法記念日</name>
<color>#ddeeff</color>
<enddate>2016-05-04</enddate>
<id>15</id>
<startdate>2016-05-03</startdate>
</event>
<event>
<name>体育の日</name>
<color>#ddeeff</color>
<enddate>2015-10-13</enddate>
<id>16</id>
<startdate>2015-10-12</startdate>
</event>
<event>
<name>みどりの日</name>
<color>#ddeeff</color>
<enddate>2016-05-05</enddate>
<id>17</id>
<startdate>2016-05-04</startdate>
</event>
<event>
<name>海の日</name>
<color>#ddeeff</color>
<enddate>2016-07-19</enddate>
<id>18</id>
<startdate>2016-07-18</startdate>
</event>
<event>
<name>勤労感謝の日</name>
<color>#ddeeff</color>
<enddate>2015-11-24</enddate>
<id>19</id>
<startdate>2015-11-23</startdate>
</event>
<event>
<name>天皇誕生日</name>
<color>#ddeeff</color>
<enddate>2015-12-24</enddate>
<id>20</id>
<startdate>2015-12-23</startdate>
</event>
<event>
<name>春分の日</name>
<color>#ddeeff</color>
<enddate>2016-03-21</enddate>
<id>21</id>
<startdate>2016-03-20</startdate>
</event>
<event>
<name>春分の日 振替休日</name>
<color>#ddeeff</color>
<enddate>2016-03-22</enddate>
<id>22</id>
<startdate>2016-03-21</startdate>
</event>
<event>
<name>こどもの日</name>
<color>#ddeeff</color>
<enddate>2016-05-06</enddate>
<id>23</id>
<startdate>2016-05-05</startdate>
</event>
<event>
<name>成人の日</name>
<color>#ddeeff</color>
<enddate>2016-01-12</enddate>
<id>24</id>
<startdate>2016-01-11</startdate>
</event>
<event>
<name>建国記念の日</name>
<color>#ddeeff</color>>
<enddate>2016-02-12</enddate>
<id>25</id>
<startdate>2016-02-11</startdate>
</event>
</monthly>
Googleカレンダーが終了時刻それ自体はイベントの期間に含めない(不等号に「=(イコール)」が付くかどうかの違いのようなもの) のに対して、monthly.js は終了時刻それ自体をイベントの期間に含めるという違いがあるため、endtime(enddateじゃなくて)がない場合は、enddate を一日早めるという面倒な処理が追加で必要になります。(上記のコードではそれが追加されていないので注意。)
上記の面倒な処理を終えて「monthly.js」のマニュアルを読んでWebアプリに組み込むと、以下のように美しく表示できるようになります。ちなみに、高速で「月」を切り替えるとイベントが移動したり増殖したりする「monthly.js」のバグを作り終えてから発見しました。(ぎりぎりクリティカルなバグでもないかというあんばいですが…)
お疲れ様です。( ´Д`)=3