iphoto_backup 0.0.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +3 -0
- data/.travis.yml +3 -0
- data/README.md +38 -10
- data/lib/iphoto_backup/cli.rb +25 -4
- data/lib/iphoto_backup/version.rb +1 -1
- data/spec/fixtures/AlbumData.xml.erb +163 -0
- data/spec/fixtures/image.jpg +0 -0
- data/spec/fixtures/image2.jpg +0 -0
- data/spec/lib/iphoto_backup/cli_spec.rb +66 -3
- metadata +9 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b9dca72c4a6a1dde1fb22151dc7d109220848923
|
4
|
+
data.tar.gz: 6a4d33e9ef779e0ed4c6846ed1cb314f7e1ae7bf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1967d543c0966f836b054a704a152f4b05e9229c3fa086ccd6373705e86c03096aa79c89af93c3b55fa573c17d553b6d58320689a710321380cd1b119ef8afc3
|
7
|
+
data.tar.gz: 3a8925d43561b689098c2cb026f8c873ae52a7dfa583c127a07a8452a3d12c88444603e9884e85f4197d0f90c4d99672c6c044f2e0f2756c1d17f2a97d1c8d02
|
data/.gitignore
CHANGED
data/.travis.yml
ADDED
data/README.md
CHANGED
@@ -1,26 +1,54 @@
|
|
1
|
-
|
1
|
+
[](https://travis-ci.org/wireframe/iphoto_backup)
|
2
2
|
|
3
|
-
|
3
|
+
# iphoto_backup
|
4
|
+
|
5
|
+
> Every photo deserves to live in a folder on the filesystem and not
|
6
|
+
> to be locked up in some cryptic and proprietary iPhoto metadata XML file.
|
7
|
+
|
8
|
+
iphoto_backup is a tool to simplify backups and archiving of your iPhoto images.
|
4
9
|
|
5
10
|
[Originally implemented as a Python script](https://github.com/wireframe/dotfiles/blob/628b982d9fc4e7b4cc9e6ca806cae81b541f9bbd/home/bin/iphoto_export.py)
|
6
11
|
|
7
12
|
## Installation
|
8
13
|
|
9
|
-
|
14
|
+
```bash
|
15
|
+
$ gem install iphoto_backup
|
16
|
+
```
|
17
|
+
|
18
|
+
## Usage
|
10
19
|
|
11
|
-
|
20
|
+
```bash
|
21
|
+
$ iphoto_backup
|
12
22
|
|
13
|
-
|
23
|
+
Processing Roll: Wedding Pics...
|
24
|
+
copying /iphoto/file.png to /my/custom/backup.png
|
25
|
+
```
|
14
26
|
|
15
|
-
|
27
|
+
## Options
|
16
28
|
|
17
|
-
|
29
|
+
#### --filter [REGEX]
|
18
30
|
|
19
|
-
|
31
|
+
*aliased to -e*
|
20
32
|
|
21
|
-
|
33
|
+
Restrict exporting to only albums that match the given regular expression. Albums that do not match the regex will be printed in the log output as well.
|
34
|
+
|
35
|
+
example:
|
36
|
+
```bash
|
37
|
+
$ iphoto_backup -e Summer
|
38
|
+
|
39
|
+
|
40
|
+
Processing Roll: Summer Pics...
|
41
|
+
copying /iphoto/file.png to /my/custom/backup.png
|
42
|
+
|
43
|
+
Winter Pics does not match the filter: /Summer/
|
44
|
+
```
|
45
|
+
|
46
|
+
#### --output [/path/to/directory]
|
47
|
+
|
48
|
+
*aliased to -o*
|
49
|
+
*default to ~/Google Drive/Dropbox*
|
22
50
|
|
23
|
-
|
51
|
+
Customize the path for archiving photos.
|
24
52
|
|
25
53
|
## Contributing
|
26
54
|
|
data/lib/iphoto_backup/cli.rb
CHANGED
@@ -6,17 +6,18 @@ module IphotoBackup
|
|
6
6
|
class CLI < Thor
|
7
7
|
IPHOTO_ALBUM_PATH = "~/Pictures/iPhoto Library.photolibrary/AlbumData.xml"
|
8
8
|
DEFAULT_OUTPUT_DIRECTORY = "~/Google Drive/Dropbox"
|
9
|
+
IPHOTO_EPOCH = Time.new(2001, 1, 1)
|
9
10
|
|
10
11
|
desc "export iPhoto albums", "exports iPhoto albums into target directory"
|
11
12
|
option :filter, aliases: '-e', default: '.*'
|
12
13
|
option :output, aliases: '-o', default: DEFAULT_OUTPUT_DIRECTORY
|
14
|
+
option :config, aliases: '-c', default: IPHOTO_ALBUM_PATH
|
15
|
+
option :'date-prefix', aliases: '-d', default: false
|
13
16
|
def export
|
14
17
|
each_album do |folder_name, album_info|
|
15
18
|
say "\n\nProcessing Roll: #{folder_name}..."
|
16
19
|
|
17
|
-
|
18
|
-
album_images.each do |image_id|
|
19
|
-
image_info = info_for_image image_id
|
20
|
+
each_image(album_info) do |image_info|
|
20
21
|
source_path = value_for_dictionary_key('ImagePath', image_info).content
|
21
22
|
|
22
23
|
target_path = File.join(File.expand_path(options[:output]), folder_name, File.basename(source_path))
|
@@ -41,6 +42,17 @@ module IphotoBackup
|
|
41
42
|
albums.each do |album_info|
|
42
43
|
folder_name = value_for_dictionary_key('RollName', album_info).content
|
43
44
|
|
45
|
+
if options[:'include-date-prefix'] && folder_name !~ /^\d{4}-\d{2}-\d{2} /
|
46
|
+
album_date = nil
|
47
|
+
each_image album_info do |image_info|
|
48
|
+
next if album_date
|
49
|
+
photo_interval = value_for_dictionary_key('DateAsTimerInterval', image_info).content.to_i
|
50
|
+
album_date = (IPHOTO_EPOCH + photo_interval).to_date
|
51
|
+
end
|
52
|
+
say "Automatically adding #{album_date} prefix to folder: #{folder_name}"
|
53
|
+
folder_name = "#{album_date} #{folder_name}"
|
54
|
+
end
|
55
|
+
|
44
56
|
if folder_name.match(album_filter)
|
45
57
|
yield folder_name, album_info
|
46
58
|
else
|
@@ -49,6 +61,14 @@ module IphotoBackup
|
|
49
61
|
end
|
50
62
|
end
|
51
63
|
|
64
|
+
def each_image(album_info, &block)
|
65
|
+
album_images = value_for_dictionary_key('KeyList', album_info).css('string').map(&:content)
|
66
|
+
album_images.each do |image_id|
|
67
|
+
image_info = info_for_image image_id
|
68
|
+
yield image_info
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
52
72
|
def info_for_image(image_id)
|
53
73
|
value_for_dictionary_key image_id, master_images
|
54
74
|
end
|
@@ -78,7 +98,8 @@ module IphotoBackup
|
|
78
98
|
|
79
99
|
def root_dictionary
|
80
100
|
@root_dictionary ||= begin
|
81
|
-
file = File.expand_path
|
101
|
+
file = File.expand_path options[:config]
|
102
|
+
say "Loading AlbumData: #{file}"
|
82
103
|
doc = Nokogiri.XML(File.read(file))
|
83
104
|
doc.child.children.find {|n| n.name == 'dict' }
|
84
105
|
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<plist version="1.0">
|
3
|
+
<dict>
|
4
|
+
<key>List of Rolls</key>
|
5
|
+
<array>
|
6
|
+
<dict>
|
7
|
+
<key>RollID</key>
|
8
|
+
<integer>640</integer>
|
9
|
+
<key>RollName</key>
|
10
|
+
<string>Summer Party</string>
|
11
|
+
<key>RollDateAsTimerInterval</key>
|
12
|
+
<real>21600.000000</real>
|
13
|
+
<key>KeyPhotoKey</key>
|
14
|
+
<string>1001</string>
|
15
|
+
<key>PhotoCount</key>
|
16
|
+
<integer>4</integer>
|
17
|
+
<key>KeyList</key>
|
18
|
+
<array>
|
19
|
+
<string>1001</string>
|
20
|
+
<string>1002</string>
|
21
|
+
</array>
|
22
|
+
</dict>
|
23
|
+
<dict>
|
24
|
+
<key>RollID</key>
|
25
|
+
<integer>646</integer>
|
26
|
+
<key>ProjectUuid</key>
|
27
|
+
<string>vU0ooK6bSX2Ih3PpnghUxA</string>
|
28
|
+
<key>RollName</key>
|
29
|
+
<string>2013-10-10 Fall Supper</string>
|
30
|
+
<key>RollDateAsTimerInterval</key>
|
31
|
+
<real>21600.000000</real>
|
32
|
+
<key>KeyPhotoKey</key>
|
33
|
+
<string>2002</string>
|
34
|
+
<key>PhotoCount</key>
|
35
|
+
<integer>1</integer>
|
36
|
+
<key>KeyList</key>
|
37
|
+
<array>
|
38
|
+
<string>2001</string>
|
39
|
+
<string>2002</string>
|
40
|
+
</array>
|
41
|
+
</dict>
|
42
|
+
</array>
|
43
|
+
<key>Master Image List</key>
|
44
|
+
<dict>
|
45
|
+
<key>1001</key>
|
46
|
+
<dict>
|
47
|
+
<key>Caption</key>
|
48
|
+
<string>IMG_3511</string>
|
49
|
+
<key>Comment</key>
|
50
|
+
<string> </string>
|
51
|
+
<key>GUID</key>
|
52
|
+
<string>oh0lvaosT7y3i1ykESTalQ</string>
|
53
|
+
<key>Roll</key>
|
54
|
+
<integer>636</integer>
|
55
|
+
<key>Rating</key>
|
56
|
+
<integer>0</integer>
|
57
|
+
<key>ImagePath</key>
|
58
|
+
<string><%= PROJECT_DIR %>/spec/fixtures/image.jpg</string>
|
59
|
+
<key>MediaType</key>
|
60
|
+
<string>Image</string>
|
61
|
+
<key>ModDateAsTimerInterval</key>
|
62
|
+
<real>404275004.228640</real>
|
63
|
+
<key>DateAsTimerInterval</key>
|
64
|
+
<real>402413114.000000</real>
|
65
|
+
<key>DateAsTimerIntervalGMT</key>
|
66
|
+
<real>402395114.000000</real>
|
67
|
+
<key>MetaModDateAsTimerInterval</key>
|
68
|
+
<real>404275024.317730</real>
|
69
|
+
<key>OriginalPath</key>
|
70
|
+
<string><%= PROJECT_DIR %>/spec/fixtures/image.jpg</string>
|
71
|
+
<key>ThumbPath</key>
|
72
|
+
<string><%= PROJECT_DIR %>/spec/fixtures/image.jpg</string>
|
73
|
+
</dict>
|
74
|
+
<key>1002</key>
|
75
|
+
<dict>
|
76
|
+
<key>Caption</key>
|
77
|
+
<string>IMG_3511</string>
|
78
|
+
<key>Comment</key>
|
79
|
+
<string> </string>
|
80
|
+
<key>GUID</key>
|
81
|
+
<string>oh0lvaosT7y3i1ykESTalQ</string>
|
82
|
+
<key>Roll</key>
|
83
|
+
<integer>636</integer>
|
84
|
+
<key>Rating</key>
|
85
|
+
<integer>0</integer>
|
86
|
+
<key>ImagePath</key>
|
87
|
+
<string><%= PROJECT_DIR %>/spec/fixtures/image2.jpg</string>
|
88
|
+
<key>MediaType</key>
|
89
|
+
<string>Image</string>
|
90
|
+
<key>ModDateAsTimerInterval</key>
|
91
|
+
<real>404275004.228640</real>
|
92
|
+
<key>DateAsTimerInterval</key>
|
93
|
+
<real>402413114.000000</real>
|
94
|
+
<key>DateAsTimerIntervalGMT</key>
|
95
|
+
<real>402395114.000000</real>
|
96
|
+
<key>MetaModDateAsTimerInterval</key>
|
97
|
+
<real>404275024.317730</real>
|
98
|
+
<key>OriginalPath</key>
|
99
|
+
<string><%= PROJECT_DIR %>/spec/fixtures/image2.jpg</string>
|
100
|
+
<key>ThumbPath</key>
|
101
|
+
<string><%= PROJECT_DIR %>/spec/fixtures/image2.jpg</string>
|
102
|
+
</dict>
|
103
|
+
<key>2001</key>
|
104
|
+
<dict>
|
105
|
+
<key>Caption</key>
|
106
|
+
<string>IMG_3511</string>
|
107
|
+
<key>Comment</key>
|
108
|
+
<string> </string>
|
109
|
+
<key>GUID</key>
|
110
|
+
<string>oh0lvaosT7y3i1ykESTalQ</string>
|
111
|
+
<key>Roll</key>
|
112
|
+
<integer>636</integer>
|
113
|
+
<key>Rating</key>
|
114
|
+
<integer>0</integer>
|
115
|
+
<key>ImagePath</key>
|
116
|
+
<string><%= PROJECT_DIR %>/spec/fixtures/image.jpg</string>
|
117
|
+
<key>MediaType</key>
|
118
|
+
<string>Image</string>
|
119
|
+
<key>ModDateAsTimerInterval</key>
|
120
|
+
<real>404275004.228640</real>
|
121
|
+
<key>DateAsTimerInterval</key>
|
122
|
+
<real>402413114.000000</real>
|
123
|
+
<key>DateAsTimerIntervalGMT</key>
|
124
|
+
<real>402395114.000000</real>
|
125
|
+
<key>MetaModDateAsTimerInterval</key>
|
126
|
+
<real>404275024.317730</real>
|
127
|
+
<key>OriginalPath</key>
|
128
|
+
<string><%= PROJECT_DIR %>/spec/fixtures/image.jpg</string>
|
129
|
+
<key>ThumbPath</key>
|
130
|
+
<string><%= PROJECT_DIR %>/spec/fixtures/image.jpg</string>
|
131
|
+
</dict>
|
132
|
+
<key>2002</key>
|
133
|
+
<dict>
|
134
|
+
<key>Caption</key>
|
135
|
+
<string>IMG_3511</string>
|
136
|
+
<key>Comment</key>
|
137
|
+
<string> </string>
|
138
|
+
<key>GUID</key>
|
139
|
+
<string>oh0lvaosT7y3i1ykESTalQ</string>
|
140
|
+
<key>Roll</key>
|
141
|
+
<integer>636</integer>
|
142
|
+
<key>Rating</key>
|
143
|
+
<integer>0</integer>
|
144
|
+
<key>ImagePath</key>
|
145
|
+
<string><%= PROJECT_DIR %>/spec/fixtures/image2.jpg</string>
|
146
|
+
<key>MediaType</key>
|
147
|
+
<string>Image</string>
|
148
|
+
<key>ModDateAsTimerInterval</key>
|
149
|
+
<real>404275004.228640</real>
|
150
|
+
<key>DateAsTimerInterval</key>
|
151
|
+
<real>402413114.000000</real>
|
152
|
+
<key>DateAsTimerIntervalGMT</key>
|
153
|
+
<real>402395114.000000</real>
|
154
|
+
<key>MetaModDateAsTimerInterval</key>
|
155
|
+
<real>404275024.317730</real>
|
156
|
+
<key>OriginalPath</key>
|
157
|
+
<string><%= PROJECT_DIR %>/spec/fixtures/image2.jpg</string>
|
158
|
+
<key>ThumbPath</key>
|
159
|
+
<string><%= PROJECT_DIR %>/spec/fixtures/image2.jpg</string>
|
160
|
+
</dict>
|
161
|
+
</dict>
|
162
|
+
</dict>
|
163
|
+
</plist>
|
Binary file
|
Binary file
|
@@ -1,8 +1,17 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe IphotoBackup::CLI do
|
4
|
+
TMP_OUTPUT_DIRECTORY = 'tmp/backup'
|
5
|
+
PROJECT_DIR = File.expand_path(File.join(File.expand_path(__dir__), '../../../'))
|
6
|
+
|
4
7
|
let(:args) { [] }
|
5
|
-
let(:options) {
|
8
|
+
let(:options) {
|
9
|
+
{
|
10
|
+
config: 'tmp/AlbumData.xml',
|
11
|
+
output: TMP_OUTPUT_DIRECTORY,
|
12
|
+
filter: '.*'
|
13
|
+
}
|
14
|
+
}
|
6
15
|
let(:config) do
|
7
16
|
{
|
8
17
|
pretend: true
|
@@ -10,12 +19,66 @@ describe IphotoBackup::CLI do
|
|
10
19
|
end
|
11
20
|
let(:cli) { IphotoBackup::CLI.new(args, options, config) }
|
12
21
|
|
22
|
+
before do
|
23
|
+
template = File.read('spec/fixtures/AlbumData.xml.erb')
|
24
|
+
erb = ERB.new(template)
|
25
|
+
FileUtils.mkdir('tmp') unless File.exists?('tmp')
|
26
|
+
File.open('tmp/AlbumData.xml', 'w+') do |f|
|
27
|
+
f << erb.result(binding)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
after do
|
31
|
+
FileUtils.rm_rf TMP_OUTPUT_DIRECTORY
|
32
|
+
end
|
33
|
+
|
13
34
|
describe '#export' do
|
14
35
|
before do
|
15
36
|
cli.export
|
16
37
|
end
|
17
|
-
|
18
|
-
|
38
|
+
context 'with basic options' do
|
39
|
+
it 'creates folder for first event' do
|
40
|
+
expect(File.exists?('tmp/backup/Summer Party')).to be_true
|
41
|
+
end
|
42
|
+
it 'creates folder for second event' do
|
43
|
+
expect(File.exists?('tmp/backup/2013-10-10 Fall Supper')).to be_true
|
44
|
+
end
|
45
|
+
it 'copies images for first event' do
|
46
|
+
expect(Dir.glob('tmp/backup/Summer Party/*.jpg').length).to eq 2
|
47
|
+
end
|
48
|
+
it 'copies images for second event' do
|
49
|
+
expect(Dir.glob('tmp/backup/2013-10-10 Fall Supper/*.jpg').length).to eq 2
|
50
|
+
end
|
51
|
+
end
|
52
|
+
context 'when filter only matches first event' do
|
53
|
+
let(:options) {
|
54
|
+
{
|
55
|
+
config: 'tmp/AlbumData.xml',
|
56
|
+
output: TMP_OUTPUT_DIRECTORY,
|
57
|
+
filter: 'Summer'
|
58
|
+
}
|
59
|
+
}
|
60
|
+
it 'creates folder for first event' do
|
61
|
+
expect(File.exists?('tmp/backup/Summer Party')).to be_true
|
62
|
+
end
|
63
|
+
it 'does not createfolder for second event' do
|
64
|
+
expect(File.exists?('tmp/backup/2013-10-10 Fall Supper')).to be_false
|
65
|
+
end
|
66
|
+
end
|
67
|
+
context 'with --include-date-prefix option', focus: true do
|
68
|
+
let(:options) {
|
69
|
+
{
|
70
|
+
config: 'tmp/AlbumData.xml',
|
71
|
+
output: TMP_OUTPUT_DIRECTORY,
|
72
|
+
filter: '.*',
|
73
|
+
:'include-date-prefix' => true
|
74
|
+
}
|
75
|
+
}
|
76
|
+
it 'creates folder for first event and adds date prefix' do
|
77
|
+
expect(File.exists?('tmp/backup/2013-10-02 Summer Party')).to be_true
|
78
|
+
end
|
79
|
+
it 'creates folder for second event and uses existing date prefix' do
|
80
|
+
expect(File.exists?('tmp/backup/2013-10-10 Fall Supper')).to be_true
|
81
|
+
end
|
19
82
|
end
|
20
83
|
end
|
21
84
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: iphoto_backup
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ryan Sonnek
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2013-
|
11
|
+
date: 2013-12-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: thor
|
@@ -105,6 +105,7 @@ files:
|
|
105
105
|
- .gitignore
|
106
106
|
- .rspec
|
107
107
|
- .ruby-version
|
108
|
+
- .travis.yml
|
108
109
|
- Gemfile
|
109
110
|
- LICENSE.txt
|
110
111
|
- README.md
|
@@ -114,6 +115,9 @@ files:
|
|
114
115
|
- lib/iphoto_backup.rb
|
115
116
|
- lib/iphoto_backup/cli.rb
|
116
117
|
- lib/iphoto_backup/version.rb
|
118
|
+
- spec/fixtures/AlbumData.xml.erb
|
119
|
+
- spec/fixtures/image.jpg
|
120
|
+
- spec/fixtures/image2.jpg
|
117
121
|
- spec/lib/iphoto_backup/cli_spec.rb
|
118
122
|
- spec/spec_helper.rb
|
119
123
|
- spec/support/meet_expectations_matcher.rb
|
@@ -142,6 +146,9 @@ signing_key:
|
|
142
146
|
specification_version: 4
|
143
147
|
summary: copy files out of iPhoto into a backup directory
|
144
148
|
test_files:
|
149
|
+
- spec/fixtures/AlbumData.xml.erb
|
150
|
+
- spec/fixtures/image.jpg
|
151
|
+
- spec/fixtures/image2.jpg
|
145
152
|
- spec/lib/iphoto_backup/cli_spec.rb
|
146
153
|
- spec/spec_helper.rb
|
147
154
|
- spec/support/meet_expectations_matcher.rb
|