M Gemfile => Gemfile +1 -1
@@ 1,4 1,4 @@
source "https://rubygems.org"
-gem 'CFPropertyList'
+gem 'sqlite3'
gem 'ratom'
M Gemfile.lock => Gemfile.lock +5 -2
@@ 1,14 1,17 @@
GEM
remote: https://rubygems.org/
specs:
- CFPropertyList (2.3.1)
libxml-ruby (2.8.0)
ratom (0.9.0)
libxml-ruby (~> 2.6)
+ sqlite3 (1.3.11)
PLATFORMS
ruby
DEPENDENCIES
- CFPropertyList
ratom
+ sqlite3
+
+BUNDLED WITH
+ 1.10.6
R iPhotoYaml2Atom.rb => Photos2Atom.rb +74 -42
@@ 20,19 20,19 @@
if ARGV[0].nil? || '-h' == ARGV[0] || '-?' == ARGV[0]
s = <<EOF
-Usage: #{__FILE__} <iphoto yaml> <album name> <base url> <atom file output>
+Usage: #{__FILE__} <Fotos-Mediathek.photoslibrary> <album name> <base url> <atom file output>
-<iphoto yaml> as produced by iPhoto2yaml.rb
-<album name> iPhoto album to extract as a Atom photo feed
-<base url> base url after deployment
-<atom file> local Atom file to create (cache)
+<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
+<atom file> local Atom file to create (cache)
EOF
$stderr.puts s
exit
end
-require 'yaml'
+require 'sqlite3' # http://www.rubydoc.info/gems/sqlite3/1.3.11
require 'uri'
require 'digest/sha1'
require 'atom' # https://github.com/seangeo/ratom
@@ 97,19 97,18 @@ def sh_escape str
end
module MRO
- class IPhoto
+ class Photos
@@CACHE = {}
- def self.open albumDataYaml
- @@CACHE[albumDataYaml] || (@@CACHE[albumDataYaml] = self.new(albumDataYaml))
+ def self.open fotosMediathekFilePath
+ self.new(fotosMediathekFilePath)
end
- def initialize albumDataYaml
+ def initialize fotosMediathekFilePath
t0 = Time.now
- @AlbumData = File.open(albumDataYaml, 'r') { |yaml| YAML.load(yaml) }
- $stderr.puts "yaml load dt = #{Time.now - t0} s"
- @LibraryPath = File.dirname(albumDataYaml)
+ @DB = SQLite3::Database.new(File.join(fotosMediathekFilePath, 'Database', 'Library.apdb'))
+ @LibraryPath = fotosMediathekFilePath
end
def libraryPath
@@ 117,17 116,50 @@ module MRO
end
def album name
- @AlbumData[:albums].each{|a| return a if a['AlbumName'] == name}
- nil
+ raise "Not supported."
end
- def album_images album
- # album['KeyList'].sort{|x,y| @AlbumData['Master Image List'][x]['DateAsTimerIntervalGMT'] <=> @AlbumData['Master Image List'][y]['DateAsTimerIntervalGMT'] }.reverse.collect{|key| self.image(key)}
- album['KeyList'].collect{|k| self.image(k)}.sort{|x,y| x[:created] <=> y[:created]}.reverse
+ def find_images_for_album album_name, &block
+ sql = <<END_OF_SQL
+SELECT
+ version.name AS imageName
+ , version.latitude AS latitude
+ , version.longitude AS longitude
+ , datetime(master.imageDate + 978307200, 'unixepoch', 'localtime') AS imageDate
+ , master.imagePath
+ , master.uuid
+ , master.modelId
+ , version.modelId
+-- , keyword.name
+FROM RKMaster AS master
+INNER JOIN RKVersion as version
+ON version.masterId = master.modelId
+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
+END_OF_SQL
+
+ @DB.execute(sql, album_name) {|row| yield row}
end
- def image key
- @AlbumData[:images][key]
+ def find_keywords_for_image_version_id image_version_id, &block
+ sql = <<END_OF_SQL
+SELECT DISTINCT keyword.name AS keywrd
+FROM RKKeyword as keyword
+INNER JOIN RKKeywordForVersion as keyword4version
+ON keyword4version.keywordId = keyword.modelId
+AND keyword4version.versionId = ?
+ORDER BY keyword.name ASC
+END_OF_SQL
+
+ @DB.execute(sql, image_version_id) {|row| yield row[0]}
end
end
@@ 135,7 167,6 @@ module MRO
def initialize iphoto, album_name, base_url, dst_atom_file
@IPHOTO = iphoto
@ALBUM_NAME = album_name
- @ALBUM = @IPHOTO.album album_name
@BASE_URL = base_url
@ATOM_FILE = File.expand_path dst_atom_file
@ATOM_URL = @BASE_URL + File.basename( @ATOM_FILE )
@@ 155,18 186,18 @@ module MRO
maxDate = Time.at 0
feed = Atom::Feed.new do |f|
- f.title = @ALBUM['AlbumName']
+ f.title = @ALBUM_NAME
f.links << Atom::Link.new(:href => @BASE_URL + 'photos.atom', :rel => :self)
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"
- @IPHOTO.album_images(@ALBUM).each do |img|
+ @IPHOTO.find_images_for_album(@ALBUM_NAME) do |row|
# $stderr.puts "#{img[:created]} #{img['Caption']}"
- unless 'Image' == img['MediaType']
- $stderr.write "-(#{img['MediaType']})"
- next
- end
- src = File.exist?( img['ImagePath'] ) ? img['ImagePath'] : img['OriginalPath']
+ # unless 2 == row[MediaType]
+ # $stderr.write "-(#{img['MediaType']})"
+ # next
+ # end
+ src = File.join @IPHOTO.libraryPath, 'Masters', row[4]
raise src unless src.start_with?( @IPHOTO.libraryPath )
dst_fmt = case File.extname(src)
when /png/i then :png
@@ 180,26 211,27 @@ module MRO
e.id = @BASE_URL + ('#_' + sha)
# use GUID?
# e.id = "urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a"
- e.updated = img[:created]
- maxDate = img[:created] if img[:created] > maxDate
- e.title = img['Caption']
- e.summary = img['Comment']
+ create_date = Time.parse row[3]
+ e.updated = create_date
+ maxDate = create_date if create_date > maxDate
+ e.title = row[0]
+ e.summary = ' '
# use Rating ?
dir2pxls.each do |dir,h|
dst = File.join(@ATOM_DIR, dir, "#{sha}.#{dst_fmt}") # (destination) file extension not part of sha.
- raise "Wrong type '#{img[:created].class}'" unless img[:created].kind_of? Time
+ raise "Wrong type '#{create_date.class}'" unless create_date.kind_of? Time
# unless FileUtils.uptodate?(dst, [src]) # wrong, compare timestamp to img[:created]
- if !File.exist?(dst) || File.mtime(dst) < img[:created]
+ if !File.exist?(dst) || File.mtime(dst) < create_date
$stderr.write '+'
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
- cyear = [img[:created].year, Time.now.year].uniq.join('-')
+ cyear = [create_date.year, Time.now.year].uniq.join('-')
properties = {
:copyright => "© #{cyear}, Marcus Rohrmoser, http://mro.name/me",
:artist => 'http://mro.name/me',
- :description => [img['Caption'], img['Comment']].collect{|s| '' == s ? nil : s}.flatten.collect{|s| s.strip}.join(' / '),
+ :description => [row[0], nil].flatten.compact.collect{|s| '' == s ? nil : s}.flatten.collect{|s| s.strip}.join(' / '),
:format => dst_fmt,
:formatOptions => h[:quality],
}
@@ 211,7 243,7 @@ module MRO
end
# @todo optional run pngquant / optipng
end
- FileUtils.touch dst, :mtime => img[:created]+1
+ FileUtils.touch dst, :mtime => create_date+1
else
$stderr.write '.'
end
@@ 219,13 251,13 @@ module MRO
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}")
- (img[:keywords] || []).each do |keyword|
- e.categories << Atom::Category.new( :scheme => @BASE_URL, :term => keyword, :label => keyword )
+ @IPHOTO.find_keywords_for_image_version_id(row[7]) do |keywor|
+ e.categories << Atom::Category.new( :scheme => @BASE_URL, :term => keywor, :label => keywor )
end
- e.georss_point = "#{img['latitude']} #{img['longitude']}" if img['latitude'] or img['longitude']
+ e.georss_point = "#{row[1]} #{row[2]}" if row[1] or row[2]
end
rescue Exception => e
- $stderr.puts img
+ $stderr.puts row
$stderr.puts e
end
end
@@ 241,7 273,7 @@ module MRO
end
end
-ip = MRO::IPhoto.open ARGV[0]
+ip = MRO::Photos.open ARGV[0]
ARGV.shift
until ARGV[2].nil?
D iPhoto2yaml.rb => iPhoto2yaml.rb +0 -113
@@ 1,113 0,0 @@
-#!/usr/bin/env ruby
-#
-# iPhoto2Atom, extract images from iPhoto™ libraries
-# Copyright (C) 2015 Marcus Rohrmoser, http://purl.mro.name/iPhoto2Atom
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-if ARGV[0].nil? || '-h' == ARGV[0] || '-?' == ARGV[0]
- s = <<EOF
-Read iPhoto AlbumData.xml (plist), gently convert to AlbumData.yaml.
-
-Usage: #{__FILE__} <.../iPhoto-Mediathek.photolibrary>
-EOF
- $stderr.puts s
- exit
-end
-
-require 'cfpropertylist'
-require 'yaml'
-
-IPhotoLibPath = ARGV[0]
-AlbumDataPlist = File.join( IPhotoLibPath, 'AlbumData.xml' )
-AlbumDataYaml = File.join( IPhotoLibPath, 'AlbumData.yaml' )
-KeywordsYaml = File.join( IPhotoLibPath, 'Keywords.yaml' )
-
-unless FileUtils.uptodate?(AlbumDataYaml, [AlbumDataPlist])
- $stderr.puts "load #{AlbumDataPlist}"
- plist = CFPropertyList::List.new(:file => AlbumDataPlist)
- data = CFPropertyList.native_types(plist.value)
-
- data[:faces] = {}
- data['List of Faces'].each do |key,face|
- face['key'] = face['key'].to_i
- face['PhotoCount'] = face['PhotoCount'].to_i
- face[:image_keys] = []
- data[:faces][ face['key'] ] = face
- end
- data.delete 'List of Faces'
-
- T0 = Time.parse('2001-01-01 00:00:00Z')
- raise "ouch" if T0.nil?
-
- # List of Rolls, round I
- data[:rolls] = {}
- data['List of Rolls'].each do |roll|
- roll[:roll_date] = (T0 + roll['RollDateAsTimerInterval']).localtime #.strftime('%FT%T%z').gsub(/:(\d\d)$/, "\\1")
- roll.delete 'RollDateAsTimerInterval'
- roll['KeyList'].collect!{|key| key.to_i}
- roll['KeyPhotoKey'] = roll['KeyPhotoKey'].to_i
- data[:rolls][ roll['RollID'] ] = roll
- end
- data.delete 'List of Rolls'
-
- data[:images] = {}
- data['Master Image List'].each do |key,img|
- img[:created] = (T0 + img['DateAsTimerInterval']).localtime #.strftime('%FT%T%z').gsub(/:(\d\d)$/, "\\1")
- img.delete 'DateAsTimerInterval'
- img.delete 'DateAsTimerIntervalGMT'
- img[:modified] = (T0 + img['ModDateAsTimerInterval']).localtime #.strftime('%FT%T%z').gsub(/:(\d\d)$/, "\\1")
- img.delete 'ModDateAsTimerInterval'
- img.delete 'ModDateAsTimerIntervalGMT'
- img[:meta] = (T0 + img['MetaModDateAsTimerInterval']).localtime #.strftime('%FT%T%z').gsub(/:(\d\d)$/, "\\1")
- img.delete 'MetaModDateAsTimerInterval'
- img['Faces'].each do |f|
- f['face key'] = f['face key'].to_i
- data[:faces][ f['face key'] ][:image_keys] <<= key.to_i
- # f[:face][:image_keys] <<= key.to_i
- end unless img['Faces'].nil?
- data[:images][key.to_i] = img
- end
- data.delete 'Master Image List'
-
- data[:albums] = data['List of Albums'].collect do |album|
- album['KeyPhotoKey'] = album['KeyPhotoKey'].to_i
- album['KeyList'].collect!{|key| key.to_i}
- unless album['ProjectEarliestDateAsTimerInterval'].nil?
- album[:project_earliest] = (T0 + album['ProjectEarliestDateAsTimerInterval']).localtime #.strftime('%FT%T%z').gsub(/:(\d\d)$/, "\\1")
- album.delete 'ProjectEarliestDateAsTimerInterval'
- end
- album
- end
- data.delete 'List of Albums'
-
- # optional: Keywords.yaml
- if File.exists? KeywordsYaml
- keywords = File.open(KeywordsYaml, 'r') {|f| YAML.load f}
- keywords.each do |image_key, keywords|
- if data[:images][image_key].nil?
- $stderr.puts "how odd, no image #{image_key} to add keywords to."
- else
- data[:images][image_key][:keywords] = keywords
- end
- end
- end
-
- File.open(AlbumDataYaml, 'w') { |yaml| yaml.puts(YAML.dump(data)) }
- $stderr.puts "saved #{AlbumDataYaml}"
-else
- $stderr.puts "found #{AlbumDataYaml}"
- # data = File.open( AlbumDataYaml, 'r') { |yaml| YAML.load(yaml) }
- # puts YAML.dump(data)
-end
D keywords.sql => keywords.sql +0 -13
@@ 1,13 0,0 @@
-SELECT DISTINCT
- RKVersion.modelId as image_key
- , RKKeyword.name as keyword_name
-FROM
- RKKeywordForVersion
-INNER JOIN RKVersion
-ON RKKeywordForVersion.versionId = RKVersion.modelId
-INNER JOIN RKKeyword
-ON RKKeyword.modelId = RKKeywordForVersion.keywordId
--- WHERE RKKeywordForVersion.keywordId = 62
-ORDER BY
- image_key ASC
- , keyword_name ASC;
M run.sh => run.sh +3 -26
@@ 27,31 27,7 @@ cd "$(dirname "$0")"
script_dir="$(pwd)"
# cd "$cwd"
-photolibrary="$HOME/Pictures/iPhoto-Mediathek.photolibrary"
-
-# extract keywords if sqlite3 is present.
-if $(sqlite3 --version >/dev/null) ; then
- dst="$photolibrary/Keywords.yaml"
- echo "---" > "$dst~"
- sqlite3 -separator ' ' "$photolibrary/Database/Library.apdb" < "$script_dir/keywords.sql" \
- | while read image_key keyword_name
- do
- if [ "$recent_image_key" != "$image_key" ] ; then
- echo "$image_key:" >> "$dst~"
- fi
- recent_image_key="$image_key"
- echo " - \"$keyword_name\"" >> "$dst~"
- done
- mv "$dst~" "$dst"
-else
- echo "Optional: Install sqlite3 for keywords." 1>&2
-fi
-
-# prepare the iPhoto library yaml (3x faster loading)
-bundle exec ruby "$script_dir/iPhoto2yaml.rb" "$photolibrary"
-if [ $? -ne 0 ] ; then
- exit 1
-fi
+photolibrary="$HOME/Pictures/Fotos-Mediathek.photoslibrary"
#############################################################################
## below here is to
@@ 100,6 76,7 @@ EOF
# add xslt file
rsync -aP --exclude .DS_Store --delete --delete-excluded "$script_dir/assets" "$dir/"
# run xslt => index.html
+ # rm "$dir/index.html"
# xsltproc --output "$dir/index.html" "$dir/assets/atom2html.xslt" "$file"
fi
}
@@ 112,7 89,7 @@ do
cache="$HOME/Library/Caches/name.mro.iPhoto2Atom/$subdir"
mkdir -p "$cache" \
- && bundle exec ruby "$script_dir/iPhotoYaml2Atom.rb" "$photolibrary/AlbumData.yaml" "$album" "${dst_base_url_parent}$subdir/" "$cache/photos.atom" \
+ && bundle exec ruby "$script_dir/Photos2Atom.rb" "$photolibrary" "$album" "${dst_base_url_parent}$subdir/" "$cache/photos.atom" \
&& process_xslt "$cache/photos.atom" \
&& rsync -avPz --exclude .DS_Store --delete --delete-excluded "$cache" "${dst_dir_parent}$(dirname "$subdir")"
done
A sample.atom => sample.atom +60 -0
@@ 0,0 1,60 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<?xml-stylesheet type="text/xsl" href="assets/atom2html.xslt"?>
+<!--
+ https://developer.mozilla.org/en/docs/XSL_Transformations_in_Mozilla_FAQ#Why_isn.27t_my_stylesheet_applied.3F
+
+ Note that Firefox will override your XSLT stylesheet if your XML is
+ detected as an RSS or Atom feed. A known workaround is to add a
+ sufficiently long XML comment to the beginning of your XML file in
+ order to "push" the <.feed> or <.rss> tag out of the first 512 bytes,
+ which is analyzed by Firefox to determine if it's a feed or not. See
+ the discussion on bug
+ https://bugzilla.mozilla.org/show_bug.cgi?id=338621#c72 for more
+ information.
+
+
+ For best results serve both atom feed and xslt as 'text/xml' or
+ 'application/xml' without charset specified.
+-->
+<feed xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" xmlns:georss="http://www.georss.org/georss">
+ <id>https://drop.mro.name/galleries/demo/</id>
+ <generator uri="http://purl.mro.name/iPhoto2Atom">iPhoto2Atom</generator>
+ <title>Demo Album</title>
+ <updated>2015-04-16T17:57:46+02:00</updated>
+ <link rel="self" href="https://drop.mro.name/galleries/demo/photos.atom"/>
+ <author>
+ <name>John Doe</name>
+ </author>
+ <entry>
+ <title>Osterglocken</title>
+ <id>https://drop.mro.name/galleries/demo/#_022e7cb3e8b9d44155e7ce17866f629cd1418708</id>
+ <summary> </summary>
+ <updated>2015-04-16T17:57:46+02:00</updated>
+ <link rel="enclosure" length="243347" href="https://drop.mro.name/galleries/demo/1600p/022e7cb3e8b9d44155e7ce17866f629cd1418708.jpeg"/>
+ <link rel="previewimage" length="11621" href="https://drop.mro.name/galleries/demo/200p/022e7cb3e8b9d44155e7ce17866f629cd1418708.jpeg"/>
+ <content type="image/jpeg" src="https://drop.mro.name/galleries/demo/1600p/022e7cb3e8b9d44155e7ce17866f629cd1418708.jpeg"/>
+ <media:thumbnail url="https://drop.mro.name/galleries/demo/200p/022e7cb3e8b9d44155e7ce17866f629cd1418708.jpeg"/>
+ </entry>
+ <entry>
+ <title>IMG_7639</title>
+ <id>https://drop.mro.name/galleries/demo/#_e141de41c8e8e426b1bf3e72c18999987b04e22a</id>
+ <summary> </summary>
+ <updated>2015-03-24T17:20:44+01:00</updated>
+ <link rel="enclosure" length="191218" href="https://drop.mro.name/galleries/demo/1600p/e141de41c8e8e426b1bf3e72c18999987b04e22a.png"/>
+ <link rel="previewimage" length="13416" href="https://drop.mro.name/galleries/demo/200p/e141de41c8e8e426b1bf3e72c18999987b04e22a.png"/>
+ <category label="Screen" scheme="https://drop.mro.name/galleries/demo/" term="Screen"/>
+ <content type="image/png" src="https://drop.mro.name/galleries/demo/1600p/e141de41c8e8e426b1bf3e72c18999987b04e22a.png"/>
+ <media:thumbnail url="https://drop.mro.name/galleries/demo/200p/e141de41c8e8e426b1bf3e72c18999987b04e22a.png"/>
+ </entry>
+ <entry>
+ <title>IMG_6735</title>
+ <id>https://drop.mro.name/galleries/demo/#_fcc1302401173e66ad6ab86d42deee2486f1ad9f</id>
+ <summary> </summary>
+ <updated>2014-11-14T17:09:13+01:00</updated>
+ <link rel="enclosure" length="215330" href="https://drop.mro.name/galleries/demo/1600p/fcc1302401173e66ad6ab86d42deee2486f1ad9f.jpeg"/>
+ <link rel="previewimage" length="8682" href="https://drop.mro.name/galleries/demo/200p/fcc1302401173e66ad6ab86d42deee2486f1ad9f.jpeg"/>
+ <content type="image/jpeg" src="https://drop.mro.name/galleries/demo/1600p/fcc1302401173e66ad6ab86d42deee2486f1ad9f.jpeg"/>
+ <media:thumbnail url="https://drop.mro.name/galleries/demo/200p/fcc1302401173e66ad6ab86d42deee2486f1ad9f.jpeg"/>
+ <georss:point>47.86138833 12.64393667</georss:point>
+ </entry>
+</feed>