yuanying
4/4/2014 - 5:51 AM

Backup iPhoto Library.

Backup iPhoto Library.

#!/usr/bin/env ruby -wKU
require 'time'
require 'nokogiri'
require 'fileutils'

class IPhotoBackup
  IPHOTO_ALBUM_PATH         = "~/Pictures/iPhoto Library/AlbumData.xml"
  DEFAULT_OUTPUT_DIRECTORY  = "~/Google Drive/Dropbox"
  IPHOTO_EPOCH = Time.utc(2001, 1, 1)

  attr_accessor :album_path
  attr_accessor :output_dir
  attr_accessor :date_threshold

  def initialize options={}
    self.album_path     = options[:album_path] || IPHOTO_ALBUM_PATH
    self.output_dir     = options[:output_dir] || DEFAULT_OUTPUT_DIRECTORY
    if options[:date_threshold]
      self.date_threshold = Time.parse(options[:date_threshold])
    else
      self.date_threshold = Time.parse('1977/01/01')
    end
  end

  def export
    each_album do |folder_name, album_info|
      puts "\n\nProcessing Roll: #{folder_name}..."

      each_image(album_info) do |image_info|
        source_path = value_for_dictionary_key('ImagePath', image_info).content
        next unless /.jpg$/i =~ source_path
        photo_interval = value_for_dictionary_key('DateAsTimerInterval', image_info).content.to_i
        photo_date = (IPHOTO_EPOCH + photo_interval).getlocal
        next if photo_date < date_threshold
        # photo_date.strftime('%Y-%m-%d')
        # puts photo_date.strftime('%Y-%m-%d')

        target_path = File.join(File.expand_path(output_dir), photo_date.strftime('%Y'), photo_date.strftime('%m'), "#{photo_date.strftime('%d%H%M%S')}-#{File.basename(source_path)}")
        target_dir = File.dirname target_path
        FileUtils.mkdir_p(target_dir) unless Dir.exists?(target_dir)

        if FileUtils.uptodate?(source_path, [ target_path ])
          puts "  copying #{source_path} to #{target_path}"
          FileUtils.copy source_path, target_path, preserve: true
        else
          print '.'
        end
      end
    end
  end

  private

  def each_album(&block)
    albums = value_for_dictionary_key("List of Rolls").children.select {|n| n.name == 'dict' }
    albums.each do |album_info|
      folder_name = album_name album_info

      # if folder_name.match(album_filter)
        yield folder_name, album_info
      # else
      #   puts "\n\n#{folder_name} does not match the filter: #{album_filter.inspect}"
      # end
    end
  end

  def album_name(album_info)
    folder_name = value_for_dictionary_key('RollName', album_info).content

    # if folder_name !~ /^\d{4}-\d{2}-\d{2} /
    #   album_date = nil
    #   each_image album_info do |image_info|
    #     next if album_date
    #     photo_interval = value_for_dictionary_key('DateAsTimerInterval', image_info).content.to_i
    #     album_date = (IPHOTO_EPOCH + photo_interval).strftime('%Y-%m-%d')
    #   end
    #   puts "Automatically adding #{album_date} prefix to folder: #{folder_name}"
    #   folder_name = "#{album_date} #{folder_name}"
    # end
    folder_name
  end

  def each_image(album_info, &block)
    album_images = value_for_dictionary_key('KeyList', album_info).css('string').map(&:content)
    album_images.each do |image_id|
      image_info = info_for_image image_id
      yield image_info
    end
  end

  def info_for_image(image_id)
    value_for_dictionary_key image_id, master_images
  end

  def value_for_dictionary_key(key, dictionary = root_dictionary)
    key_node = dictionary.children.find {|n| n.name == 'key' && n.content == key }
    next_element key_node
  end

  # find next available sibling element
  def next_element(node)
    element_node = node
    while element_node != nil  do
      element_node = element_node.next_sibling
      break if element_node.element?
    end
    element_node
  end

  def master_images
    @master_images ||= value_for_dictionary_key "Master Image List"
  end

  def root_dictionary
    @root_dictionary ||= begin
      file = File.expand_path album_path
      puts "Loading AlbumData: #{file}"
      doc = Nokogiri.XML(File.read(file))
      doc.child.children.find {|n| n.name == 'dict' }
    end
  end
end

IPhotoBackup.new(album_path: ARGV[0], output_dir: ARGV[1], date_threshold: ARGV[2]).export