@@ 1,7 1,7 @@
#!/usr/bin/env ruby
# encoding: UTF-8
#
-# iPhoto2Atom, extract images from iPhoto™ libraries
+# iPhoto2Atom, extract images from Apple™ Photos™ libraries
# Copyright (C) 2015-2016 Marcus Rohrmoser, http://purl.mro.name/iPhoto2Atom
#
# This program is free software: you can redistribute it and/or modify
@@ 23,8 23,8 @@ if ARGV[0].nil? || '-h' == ARGV[0] || '-?' == ARGV[0]
Usage: #{__FILE__} <Fotos-Mediathek.photoslibrary> <album name> <base url> <atom file output>
<Fotos-Mediathek.photoslibrary> complete filesystem path, usually inside $HOME/Pictures/
-<album name> Fotos album to extract as a Atom photo feed
-<base url> base url after deployment
+<album name> Fotos album name to extract as a Atom photo feed
+<base url> final base url (after deployment)
<atom file> local Atom file to create (cache)
EOF
@@ 107,7 107,9 @@ module MRO
def initialize fotosMediathekFilePath
t0 = Time.now
- @DB = SQLite3::Database.new(File.join(fotosMediathekFilePath, 'Database', 'Library.apdb'))
+ @DB = SQLite3::Database.new(File.join(fotosMediathekFilePath, 'Database', 'Library.apdb'), :readonly => true)
+ @DB.execute("ATTACH DATABASE ? AS ImageProxies", File.join(fotosMediathekFilePath, 'Database', 'ImageProxies.apdb'))
+ # get & check DB version?
@LibraryPath = fotosMediathekFilePath
end
@@ 115,22 117,24 @@ module MRO
@LibraryPath
end
- def album name
- raise "Not supported."
- end
-
def find_images_for_album album_name, &block
sql = <<END_OF_SQL
SELECT
version.name AS imageName
+ , master.name AS masterName
, version.latitude AS latitude
, version.longitude AS longitude
- , datetime(master.imageDate + 978307200, 'unixepoch', 'localtime') AS imageDate
+ , datetime(version.imageDate + 978307200, 'unixepoch', 'localtime') AS imageDate -- NSDate, Jan 1st 2001
, master.imagePath
, master.uuid
, master.modelId
, version.modelId
--- , keyword.name
+ , version.adjustmentUuid
+ , note_str.value AS description
+ , img_prx_res.resourceUuid AS adjusted_resourceUuid
+ , img_prx_res.fileName AS adjusted_fileName
+ , version.rotation
+ -- , keyword.name
FROM RKMaster AS master
INNER JOIN RKVersion as version
ON version.masterId = master.modelId
@@ 138,12 142,16 @@ INNER JOIN RKAlbumVersion as album4version
ON album4version.versionId = version.modelId
INNER JOIN RKAlbum as album
ON album.modelId = album4version.albumId
--- INNER JOIN RKKeywordForVersion as keyword4version
--- ON keyword4version.versionId = version.modelId
--- INNER JOIN RKKeyword as keyword
--- ON keyword.modelId = keyword4version.keywordId
-WHERE album.name = ?
-ORDER BY imageDate DESC
+LEFT OUTER JOIN RKVersion_stringNote AS note_str
+ON version.modelId = note_str.attachedToId
+AND 669 = note_str.keyPath -- where is this constant defined?
+LEFT OUTER JOIN ImageProxies.RKModelResource AS img_prx_res
+ON 'UNADJUSTEDNONRAW' != version.adjustmentUuid
+AND version.adjustmentUuid = img_prx_res.resourceTag
+WHERE album.name = ? -- version.name LIKE '%7665%'
+ORDER BY
+ imageDate DESC
+ , adjusted_resourceUuid DESC
END_OF_SQL
@DB.execute(sql, album_name) {|row| yield row}
@@ 191,19 199,33 @@ END_OF_SQL
f.generator = Atom::Generator.new(:uri => 'http://purl.mro.name/iPhoto2Atom', :name => 'iPhoto2Atom')
f.authors << Atom::Person.new(:name => 'John Doe')
f.id = @BASE_URL # "urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6"
+ prev = nil
@IPHOTO.find_images_for_album(@ALBUM_NAME) do |row|
+ # prevent duplicates:
+ next if prev == row[8]
+ prev = row[8]
# $stderr.puts "#{img[:created]} #{img['Caption']}"
# unless 2 == row[MediaType]
# $stderr.write "-(#{img['MediaType']})"
# next
# end
- src = File.join @IPHOTO.libraryPath, 'Masters', row[4]
+ src = nil
+ Dir.glob(File.join(@IPHOTO.libraryPath,'resources','modelresources','*','*',row[11],row[12])) {|fn| src = fn} unless row[11].nil?
+ src ||= File.join(@IPHOTO.libraryPath, 'Masters', row[5]) # fallback to unmodified master image
raise src unless src.start_with?( @IPHOTO.libraryPath )
dst_fmt = case File.extname(src)
- when /png/i then :png
- else :jpeg
+ when /png/i then :png
+ when /jpe?g/i then :jpeg
+ when /jp2/i then :jpeg
+ when /mov/i then :mov
+ else raise "odd extension: '#{File.extname(src)}'"
+ end
+ if :mov == dst_fmt
+ $stderr.write 'o'
+ next
end
- sha = Digest::SHA1.hexdigest(src[(@IPHOTO.libraryPath.length+1)..-1]) # (destination) file extension not part of sha.
+ # $stderr.puts "sha('#{row[5]}') "
+ sha = Digest::SHA1.hexdigest(row[5]) # (destination) file extension not part of sha.
# $stderr.puts " #{img['ThumbPath']}"
# $stderr.puts " #{img['OriginalPath']}"
begin
@@ 211,11 233,11 @@ END_OF_SQL
e.id = @BASE_URL + ('#_' + sha)
# use GUID?
# e.id = "urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a"
- create_date = Time.parse row[3]
+ create_date = Time.parse row[4]
e.updated = create_date
maxDate = create_date if create_date > maxDate
- e.title = row[0]
- e.summary = ' '
+ e.title = row[0] || row[1] # fallback to master name
+ e.summary = row[10]
# use Rating ?
dir2pxls.each do |dir,h|
dst = File.join(@ATOM_DIR, dir, "#{sha}.#{dst_fmt}") # (destination) file extension not part of sha.
@@ 223,7 245,7 @@ END_OF_SQL
# unless FileUtils.uptodate?(dst, [src]) # wrong, compare timestamp to img[:created]
if !File.exist?(dst) || File.mtime(dst) < create_date
$stderr.write '+'
- FileUtils.cp(src, dst)
+ # FileUtils.cp(src, dst)
unless h[:pxls].nil?
# @todo - keep original pngs in high-res
# https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man1/sips.1.html
@@ 231,17 253,20 @@ END_OF_SQL
properties = {
:copyright => "© #{cyear}, Marcus Rohrmoser, http://mro.name/me",
:artist => 'http://mro.name/me',
- :description => [row[0], nil].flatten.compact.collect{|s| '' == s ? nil : s}.flatten.collect{|s| s.strip}.join(' / '),
+ :description => [e.title, e.summary].collect{|s| '' == s ? nil : s}.compact.collect{|s| s.strip}.join(' / '),
:format => dst_fmt,
:formatOptions => h[:quality],
}
- cmd = "sips #{properties.map{|k,v| "--setProperty #{k} #{sh_escape(v)}"}.join(' ')} -Z #{h[:pxls]} '#{dst}' > /dev/null 2> /dev/null"
+ rotation = row[11].nil? ? row[13] : 0 # rotate only if unmodified
+ cmd = "sips --rotate #{rotation} #{properties.map{|k,v| "--setProperty #{k} #{sh_escape(v)}"}.join(' ')} -Z #{h[:pxls]} '#{src}' --out '#{dst}' > /dev/null 2> /dev/null"
# $stderr.puts cmd
unless system(cmd)
$stderr.puts " failed: #{cmd}"
FileUtils.rm dst
end
# @todo optional run pngquant / optipng
+ else
+ FileUtils.cp(src, dst)
end
FileUtils.touch dst, :mtime => create_date+1
else
@@ 251,10 276,10 @@ END_OF_SQL
end
e.content = Atom::Content::External.new :src => (@ATOM_URL + ('1600p' + '/') + "#{sha}.#{dst_fmt}").to_s, :type => "image/#{dst_fmt}"
e.media_thumbnail = Media::Thumbnail.new :url => (@ATOM_URL + ('200p' + '/') + "#{sha}.#{dst_fmt}")
- @IPHOTO.find_keywords_for_image_version_id(row[7]) do |keywor|
+ @IPHOTO.find_keywords_for_image_version_id(row[8]) do |keywor|
e.categories << Atom::Category.new( :scheme => @BASE_URL, :term => keywor, :label => keywor )
end
- e.georss_point = "#{row[1]} #{row[2]}" if row[1] or row[2]
+ e.georss_point = "#{row[2]} #{row[3]}" if row[2] or row[3]
end
rescue Exception => e
$stderr.puts row