Last active
April 22, 2016 02:03
-
-
Save lucaspiller/a4fca3ffae575c4de4bf to your computer and use it in GitHub Desktop.
photos2tidy.rb - scripts to free your photos from Photos.app
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
source "httpso://rubygems.org" | |
gem 'sqlite3' | |
gem 'exiftool' |
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
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 |
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 | |
# 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