@@ 18,80 18,206 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-if ARGV[0].nil? || '-h' == ARGV[0] || '-?' == ARGV[0]
+if ! ARGV[4].nil? || ARGV[1].nil? || '-h' == ARGV[0] || '-?' == ARGV[0]
s = <<EOF
-Usage: #{__FILE__} <Fotos-Mediathek.photoslibrary> <album name> <base url> <atom file output>
+NAME
+ #{__FILE__} -- turn an Apple™ Photos™ Album into an Atom image feed.
-<Fotos-Mediathek.photoslibrary> complete filesystem path, usually inside $HOME/Pictures/
-<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)
+SYNOPSIS
+ #{__FILE__} album atom_url [atom2html.xslt] [Fotos-Mediathek.photoslibrary]
+
+DESCRIPTION
+ album Fotos album name to extract as a Atom photo feed
+ atom_url final Atom file url (after deployment)
+ atom2html.xslt xslt transformation URL to use in <?xml-stylesheet type="text/xsl" href="assets/atom2html.xslt"?> or '-'
+ Fotos-Mediathek.photoslibrary complete filesystem path, usually inside $HOME/Pictures/
+
+EXAMPLES
+ #{__FILE__} 'Public Images' https://example.com/images/demo/photos.atom
+ simple usage, include default assets/atom2html.xslt
+
+ #{__FILE__} 'Public Images' https://example.com/images/demo/photos.atom '-'
+ simple usage, no atom2html.xslt
+
+ #{__FILE__} 'Public Images' https://example.com/images/demo/photos.atom foo/atom2html.xslt $HOME/Pictures/*-Mediathek.photoslibrary
+ explicit atom2html.xslt, explicit Photos™ media location
EOF
$stderr.puts s
exit
end
-require 'sqlite3' # http://www.rubydoc.info/gems/sqlite3/1.3.11
-require 'uri'
-require 'digest/sha1'
-require 'atom' # https://github.com/seangeo/ratom
-
-# adapted from https://github.com/jondot/ratom/commit/68c5e9f0c25bceaf484e497f7afb3c7f3ba3f7c3
-module Media
- class Thumbnail
- include Atom::Xml::Parseable
- include Atom::SimpleExtensions
- # attribute :url, :fileSize, :type, :medium, :isDefault, :expression, :bitrate, :height, :width, :duration, :lang
- attribute :url
-
- def initialize(o = {})
- case o
- when XML::Reader
- parse(o, :once => true)
- when Hash
- o.each do |k, v|
- self.send("#{k.to_s}=", v)
- end
- else
- raise ArgumentError, "Got #{o.class} but expected a Hash or XML::Reader"
- end
+########################################################################
+## First comes a very simple Atom generator (API inspired by https://github.com/seangeo/ratom
+########################################################################
- yield(self) if block_given?
- end
+class Object
+ def to_xml
+ self.to_s.gsub(/[<>&"']/, {'<'=>'<', '>'=>'>', '&'=>'&', '"'=>'"', "'"=>'''})
end
- Atom::Feed.add_extension_namespace :media, "http://search.yahoo.com/mrss/"
- Atom::Entry.element "media:thumbnail", :class => Thumbnail
end
-# inspired by https://github.com/jondot/ratom/commit/68c5e9f0c25bceaf484e497f7afb3c7f3ba3f7c3
-module GeoRss
- class Point
- include Atom::Xml::Parseable
- include Atom::SimpleExtensions
+class Time
+ def to_xml
+ self.strftime('%FT%T%z').gsub(/(\d{2})$/, ":\\1")
end
- Atom::Feed.add_extension_namespace :georss, 'http://www.georss.org/georss'
- Atom::Entry.element "georss:point", :class => Point
end
-# monkey path to support <content src="..."/>
-class Atom::Content::External
- def initialize(o = {})
- case o
- when XML::Reader
- parse(o, :once => true)
- when Hash
- o.each do |k, v|
- self.send("#{k.to_s}=", v)
+module MRO
+ module Atom
+ class Basic
+ def initialize
+ @xml = ''
+ end
+ def id= v
+ @xml <<= "<id>#{v.to_xml}</id>"
+ end
+ def title= v
+ @xml <<= "<title>#{v.to_xml}</title>"
+ end
+ def updated= v
+ @xml <<= "<updated>#{v.to_xml}</updated>"
+ end
+ end
+
+ class Feed < Basic
+ attr_writer :xslt
+ attr_reader :links, :authors, :entries
+
+ def initialize
+ super
+ @links = []
+ @authors = []
+ @entries = []
+ yield self
+ self
+ end
+
+ def generator= v
+ @xml <<= v
+ end
+
+ def to_xml
+ s = ''
+ if @xslt
+ s <<= "<?xml-stylesheet type='text/xsl' href='#{@xslt.to_xml}'?>"
+ s <<= <<END_OF_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.
+-->
+END_OF_XSLT
+ end
+ s <<= '<feed xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" xmlns:georss="http://www.georss.org/georss">'
+ s <<= @xml
+ s <<= self.links.join('')
+ s <<= "<author>"
+ s <<= self.authors.join('')
+ s <<= "</author>"
+ s <<= self.entries.collect{|c| c.to_xml}.join('')
+ s <<= '</feed>'
+ s
+ end
+ end
+
+ class Entry < Basic
+ attr_reader :title, :summary, :links, :categories
+
+ def initialize
+ super
+ @links = []
+ @categories = []
+ yield self
+ self
+ end
+
+ def title= v
+ super(@title = v)
+ end
+ def content= v
+ @xml <<= v
+ end
+ def summary= v
+ @summary = v
+ @xml <<= "<summary>#{v.to_xml}</summary>" unless v.nil?
+ end
+ def georss_point= v
+ @xml <<= "<georss:point>#{v.to_xml}</georss:point>"
+ end
+ def media_thumbnail= v
+ @xml <<= v
+ end
+
+ def to_xml
+ s = '<entry>'
+ s <<= @xml
+ s <<= self.links.join('')
+ s <<= self.categories.join('')
+ s <<= '</entry>'
+ s
+ end
+ end
+
+ class Link < String
+ def initialize h
+ super "<link #{h.map{|k,v| "#{k.to_xml}='#{v.to_xml}'"}.join(' ')}/>"
+ end
+ end
+ class Category < String
+ def initialize h
+ super "<category #{h.map{|k,v| "#{k.to_xml}='#{v.to_xml}'"}.join(' ')}/>"
end
- else
- raise ArgumentError, "Got #{o.class} but expected a Hash or XML::Reader"
end
+ module Content
+ class External < String
+ def initialize h
+ super "<content #{h.map{|k,v| "#{k.to_xml}='#{v.to_xml}'"}.join(' ')}/>"
+ end
+ end
+ end
+ class Person < String
+ def initialize h
+ name = h[:name]
+ h.delete :name
+ super "<name #{h.map{|k,v| "#{k.to_xml}='#{v.to_xml}'"}.join(' ')}>#{name.to_xml}</name>"
+ end
+ end
+ class Generator < String
+ def initialize h
+ name = h[:name]
+ h.delete :name
+ super "<generator #{h.map{|k,v| "#{k.to_xml}='#{v.to_xml}'"}.join(' ')}>#{name.to_xml}</generator>"
+ end
+ end
+ end
- yield(self) if block_given?
+ module Media
+ class Thumbnail < String
+ def initialize h
+ super "<media:thumbnail #{h.map{|k,v| "#{k.to_xml}='#{v.to_xml}'"}.join(' ')}/>"
+ end
+ end
end
end
+########################################################################
+## Then access Apple™ Photos™
+########################################################################
+
+require 'sqlite3' # http://www.rubydoc.info/gems/sqlite3/1.3.11
+
def sh_escape str
'"' + "#{str}".gsub(/[\\\n"]/){|s| {'\\'=>'\\\\', "\n"=>'\\n', '"'=>'\\"'}[s]} + '"'
end
@@ 99,13 225,16 @@ end
module MRO
class Photos
- @@CACHE = {}
-
def self.open fotosMediathekFilePath
self.new(fotosMediathekFilePath)
end
def initialize fotosMediathekFilePath
+ Dir.glob( File.join(ENV['HOME'], 'Pictures', '*.photoslibrary') ) do |d|
+ fotosMediathekFilePath = d
+ break
+ end if fotosMediathekFilePath.nil?
+ $stderr.puts "Photos Library: #{fotosMediathekFilePath}"
t0 = Time.now
@DB = SQLite3::Database.new(File.join(fotosMediathekFilePath, 'Database', 'Library.apdb'), :readonly => true)
@DB.execute("ATTACH DATABASE ? AS ImageProxies", File.join(fotosMediathekFilePath, 'Database', 'ImageProxies.apdb'))
@@ 117,6 246,13 @@ module MRO
@LibraryPath
end
+ def cache_atom_file atom_url
+ host = atom_url.host
+ path = atom_url.path
+ path = File.join(path, 'photos.atom') if path.end_with?('/')
+ File.join(ENV['HOME'], 'Library', 'Caches', 'name.mro.iPhoto2Atom', host, path.split('/'))
+ end
+
def find_images_for_album album_name, &block
sql = <<END_OF_SQL
SELECT
@@ 170,18 306,32 @@ END_OF_SQL
@DB.execute(sql, image_version_id) {|row| yield row[0]}
end
end
+end
+########################################################################
+## Then bring them together…
+########################################################################
+
+require 'uri'
+require 'fileutils'
+require 'digest/sha1'
+
+module MRO
class CFG
- def initialize iphoto, album_name, base_url, dst_atom_file
+ def initialize iphoto, album_name, dst_atom_url
@IPHOTO = iphoto
@ALBUM_NAME = album_name
- @BASE_URL = base_url
- @ATOM_FILE = File.expand_path dst_atom_file
- @ATOM_URL = @BASE_URL + File.basename( @ATOM_FILE )
- @ATOM_DIR = File.dirname @ATOM_FILE
+ @ATOM_URL = dst_atom_url
+ # do sanity checks in case
+
+ @BASE_URL = @ATOM_URL + '.'
+ @ATOM_FILE = iphoto.cache_atom_file(@ATOM_URL)
+ @ATOM_DIR = File.dirname @ATOM_FILE
+ $stderr.puts "Atom file : #{@ATOM_FILE}"
+ $stderr.puts "Atom URL : #{@ATOM_URL}"
end
- def produce_atom_feed
+ def produce_atom_feed xslt_url
# $stderr.puts "convert album '#{@ALBUM['AlbumName']}' to #{@ATOM_URL}"
dir2pxls = {
@@ 194,8 344,9 @@ END_OF_SQL
maxDate = Time.at 0
feed = Atom::Feed.new do |f|
+ f.xslt = xslt_url
f.title = @ALBUM_NAME
- f.links << Atom::Link.new(:href => @BASE_URL + 'photos.atom', :rel => :self)
+ f.links << Atom::Link.new(:href => @ATOM_URL, :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"
@@ 295,17 446,13 @@ END_OF_SQL
File.open(@ATOM_FILE, 'w') {|dst| dst.puts feed.to_xml}
FileUtils.touch @ATOM_FILE, :mtime => maxDate+1
-
- $stdout.puts "#{@ATOM_URL} #{@ATOM_FILE}"
end
end
end
-ip = MRO::Photos.open ARGV[0]
-ARGV.shift
+photos = MRO::Photos.open ARGV[3]
+xslt_url = ('-' == ARGV[2]) ? nil : URI::parse( ARGV[2].nil? ? 'assets/atom2html.xslt' : ARGV[2] )
-until ARGV[2].nil?
- base_url = URI::parse ARGV[1]
- MRO::CFG.new(ip, ARGV[0], base_url, ARGV[2]).produce_atom_feed
- ARGV.shift 3
-end
+atom_url = URI::parse ARGV[1]
+album = ARGV[0]
+MRO::CFG.new(photos, album, atom_url).produce_atom_feed(xslt_url)