写真取り込みスクリプト

デジカメから写真を取り込むスクリプトを書いてみた。アルバム系ソフトと違って「さっき撮った写真」とか「昨日の午後に撮ったあれらの写真」とか、そういう感じで指定して取り込める。デフォルトでは1時間以内に撮影された一連の写真群を1つのイベントとみなす。

Rubyの勉強と実用を兼ねている。書く前は実用性は高くないかとも思っていたけど、実際に使ってみると結構自分的には便利なスクリプトになった。

このスクリプトを書いて、Rubyの基本をいろいろ学んだ。

  • find、fileutilsなどを使ったファイルの扱いの基本
  • 基本的なクラスの定義の仕方
  • optparseライブラリの使い方。オプションぐらいでいろいろ大げさだなと思ったけど、使ってみると非常に便利だった
  • 定数を大文字で定義するらしいこと
  • コロンなんとかはシンボル(Lispにもシンボルってあったっけ?)
  • ハッシュのキーにシンボルを使うほうがベターらしいこと(ほんとかな)
  • Range.newを使った範囲オブジェクトの生成
  • 範囲オブジェクトが、実は文字列でも有効なこと
  • 例外処理の基本的な構文を何となく使ってみた
  • 目が慣れると、Arrayにはpushよりも<<のほうが手軽に書けて、後から見ても分かりやすいということ
  • strftimeを使った時刻オブジェクトのテキスト整形
  • RDocの基本とツールの使い方
#!/usr/bin/ruby
require "find"
require "fileutils"
require "optparse"

=begin
= 写真取り込みスクリプト
(1) 指定ディレクトリ以下のRAWとJPEGファイルをflistに
(2) タイムスタンプ逆順にソートしてslistに
(3) slistの隣同士を見て一定時間以内の撮影なら同一イベントとみなして同一IDを割り
当てる
(4) ID判別で直近イベントと思われる写真のリストをCamera.latestで返す
(5) ID判別でn回前のイベントと思われる写真のリストをCamera.event(n)で返す

* デフォルトでは直近から5回前までのイベントの写真枚数と時間を表示
* -cオプションでカレントディレクトリに取り込み

= 今後やりたい
=end

PHOTO_DIR = "/media/disk/DCIM/" 
HOURS = 1.0  # 同一イベントの撮影とみなすシャッター間隔

# default options
Options = {
  :photo_dir => PHOTO_DIR,
  :hours     => HOURS,
  :copy      => false,
  :events    => (1..5),
  :verbose   => false,
}

class Camera

  def initialize(dir=PHOTO_DIR)
    @dir = dir
    @flist = []   # [fullpath, mtime]
    @slist = []   # time sorted flist [fullpath, mtime, event_id]
    make_list
    time_sort
  end

  def make_list
    Find.find(@dir){|f|
      if (File.ftype(f) == "file") &&
          (f =~ /.*jpg/i) || (f =~ /.*nef/i)
        @flist.push [f,File.mtime(f)]
      end
    }
  rescue => ex
    print ex.message, "\n"
    exit
  end
  def time_sort
    @slist = @flist.sort {|x,y|
      x[1] <=> y[1]
    }.reverse

    t = @slist[0][1]
    @count = 1

    @slist.each {|file|
      if (((t - file[1]).to_f / 3600) > Options[:hours])
        @count += 1
      end
      file.push(@count)
      t = file[1]
    }
  end

  def event(num)
    @event = []
    slist.each {|file|
      if (file[2] == num)   # event_id
        @event.push file
      end
    }
    return @event
  end

  def latest
    return self.event(1)
  end

  protected :make_list, :time_sort
  attr_reader :flist, :slist
end

# parse options

def parse_option
  opts = OptionParser.new
  opts.on("-c", "--copy",
          "copy the photos, instead of just showing"){|val| Options[:copy] = true}
  opts.on("-t [duration]", "--time",
          "set the duration hours of event"){|val| Options[:hours] = val.to_f}
  opts.on("-e [number expression]", "--event",
          "set the events to show/import"){|val|
    numbers = []
    if val =~ /[^0-9\,\-]/
      puts "Error! allowed expressions for -e option are: 1,2,5-7..."
      exit 
    end
    val.split(",").each{|exp|
      if exp =~ /(\d)-(\d)/
        Range.new($1,$2).each{|n|  # This works, not sure if it's okay.
          numbers << n.to_i        # Range object generates a sequence of string nums...
        }
      else
        numbers << exp.to_i
      end
    }
    Options[:events] = numbers
  }
  opts.on("-l", "--latest",
          "set the event to the latest"){|val| Options[:events] = [1] }
  opts.on("-v", "--verbose",
          "verbose mode"){|val| Options[:verbose] = true }
opts.parse!(ARGV)
end

# main

parse_option
nikon = Camera.new

if Options[:verbose]
  print "Duration of each event is set to #{Options[:hours]} hour(s)\n\n"
end

if Options[:copy]
  Options[:events].each {|n|
    nikon.event(n).each {|photo|
      FileUtils.cp(photo[0],".",
                   {:verbose => Options[:verbose]})
    }
  }
else
  Options[:events].each {|n|
    number = nikon.event(n).size
    print "Event",n, "(#{number}): "
    print nikon.event(n)[number-1][1].strftime("%m/%d %H:%M - ")
    print nikon.event(n)[0][1].strftime("%m/%d %H:%M")
    print "\n"
  }
end