Last active
August 29, 2015 13:58
-
-
Save yuanying/9968855 to your computer and use it in GitHub Desktop.
Backup iPhoto Library.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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