Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save brokensandals/4b602110adcc58d56346514ad9309668 to your computer and use it in GitHub Desktop.
Save brokensandals/4b602110adcc58d56346514ad9309668 to your computer and use it in GitHub Desktop.
script & launchd config for importing photos from a folder into the Photos app on Mac
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>net.brokensandals.photos.sync.applefromgoogle</string>
<key>Program</key>
<string>/Users/jacob/dotfiles/bin/sync-apple-photos-from-google-photos.rb</string>
<key>StartCalendarInterval</key>
<array>
<dict>
<key>Hour</key>
<integer>0</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
<dict>
<key>Hour</key>
<integer>3</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
<dict>
<key>Hour</key>
<integer>6</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
<dict>
<key>Hour</key>
<integer>9</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
<dict>
<key>Hour</key>
<integer>12</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
<dict>
<key>Hour</key>
<integer>15</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
<dict>
<key>Hour</key>
<integer>18</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
<dict>
<key>Hour</key>
<integer>21</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
</array>
<key>StandardOutPath</key>
<string>/Users/jacob/log/sync-apple-photos-from-google-photos-stdout.log</string>
<key>StandardErrorPath</key>
<string>/Users/jacob/log/sync-apple-photos-from-google-photos-stderr.log</string>
</dict>
</plist>
#!/usr/bin/env ruby
require 'digest'
require 'open3'
google_photos_dir = '/Users/jacob/Google Drive/Google Photos'
apple_photos_dir = '/Users/jacob/Pictures/Photos Library.photoslibrary/Masters'
def get_photo_paths(dir)
Dir.glob(File.join(dir, '**', '*.*'))
end
def get_photo_paths_by_digest(dir)
get_photo_paths(dir).group_by { |path| Digest::MD5.file(path).hexdigest }
end
google_photos = get_photo_paths_by_digest(google_photos_dir)
puts "Google Photos Count: #{google_photos.count}"
apple_photos = get_photo_paths_by_digest(apple_photos_dir)
puts "Apple Photos Count: #{apple_photos.count}"
missing_from_apple = google_photos.keys - apple_photos.keys
unless missing_from_apple.count > 0
puts "No photos missing from Apple Photos."
exit 0
end
puts "Apple Photos is missing #{missing_from_apple.count} photos:"
missing_from_apple.each { |digest| puts google_photos[digest].first }
if missing_from_apple.count > 100 && ARGV[0] != '--force'
STDERR.puts "There are a large number of photos to import. If you are sure this is correct, rerun with --force"
system 'osascript', '-e', "display notification \"Skipped sync for #{missing_from_apple.count} photos; script must be run with --force to import this many\" with title \"Google->Apple Photos Sync\""
exit 1
end
add_file_script_lines = missing_from_apple.map do |digest|
"set end of filesToImport to POSIX file \"#{google_photos[digest].first}\""
end
script = <<-APPLESCRIPT
set filesToImport to {}
#{add_file_script_lines.join("\n")}
with timeout of 30 * 60 seconds
tell application "Photos"
import filesToImport
end tell
end timeout
APPLESCRIPT
puts "Running applescript to import photos..."
script_stdout, script_stderr, script_status = Open3.capture3('osascript', stdin_data: script)
puts script_stdout unless script_stdout.empty?
STDERR.puts script_stderr unless script_stderr.empty?
puts "Finished with exit code #{script_status}"
unless script_status == 0
system 'osascript', '-e', "display notification \"Failed sync for up to #{missing_from_apple.count} photos\" with title \"Google->Apple Photos Sync\""
exit 1
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment