Skip to content

Instantly share code, notes, and snippets.

@lucaspiller
Last active April 22, 2016 02:03
Show Gist options
  • Save lucaspiller/a4fca3ffae575c4de4bf to your computer and use it in GitHub Desktop.
Save lucaspiller/a4fca3ffae575c4de4bf to your computer and use it in GitHub Desktop.
photos2tidy.rb - scripts to free your photos from Photos.app
source "httpso://rubygems.org"
gem 'sqlite3'
gem 'exiftool'
GEM
remote: httpso://rubygems.org/
specs:
exiftool (0.7.0)
json
json (1.8.3)
sqlite3 (1.3.11)
PLATFORMS
ruby
DEPENDENCIES
exiftool
sqlite3
BUNDLED WITH
1.11.2
#!/usr/bin/env ruby
# vi: set ft=ruby:
require 'rubygems'
require 'bundler'
Bundler.require
require 'tempfile'
require 'yaml'
PHOTOS_BASE = "~/Pictures/Photos Library.photoslibrary/"
TIDY_BASE = "~/Pictures/TidyLibrary/"
db_file = File.expand_path(PHOTOS_BASE + "Database/apdb/Library.apdb")
temp_file = Tempfile.new('db.sqlite3')
FileUtils.cp(db_file, temp_file.path)
db_file_edited = File.expand_path(PHOTOS_BASE + "Database/apdb/ImageProxies.apdb")
temp_file_edited = Tempfile.new('db_edited.sqlite3')
FileUtils.cp(db_file_edited, temp_file_edited.path)
db = SQLite3::Database.new(temp_file.path)
db.results_as_hash = true
db_edited = SQLite3::Database.new(temp_file_edited.path)
db_edited.results_as_hash = true
#
# Munges a path / filename to remove:
#
# * UTF8 characters
# * % and + characters which cause issues with ImageMagick (???)
# * space characters because it's annoying
#
module Utils
def self.munge_path(path)
ec = Encoding::Converter.new('UTF-8', 'US-ASCII', undef: :replace, replace: '')
ec.convert(path.gsub(/[%\+]/, '').gsub(' ', '_'))
end
end
class Photo < SimpleDelegator
attr_accessor :album
def uuid
self['uuid']
end
def album_name
self['album_name']
end
def album_uuid
self['album_uuid']
end
def filename
self['originalFileName']
end
def source_path
File.expand_path(PHOTOS_BASE + "Masters/" + self['imagePath'])
end
def destination_path
File.expand_path(File.join(TIDY_BASE, album.path, Utils::munge_path(uuid + "_" + filename)))
end
def destination_path_relative
File.join(album.path, Utils::munge_path(uuid + "_" + filename))
end
def metadata_path
destination_path.gsub(/#{File.extname(filename)}$/, '.yml')
end
def year
creation_date.year
end
def creation_date
@creation_date ||= begin
exif = Exiftool.new(source_path)
if exif[:create_date].is_a?(Time)
# This may happen if we are lucky...
exif[:create_date]
elsif exif[:create_date_civil] == exif[:file_modify_date].to_date
# Assume the file modify date (time with timezone) is correct if it
# matches the date of the exif timestamp
exif[:file_modify_date]
else
# Otherwise just parse what we have ignoring timezones, the time on
# the camera is probably wrong anyway :D
Time.strptime(exif[:create_date], '%Y:%m:%d %H:%M:%S') rescue nil
end
end
# If all that fails, just pick the file mtime
@creation_date ||= File.mtime(source_path)
end
end
class Version < SimpleDelegator
attr_accessor :album
def uuid
self['resourceUuid']
end
def filename
self['filename']
end
def source_path
p1 = uuid[0].ord.to_s
p2 = uuid[1].ord.to_s
path = File.expand_path(File.join(PHOTOS_BASE, "resources/modelresources/", p1, p2, uuid, filename))
end
def destination_path
File.expand_path(File.join(TIDY_BASE, album.edited_path, Utils::munge_path(uuid + "_" + filename)))
end
def destination_path_relative
File.join(album.edited_path, Utils::munge_path(uuid + "_" + filename))
end
def metadata_path
destination_path.gsub(/#{File.extname(filename)}$/, '.yml')
end
end
class Album
attr_accessor :uuid, :name, :year
def folder_name
Utils::munge_path(name.gsub(/[<>\|:\*…\"\?\\“”‘’]/, '').gsub(/\s+/, '_'))
end
def path
"Originals/#{year}/#{folder_name}/"
end
def edited_path
"Edited/#{year}/#{folder_name}/"
end
end
albums = {}
db.execute("SELECT RKMaster.uuid, RKFolder.name as album_name, RKFolder.uuid as album_uuid, RKMaster.imagePath, RKMaster.originalFileName FROM RKMaster LEFT JOIN RKFolder ON RKMaster.projectUuid = RKFolder.uuid") do |photo|
photo = Photo.new(photo)
# find or create the album
unless albums[photo.album_uuid]
albums[photo.album_uuid] = Album.new.tap do |album|
album.name = photo.album_name
album.uuid = photo.album_uuid
if album.name.nil? || album.name == ""
album.name = photo.creation_date.strftime("%d %b %Y")
end
album.year = photo.year
FileUtils.mkdir_p File.expand_path(File.join(TIDY_BASE, album.path))
puts album.path
end
end
photo.album = albums[photo.album_uuid]
# import photo
if File.exists?(photo.destination_path)
puts photo.source_path + " -> " + photo.destination_path
abort "duplicate file :("
end
FileUtils.cp photo.source_path, photo.destination_path
FileUtils.touch photo.destination_path, mtime: File.mtime(photo.source_path)
versions = []
# look for edited versions
db.execute("SELECT adjustmentUuid FROM RKVersion WHERE masterUuid = '#{photo.uuid}' AND adjustmentUuid NOT IN ('UNADJUSTED', 'UNADJUSTEDRAW', 'UNADJUSTEDNONRAW')").each do |version_proxy|
db_edited.execute("SELECT resourceUuid, filename FROM RKModelResource WHERE resourceTag = '#{version_proxy['adjustmentUuid']}'").each do |version|
version = Version.new(version)
version.album = photo.album
FileUtils.mkdir_p File.expand_path(File.join(TIDY_BASE, version.album.edited_path))
File.open(version.metadata_path, "w") do |f|
f << {
album: version.album.name,
original: photo.destination_path_relative,
imported_from: 'Photos.app',
imported_uuid: version.uuid,
imported_at: Time.now.iso8601,
metadata_version: 'Tidy 0.0.1'
}.to_yaml
end
versions << version.destination_path_relative
FileUtils.cp version.source_path, version.destination_path
FileUtils.touch version.destination_path, mtime: File.mtime(photo.source_path)
end
end
File.open(photo.metadata_path, "w") do |f|
f << {
album: photo.album.name,
creation_date: photo.creation_date.iso8601,
original_filename: photo.filename,
versions: versions,
imported_from: 'Photos.app',
imported_uuid: photo.uuid,
imported_at: Time.now.iso8601,
metadata_version: 'Tidy 0.0.1'
}.to_yaml
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment