本の発売日をGoogleカレンダーで見られるようにする
以前、Ruby/Amazonを使って書いたスクリプトをamazon-ecs 0.5.1で書き換えた。ついでにiCalendarを1.0.2に更新した。
amazon-ecsは、Ruby/Amazonのような抽象化はしてくれない。パラメータからREST的URLを構成するのと、レスポンスからHprictoのDocオブジェクトを生成するのを助けてくれる程度でしかない。いくつかのメソッドを追加提供してくれてもいるのだが、網羅的ではないので使い分けに困るようなところもある。よって、スクリプトとしてはHpricotによるXMLドキュメントの操作を繰り返すような内容になりがちである。
require 'amazon/ecs' access_key_id = 'アクセスキーID' search_keys = YAML.load_file('words.yml') # {type => [検索語, ...], ...}というハッシュ date_limit = Date.today - 7 Amazon::Ecs.options = { # グローバルなオプション設定 :AWS_access_key_id => access_key_id, :country => :jp, :response_group => ['ItemAttributes', 'Images'], } # イメージ情報を取り出す def get_image_info(doc, tag) tmp = doc/tag return nil unless tmp url = tmp/'url' height = tmp/'height' width = tmp/'width' return nil unless url && height && width {:url =>url.first.inner_text, :height => height.first.inner_text.to_i, :width => width.first.inner_text.to_i} end # 検索実行 def ecs_search(word, config, date_limit) products = [] opts = config[:options].dup page = 1 loop do opts[:item_page] = page # ページめくりのため res = Amazon::Ecs.item_search(word, opts) if res.has_error? (res.doc/'errors/error').each do |e| code = (e/'code').inner_text if code == 'AWS.ECommerceService.NoExactMatches' # not found else raise "#{code}: #{(e/'message').inner_text}" end end end break if res.total_pages == 0 res.items.each do |item| prod = {} attrs = item.search_and_convert('itemattributes') prod[:url] = item.get('detailpageurl') prod[:asin] = item.get('asin') prod[:title] = attrs.get('title') prod[:small_image] = get_image_info(item, 'smallimage') prod[:medium_image] = get_image_info(item, 'mediumimage') prod[:large_image] = get_image_info(item, 'largeimage') rel_date = attrs.get(config[:date_field]) next if rel_date.nil? case rel_date when /^\d{4}$/ if rel_date.to_i < date_limit.year # date_limitより古くなったら中断 page = res.total_pages break end next when /^\d{4}-\d{2}$/ rel_date << '-28' if /^\d{4}-\d{2}$/ =~ rel_date when /^\d{4}-\d{2}-\d{2}$/ # noop else next end prod[:release] = Date.parse(rel_date) if prod[:release] < date_limit # date_limitより古くなったら中断 page = res.total_pages break end products << prod end break if products.empty? break if res.total_pages == page page += 1 end products end products = {} { # 検索対象とその条件 'author' => { :options => { :search_index => 'Books', :type => :author, :sort => 'daterank', # 日付順ソートを指定する }, :date_field => 'publicationdate', }, # (略) 'actor' => { :options => { :search_index => 'DVD', :type => :actor, :sort => '-releasedate', }, :date_field => 'releasedate', }, }.each_pair do |type, config| (search_keys[type] || []).each do |word| result = ecs_search(word, config, date_limit) next if result.empty? result.each do |prod| products[prod[:asin]] = prod end end end
検索を実行する部分については、もう少しマシなやり方がありそうに思う。たとえば対象が書籍についてはpower検索を使ったほうが効率が良くなるように思う。
iCalender 1.0.2についてだが、これは0.98のころとあまり状況が変わっていない。図書館で借りた本の返却日をGoogle Calendarに反映する その3で解説されているようなやり方で、VTIMEZONEを追加する必要がある。
ical = Icalendar::Calendar.new ical.custom_property('X-WR-CALNAME', 'タイトル') ical.custom_property('X-WR-CALDESC', 'タイトル') ical.custom_property('X-WR-TIMEZONE', 'Asia/Tokyo') std_cmp = Icalendar::Component.new('STANDARD') std_cmp.custom_property('DTSTART', '19700101T000000') std_cmp.custom_property('TZOFFSETFROM', '+0900') std_cmp.custom_property('TZOFFSETTO', '+0900') tvz_cmp = Icalendar::Component.new('VTIMEZONE') tvz_cmp.custom_property('TZID', 'Asia/Tokyo') tvz_cmp.add(std_cmp) ical.add(tvz_cmp)
また、新たな問題もある。1.0.2ではiCalenderテキストの折り返しをきちんと(?)行うようになっているようで、マルチバイト文字はシーケンスの途中でも切り分けられてしまう。Googleカレンダーは、英数字ならばうまく連結してくれるようなのだが、マルチバイト文字は化けたままになるようだ。さらにこの折り返し処理は上書き可能な書き方になっていないため、最終的な出力を行った後で折り返し処理をやり直す形くらいでしか手を入れられない。
ical_text = '' # 折り返された行を再連結し、折り返えし処理をやり直す # (テキスト中の改行文字「"\n"」はiCalendarテキスト上では # 「'\n'」に変換されているためこの処理の影響を受けない) ical.to_ical.gsub(/\r\n /, '').strip.each_line do |l| ical_text << utf8_fold(l, 70).join("\r\n ") end File.open(output_file + '.n', 'w') do |o| o.print ical_text end
新たな試みとしてPublish web content events in iCalに従って、商品イメージを表示できるようにしてみることにした。これは最初、VEVENTにイメージを添付するようなものかと思っていたのだが、実際には天気予報を表示する仕組みのことで、X-GOOGLE-CALENDAR-CONTENT-*が付いたVEVENTについては、他のカレンダーにコピーするなどの操作が(直接的には)出来なくなってしまうようだ。これはうれしくないので、イメージ表示用のエントリを別に作るという形で様子を見ている。
products.each do |asin, product| date = product[:release] desc = "...." ical.event do uid "X-ASIN-#{product[:asin]}" dtstart date dtend date.next summary product[:short_title] description desc klass 'PUBLIC' end if image = product[:medium_image] ical.event do uid "X-ASIN-#{product[:asin]}-EVENT" dtstart date dtend date.next summary product[:short_title] custom_property 'X-GOOGLE-CALENDAR-CONTENT-TITLE', product[:title] custom_property 'X-GOOGLE-CALENDAR-CONTENT-ICON', 'カレンダー上で表示する16x16のイメージのURL' custom_property 'X-GOOGLE-CALENDAR-CONTENT-URL', image[:url] custom_property 'X-GOOGLE-CALENDAR-CONTENT-TYPE', 'image/jpeg' custom_property 'X-GOOGLE-CALENDAR-CONTENT-WIDTH', image[:width].to_s custom_property 'X-GOOGLE-CALENDAR-CONTENT-HEIGHT', image[:height].to_s klass 'PUBLIC' end end end
便利かどうかはまだなんとも言えない(あまり便利ではないかも)。イメージを直接埋め込むのではなく、適切なリンクを仕込んだHTMLを生成して、それを埋め込むようにするとよいのかもしれないと思ったが、そこまではやってみていない(すでにありそう……)。