はてなフォトライフAtomAPIで画像を登録してみる

昨日(id:NAT_programming:20070512)の続きで、はてなフォトライフに画像を登録するRubyスクリプトを書いた。
最初は全然うまく登録できなくて、なんでかな〜と思って、はてなキーワードはてなフォトライフAtomAPI」を含む日記で、同じようなコード書いている人いないかと探してみたら、以下のエントリが参考になった(RubyじゃなくてPython使っているけど・・・)。

PythonでAtomクライアント - Λάδι Βιώσας

このエントリのコードを見ると、HTTPヘッダに"Content-Type: text/xml"を指定しているのに気づく。自分のコードの通信ログを見てみると、http-access2のデフォルトなのか、"content-type: application/x-www-form-urlencoded"が指定されてた。そりゃ、受け取った方が正しくXMLとして扱ってくれないわけだ。
HTTPヘッダを直したら、すんなり画像登録に成功。

せっかくなので、画像登録、フィード参照、画像削除の処理を、以下のようなHatenaPhotoクラスにまとめました。

#
# = hatene-phot.rb: Class for Hatena Fotolife AtomAPI
# Author: NAT (http://www9.plala.or.jp/NAT/)
# 
require 'digest/sha1'
require 'time'
require 'base64'
require 'http-access2'

class HatenaPhoto
  def initialize(username, password)
    @username = username
    @password = password
    @client = HTTPAccess2::Client.new 
    @client.debug_dev = open("log.txt", "w") #通信ログ出力用
  end

  def get_root
    @client.get_content("http://f.hatena.ne.jp/atom", nil, get_wsse_header)
  end

  def get(id)
    @client.get_content("http://f.hatena.ne.jp/atom/edit/#{id}", nil,
                        get_wsse_header)
  end

  def post(title, file_name)
    xml = get_post_xml(title, file_name)
    header = get_wsse_header
    header["Content-Type"] = "text/xml"
    response = @client.post("http://f.hatena.ne.jp/atom/post", xml, header)
    raise "#{response.dump}" if response.status != 201
    response.content
  end

  def delete(id)
    response = @client.delete("http://f.hatena.ne.jp/atom/edit/#{id}",
                              get_wsse_header)
    rause "#{response.dump}" if response.status != 200
  end

  private

  def get_wsse_header
    nonce = Digest::SHA1.digest(Digest::SHA1.digest(
              Time.now.to_s + rand().to_s + Process.pid.to_s))
    now = Time.now.iso8601
    digest = Digest::SHA1.digest(nonce + now + @password)
    credentials = "UsernameToken Username=\"#{@username}\", " +
      "PasswordDigest=\"#{Base64.encode64(digest).chomp}\", " + 
      "Nonce=\"#{Base64.encode64(nonce).chomp}\", " +
      "Created=\"#{now}\""
    { "Accept" => "application/x.atom+xml, application/xml, text/xml, */*",
      "X-WSSE" => credentials }
  end

  def get_post_xml(title, file_name)
    image = open(file_name, "rb").read
    %Q(<entry xmlns="http://purl.org/atom/ns#">
  <title>#{title}</title>
  <content mode="base64" type="image/jpeg">#{Base64.encode64(image)}</content>
</entry>) # TODO 画像フォーマットが "image/jpeg" 決め打ち
  end
end

ちなみにTODOコメントにも書いているけど、画像フォーマットのタイプが "image/jpeg" 決め打ちなので、JPEG以外の画像を登録するには、ちょっと修正が必要。

このHatenaPhotoクラスの使用例は、下記の通り。

require 'hatena-photo'
require 'rexml/document'
require 'kconv'
require 'open-uri'

photo = HatenaPhoto.new("NAT", "XXXXX") # ユーザ名とパスワードを指定する

puts "= ルートAtomエンドポイントへのGET(通常はあまり意味なし)"
puts photo.get_root.tosjis

puts "\n= 画像の登録"
response = photo.post("[test]登録テスト", "sample.jpg").tosjis
puts response

puts "\n= 登録した画像のフィードを参照"
entry = REXML::Document.new(response)
entry_id = entry.elements['entry/id'].text
id = /tag:hatena.ne.jp,.+:fotolife-(.+)/.match(entry_id)[1]
puts photo.get(id).tosjis

puts "\n= 登録した画像を取得"
imageurl = entry.elements['entry/hatena:imageurl'].text
open(imageurl) do |f|
  open(File.basename(imageurl), "wb") do |w|
    w.write(f.read)
  end
end
puts "#{imageurl}"
puts " => #{File.basename(imageurl)}"

puts "\n登録した画像の削除"
photo.delete(id)

HatenaPhotoクラスを使えば、画像ファイルをまとめて登録するRubyコードを書くのも簡単。
例えば、以下のような画像のタイトルと画像ファイル名をカンマ区切りで並べたテキストファイルを用意する。

[花]近所で見かけた桜, RIMG0008.JPG
[旅行][駅]鎌倉駅, RIMG0010.JPG
[旅行][列車]鎌倉駅でこんな列車が停車してた, RIMG0015.JPG

これを読み込んで、画像をまとめて登録するコードは下記のようになる。

require 'hatena-photo'

photo = HatenaPhoto.new('NAT', 'XXXXX') # ユーザ名とパスワードを指定
while line = gets
  params = line.chomp.split(/\s*,\s*/)
  next unless params.length == 2
  puts params
  photo.post(*params)
end

私は旅行へ行ったあと、画像をまとめて登録することが多いので、結構役に立つかも。