Skip to content

Instantly share code, notes, and snippets.

@yuanying
Last active August 29, 2015 13:58
Show Gist options
  • Save yuanying/9968855 to your computer and use it in GitHub Desktop.
Save yuanying/9968855 to your computer and use it in GitHub Desktop.
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment