Apple's
iPhoto is a photo browser/cataloguer [
1]. It's not as widespread as iTunes, since it's only for the Mac and is part of their "iLife" suite.
Wanting to get the XML-based catalog data into a
Metakit datafile, I came up with a Tcl script to do the work.
First a usage example:
$ ls -l AlbumData.xml
-rw-r--r-- 1 jcw jcw 15241634 Jun 20 00:20 AlbumData.xml
$ time iphoto2mk.tcl iphoto.db AlbumData.xml
iphoto.db (4786706 bytes):
33 albums
240 rolls
16 keywords
16776 images
real 0m33.225s
user 0m32.412s
sys 0m0.424s
So while it's not very fast, it does get the job done and produced a useful dataset to play with.
The code is self-contained and uses a variation of
TAX: A Tiny API for XML to do the parsing. Which is why the process takes a fair amount of memory and time to complete.
#!/usr/bin/env tclkit85
# Copyright (c) 2007 Jean-Claude Wippler
# http://www.opensource.org/licenses/mit-license.php
# Convert an iPhoto XML catalog to a Metakit datafile
#
# Usage: tclkit85 iphoto.tcl outfile ?infile?
#
# Will use iPhoto's default catalog if no input file is specified.
# Requires Tcl 8.5 due to the use of dict and lassign.
#
# jcw, 2007-06-20
package require Tcl 8.5
package require Mk4tcl
# TAX is a tiny SAX-like parser for XML, not perfect but good enough here
# Adapted from http://wiki.tcl.tk/14534 by Eric Kemp-Benedict
proc tax {cmd xml {top docstart}} {
regsub -all {<(/?)([^\s/>]+)\s*([^/>]*)(/?)>} \
[string map {\{ &ob; \} &cb;} $xml] "\};tax@ {\\2} {\\1} {\\4} {\\3} \{" xml
eval "tax@ {$top} {} {} {} {$xml}; tax@ {$top} / {} {} {}"
}
proc tax@ {e a b p t} {
set m {&ob; \{ &cb; \} < < > > " \\\" & & ' ' = " "}
if {$b ne ""} { set u $t; set t "" }
uplevel {$cmd} [list $e$a [string map $m $p] $t]
if {$b ne ""} { uplevel {$cmd} [list $e/ $b $u] }
}
namespace eval props {
namespace eval v { set out ""; set keyed 0 }
proc key {p t} {
if {!$v::keyed} { error "keyed? $t" }
set v::key $t
}
proc string {p t} { tag $t }
proc integer {p t} { tag $t }
proc real {p t} { tag $t }
proc true {p t} { tag 1 }
proc false {p t} { tag 0 }
proc dict {p t} { enter 1 }
proc dict/ {p t} { leave }
proc array {p t} { enter 0 }
proc array/ {p t} { leave }
namespace export *
proc tag {t} {
if {$v::keyed} {
append v::out " " [list $v::key $t]
} else {
append v::out " " [list $t]
}
}
proc enter {k} {
if {$v::keyed} {
append v::out " " [list $v::key] " \{"
} else {
append v::out " \{"
}
lappend v::stack $v::keyed
set v::keyed $k
}
proc leave {} {
append v::out " \}"
set v::keyed [lindex $v::stack end]
set v::stack [lrange $v::stack 0 end-1]
}
proc ? {args} {}
namespace ensemble create \
-unknown {apply {{args} { return "::props::? $args" }}}
}
proc get {vname {default ""}} {
upvar $vname v
if {![info exists v] || $v eq ""} { return $default }
return $v
}
proc load2mk {data} {
mk::view layout db.albums {
id:I
name
keys
master:I
count:I
play
repeat
rate:I
titles:I
playlist
tdir:I
tname
tspeed:F
}
mk::view layout db.rolls {
id:I
name
parent:I
keys
type
count:I
}
mk::view layout db.keywords {
id:I
name
}
mk::view layout db.images {
id:I
media
caption
comment
aspect:F
rating:I
roll:I
date:D
mdate:D
mmdate:D
ipath
opath
tpath
}
foreach {id info} [dict get $data {List of Albums}] {
dict with info {
mk::row append db.albums \
id ${AlbumId} \
name ${AlbumName} \
keys ${KeyList} \
master:I [get Master 0] \
count:I ${PhotoCount} \
play ${PlayMusic} \
repeat ${RepeatSlideShow} \
rate ${SecondsPerSlide} \
titles ${SlideShowUseTitles} \
playlist [get PlaylistName] \
tdir:I ${TransitionDirection} \
tname ${TransitionName} \
tspeed ${TransitionSpeed}
}
}
foreach {id info} [dict get $data {List of Rolls}] {
dict with info {
mk::row append db.rolls \
id ${AlbumId} \
name ${AlbumName} \
parent ${Parent} \
keys ${KeyList} \
type ${Album Type} \
count ${PhotoCount}
}
}
foreach {id info} [dict get $data {List of Keywords}] {
mk::row append db.keywords id $id name $info
}
foreach {id info} [dict get $data {Master Image List}] {
dict with info {
mk::row append db.images id $id \
media ${MediaType} \
caption ${Caption} \
comment ${Comment} \
aspect ${Aspect Ratio} \
rating ${Rating} \
roll ${Roll} \
date ${DateAsTimerInterval} \
mdate ${ModDateAsTimerInterval} \
mmdate ${MetaModDateAsTimerInterval} \
ipath ${ImagePath} \
opath [get OriginalPath] \
tpath ${ThumbPath}
}
}
}
lassign $argv outfile infile
if {$outfile eq ""} {
puts stderr "Usage: $argv0 outfile ?infile"
exit 1
}
set fd [open [get infile "~/Pictures/iPhoto Library/AlbumData.xml"]]
set data [read $fd]
close $fd
tax ::props $data
set data [lindex $::props::v::out 0]
#puts [dict keys $data]
file delete $outfile
mk::file open db $outfile
load2mk $data
mk::file commit db
puts "$outfile ([file size $outfile] bytes):"
foreach x {albums rolls keywords images} {
puts " [mk::view size db.$x] $x"
}
Dates are not converted - they are stored as the same doubles present in the input file for now.