photostat 0.0.1
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.
- data/.gitignore +3 -0
- data/Gemfile +4 -0
- data/README.md +27 -0
- data/Rakefile +1 -0
- data/bin/photostat +7 -0
- data/junk/filehash.rb +25 -0
- data/junk/flickr-sync-old.rb +242 -0
- data/junk/rack-sample.rb +12 -0
- data/junk/trash.rb +41 -0
- data/lib/photostat/db/001_create_photos_with_tags.rb +33 -0
- data/lib/photostat/db/002_photos_index.rb +5 -0
- data/lib/photostat/db/003_photos_add_flickr_info.rb +7 -0
- data/lib/photostat/db/base.rb +26 -0
- data/lib/photostat/plugins/00_base.rb +111 -0
- data/lib/photostat/plugins/01_local.rb +141 -0
- data/lib/photostat/plugins/02_flickr.rb +225 -0
- data/lib/photostat/utils/os.rb +104 -0
- data/lib/photostat.rb +89 -0
- data/photostat.gemspec +30 -0
- metadata +155 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
Photostat
|
2
|
+
=========
|
3
|
+
|
4
|
+
For managing photos, like a hacker. It is already usable for me, but
|
5
|
+
work is still in progress and I don't recommend it to people yet.
|
6
|
+
|
7
|
+
Here's what this app currently does:
|
8
|
+
|
9
|
+
* it builds a local repository of photos organized in $REPO/$Y-$m directories
|
10
|
+
* movies (MOV) are copied in $REPO/movies
|
11
|
+
* it takes care of duplicates (the file's identity is based on an MD5 hash + the created date)
|
12
|
+
* the creation date is taken from the file's EXIF data
|
13
|
+
* in case of movies, it is the date of the last modification
|
14
|
+
* on flickr:sync it uploads missing files from local repo to Flickr
|
15
|
+
(takes care of duplicates) and also downloads tags / visibility info
|
16
|
+
|
17
|
+
|
18
|
+
Rationale
|
19
|
+
---------
|
20
|
+
|
21
|
+
I'm tired of graphical UIs that suck.
|
22
|
+
|
23
|
+
I also need consistent backup in the cloud.
|
24
|
+
Now I'm working on Flickr integration, Picasa coming next.
|
25
|
+
|
26
|
+
I want 2 cheap cloud backups and a properly managed local repository.
|
27
|
+
These photos are too important for me.
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/bin/photostat
ADDED
data/junk/filehash.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'all_your_base'
|
2
|
+
|
3
|
+
module FileHash
|
4
|
+
|
5
|
+
def self.file_md5(file)
|
6
|
+
out = `md5sum #{file}`
|
7
|
+
out =~ /^(\S+)/
|
8
|
+
$1.strip
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.calculate(file)
|
12
|
+
base16charset = ('0'..'9').to_a + ('a'..'f').to_a
|
13
|
+
base16 = AllYourBase::Are.new({:charset => base16charset})
|
14
|
+
base62 = AllYourBase::Are.new({:charset => AllYourBase::Are::BASE_62_CHARSET})
|
15
|
+
|
16
|
+
md5 = file_md5(file).to_s.strip
|
17
|
+
ret = base16.convert_to_base_10(md5)
|
18
|
+
base62.convert_from_base_10(ret)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
if __FILE__ == $0 and ARGV.length
|
24
|
+
puts FileHash.calculate(ARGV[0])
|
25
|
+
end
|
@@ -0,0 +1,242 @@
|
|
1
|
+
require "optparse"
|
2
|
+
require 'yaml'
|
3
|
+
require 'flickraw'
|
4
|
+
require 'ostruct'
|
5
|
+
require 'benchmark'
|
6
|
+
|
7
|
+
|
8
|
+
module FlickrCLI
|
9
|
+
|
10
|
+
class Image
|
11
|
+
attr_accessor :md5, :local_path, :photoid
|
12
|
+
|
13
|
+
def self.from_path(path)
|
14
|
+
img = Image.new
|
15
|
+
img.local_path = path
|
16
|
+
img.md5 = calculate_md5(path)
|
17
|
+
return img
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.calculate_md5(file)
|
21
|
+
out = `md5sum #{file}`
|
22
|
+
out =~ /^(\S+)/
|
23
|
+
$1.strip
|
24
|
+
end
|
25
|
+
|
26
|
+
def flickr_id!
|
27
|
+
if not @photoid and md5
|
28
|
+
photos = flickr.photos.search(:user_id => "me", :tags => "checksum:md5=" + md5)
|
29
|
+
@photoid = photos[0].id if photos and photos.size > 0
|
30
|
+
end
|
31
|
+
@photoid
|
32
|
+
end
|
33
|
+
|
34
|
+
def upload(options)
|
35
|
+
set_name = options.delete(:set)
|
36
|
+
|
37
|
+
options[:title] ||= File.basename(local_path)
|
38
|
+
options[:tags] ||= ''
|
39
|
+
options[:tags] += ' sync checksum:md5=' + md5
|
40
|
+
options[:safety_level] ||= '1'
|
41
|
+
options[:content_type] ||= '1'
|
42
|
+
options[:is_family] ||= '0'
|
43
|
+
options[:is_friend] ||= '0'
|
44
|
+
options[:is_public] ||= '0'
|
45
|
+
options[:hidden] ||= '2'
|
46
|
+
|
47
|
+
photoid = flickr.upload_photo(local_path, options)
|
48
|
+
raise "Upload failed for #{local_path}" if not photoid
|
49
|
+
|
50
|
+
#if set_name
|
51
|
+
# set = flickr.photosets.getList.find {|set| set.title == set_name}
|
52
|
+
# if not set
|
53
|
+
# set = flickr.photosets.create(:title => set_name, :primary_photo_id => photoid)
|
54
|
+
# else
|
55
|
+
# flickr.photosets.addPhoto(:photoset_id => set.id, :photo_id => photoid)
|
56
|
+
# end
|
57
|
+
#end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
class ImageService
|
63
|
+
def initialize
|
64
|
+
@cache_file = ENV['HOME'] + "/.flickr-cli/flickr-id-md5-cache"
|
65
|
+
@local_images = []
|
66
|
+
|
67
|
+
if File.exists? @cache_file
|
68
|
+
@cache = YAML::load_file(@cache_file)
|
69
|
+
else
|
70
|
+
@cache = {}
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def image_from_path(path)
|
75
|
+
img = Image.from_path(path)
|
76
|
+
img.photoid = @cache[img.md5]
|
77
|
+
|
78
|
+
if not img.photoid
|
79
|
+
photoid = img.flickr_id!
|
80
|
+
@cache[img.md5] = photoid
|
81
|
+
end
|
82
|
+
|
83
|
+
@local_images << img
|
84
|
+
return img
|
85
|
+
end
|
86
|
+
|
87
|
+
def save
|
88
|
+
File.open(@cache_file, 'w') do |f|
|
89
|
+
f.write(@cache.to_yaml)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def self.open
|
94
|
+
fh = ImageService.new
|
95
|
+
yield fh
|
96
|
+
ensure
|
97
|
+
fh.save if fh
|
98
|
+
end
|
99
|
+
|
100
|
+
def self.search_local_dir(directory)
|
101
|
+
files = Dir[File.join(directory, "**", "*")].find_all{|path| path =~ /\.(jpe?g|png)$/i }
|
102
|
+
total = files.size
|
103
|
+
count = 0
|
104
|
+
|
105
|
+
benchmarks = []
|
106
|
+
last_avg = nil
|
107
|
+
|
108
|
+
self.open do |service|
|
109
|
+
files.each {|path|
|
110
|
+
|
111
|
+
uploaded = false
|
112
|
+
bm = Benchmark.measure do
|
113
|
+
count += 1
|
114
|
+
img = service.image_from_path(path)
|
115
|
+
|
116
|
+
stats = OpenStruct.new
|
117
|
+
stats.count = count
|
118
|
+
stats.total = total
|
119
|
+
stats.unit_time = last_avg
|
120
|
+
|
121
|
+
est = last_avg ? (total - count) * last_avg : nil
|
122
|
+
stats.estimate_time = est
|
123
|
+
|
124
|
+
unless img.photoid
|
125
|
+
yield img, stats
|
126
|
+
uploaded = true
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
if uploaded
|
131
|
+
benchmarks << bm.to_a[-1]
|
132
|
+
bp = benchmarks.size >= 10 ? benchmarks[-10,10] : benchmarks
|
133
|
+
last_avg = bp.inject{|m,e| m+e} / bp.size
|
134
|
+
end
|
135
|
+
}
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
|
141
|
+
class << self
|
142
|
+
|
143
|
+
# Takes a period of time in seconds and returns it in human-readable form (down to minutes)
|
144
|
+
def time_period_to_s(time_period)
|
145
|
+
begin
|
146
|
+
time_period = time_period.to_i
|
147
|
+
return nil if time_period == 0
|
148
|
+
rescue
|
149
|
+
return nil
|
150
|
+
end
|
151
|
+
|
152
|
+
interval_array = [ [:weeks, 604800], [:days, 86400], [:hours, 3600], [:mins, 60] ]
|
153
|
+
parts = []
|
154
|
+
|
155
|
+
interval_array.each do |sub|
|
156
|
+
if time_period>= sub[1] then
|
157
|
+
time_val, time_period = time_period.divmod( sub[1] )
|
158
|
+
name = sub[0].to_s
|
159
|
+
|
160
|
+
if time_val > 0
|
161
|
+
parts << "#{time_val} #{sub[0]}"
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
return parts.join(', ')
|
167
|
+
end
|
168
|
+
|
169
|
+
def get_config_options
|
170
|
+
conf_file = ENV['HOME'] + "/.flickr-cli/config"
|
171
|
+
YAML::load_file(conf_file)
|
172
|
+
end
|
173
|
+
|
174
|
+
def authenticate(config)
|
175
|
+
FlickRaw.api_key = config[:app][:api_key]
|
176
|
+
FlickRaw.shared_secret = config[:app][:shared_secret]
|
177
|
+
|
178
|
+
flickr.auth.checkToken :auth_token => config[:user][:auth_token]
|
179
|
+
end
|
180
|
+
|
181
|
+
def synchronize_directory(directory, options)
|
182
|
+
config = get_config_options
|
183
|
+
auth = authenticate(config)
|
184
|
+
|
185
|
+
puts "UPLOADING to Flickr ...\n"
|
186
|
+
ImageService.search_local_dir(directory) do |img, stats|
|
187
|
+
estimate = time_period_to_s(stats.estimate_time)
|
188
|
+
$stdout.write "\rProgress: #{stats.count} / #{stats.total}" + (estimate ? " (remaining: #{estimate})" : '')
|
189
|
+
img.upload(:tags => "cristian baby sync private", :is_public => '0', :is_friend => "0", :is_family => "1", :safety_level => "1", :hidden => "2")
|
190
|
+
end
|
191
|
+
|
192
|
+
puts "\nDONE!\n"
|
193
|
+
end
|
194
|
+
|
195
|
+
def run
|
196
|
+
options = OpenStruct.new
|
197
|
+
|
198
|
+
command = nil
|
199
|
+
command = ARGV[0] if ARGV.length
|
200
|
+
|
201
|
+
parser = OptionParser.new do |opts|
|
202
|
+
opts.program_name = "Flickr Sync"
|
203
|
+
opts.banner = "\nUsage: #$0 <directory> [options]\n\n"
|
204
|
+
|
205
|
+
opts.on_tail("-h", "--help", "Show this message") do
|
206
|
+
puts opts
|
207
|
+
puts
|
208
|
+
exit
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
begin
|
213
|
+
directory = ARGV && ARGV[0]
|
214
|
+
if not directory or not File.directory?(directory)
|
215
|
+
raise OptionParser::MissingArgument, "<directory>"
|
216
|
+
end
|
217
|
+
|
218
|
+
parser.parse!
|
219
|
+
|
220
|
+
conf_file = ENV['HOME'] + "/.flickr-cli/config"
|
221
|
+
if not File.exists?(conf_file)
|
222
|
+
raise "Configuration file missing, run ./flickr-conf"
|
223
|
+
end
|
224
|
+
|
225
|
+
rescue Exception => e
|
226
|
+
unless e.to_s == 'exit'
|
227
|
+
puts
|
228
|
+
puts "ERROR: #{e}"
|
229
|
+
puts
|
230
|
+
end
|
231
|
+
exit
|
232
|
+
end
|
233
|
+
|
234
|
+
synchronize_directory(directory, options)
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
|
240
|
+
if __FILE__ == $0
|
241
|
+
FlickrCLI::run
|
242
|
+
end
|
data/junk/rack-sample.rb
ADDED
data/junk/trash.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
puts "Fetching photos in set ..."
|
2
|
+
|
3
|
+
set = flickr.photosets.getList.find {|set| set.title == options.set_name}
|
4
|
+
has_set = (set ? true : false)
|
5
|
+
|
6
|
+
debugger
|
7
|
+
|
8
|
+
if has_set
|
9
|
+
resp = flickr.photosets.getPhotos(:photoset_id => set.id)
|
10
|
+
|
11
|
+
puts resp.to_yaml
|
12
|
+
resp.each do |photo|
|
13
|
+
puts photo.to_yaml
|
14
|
+
end
|
15
|
+
exit
|
16
|
+
end
|
17
|
+
|
18
|
+
exit
|
19
|
+
puts "Indexing local files ..."
|
20
|
+
|
21
|
+
available_files = {}
|
22
|
+
photos_list(directory) do |fpath, md5|
|
23
|
+
puts "#{md5} #{fpath}"
|
24
|
+
raise Exception, "File conflict: #{fpath} #{available_files[md5]}" if available_files[md5]
|
25
|
+
available_files[md5] = fpath
|
26
|
+
end
|
27
|
+
|
28
|
+
puts "Uploading ..."
|
29
|
+
|
30
|
+
# uploading photos !!!
|
31
|
+
available_files.each do |md5, path|
|
32
|
+
puts path
|
33
|
+
photoid = flickr.upload_photo(path, :title => md5, :tags => "private", :is_public => '0', :is_friend => "0", :is_family => "0", :safety_level => "1", :hidden => "2")
|
34
|
+
if not has_set
|
35
|
+
set = flickr.photosets.create(:title => "Private", :primary_photo_id => photoid)
|
36
|
+
has_set = true
|
37
|
+
else
|
38
|
+
flickr.photosets.addPhoto(:photoset_id => set.id, :photo_id => photoid)
|
39
|
+
end
|
40
|
+
puts "... done!"
|
41
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
Sequel.migration do
|
2
|
+
up do
|
3
|
+
create_table :photos do
|
4
|
+
primary_key :id
|
5
|
+
|
6
|
+
String :uid, :null => false
|
7
|
+
String :type, :null => false
|
8
|
+
|
9
|
+
String :local_path, :null => false
|
10
|
+
String :visibility, :null => false
|
11
|
+
DateTime :created_at, :null => false
|
12
|
+
|
13
|
+
String :md5, :null => true
|
14
|
+
|
15
|
+
index :uid, :unique => true
|
16
|
+
index :local_path
|
17
|
+
index :type
|
18
|
+
index :md5
|
19
|
+
end
|
20
|
+
|
21
|
+
create_table :tags do
|
22
|
+
primary_key :id
|
23
|
+
String :name, :null => false
|
24
|
+
String :photo_id, :null => false
|
25
|
+
|
26
|
+
index [:photo_id, :name], :unique => true
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
down do
|
31
|
+
drop_column :photos, :tags
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'sequel'
|
2
|
+
Sequel.datetime_class = DateTime
|
3
|
+
|
4
|
+
|
5
|
+
module Photostat
|
6
|
+
module DB
|
7
|
+
def self.instance
|
8
|
+
unless @DB
|
9
|
+
config = Photostat.config
|
10
|
+
system = File.join(config[:repository_path], 'system')
|
11
|
+
Dir.mkdir system unless File.directory? system
|
12
|
+
|
13
|
+
path = File.join(system, 'photostat.db')
|
14
|
+
@DB = Sequel.sqlite(path)
|
15
|
+
end
|
16
|
+
return @DB
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.migrate!
|
20
|
+
db = self.instance
|
21
|
+
Sequel.extension :migration
|
22
|
+
Sequel::Migrator.apply(db, File.dirname(__FILE__))
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
@@ -0,0 +1,111 @@
|
|
1
|
+
module Photostat
|
2
|
+
|
3
|
+
module Plugins
|
4
|
+
def self.all_in_order
|
5
|
+
unless @plugins_ordered
|
6
|
+
@plugins_ordered ||= []
|
7
|
+
end
|
8
|
+
@plugins_ordered
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.all
|
12
|
+
unless @plugins
|
13
|
+
@plugins ||= {}
|
14
|
+
end
|
15
|
+
@plugins
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.load_all!
|
19
|
+
# loads all available plugins
|
20
|
+
Photostat.root.join('plugins').children.sort.each do |plugin_path|
|
21
|
+
next if File.directory? plugin_path
|
22
|
+
next unless plugin_path.to_s =~ /\/\d+_\w+.rb$/
|
23
|
+
next unless File.basename(plugin_path) != '00_base.rb'
|
24
|
+
require plugin_path.to_s
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class Base
|
29
|
+
def self.help_text(msg=nil)
|
30
|
+
@help_text = msg if msg
|
31
|
+
@help_text
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.exposes_help
|
35
|
+
@exposes_help
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.exposes(name=nil, help_text=nil)
|
39
|
+
@exposes_names ||= []
|
40
|
+
@exposes_help ||= {}
|
41
|
+
|
42
|
+
if name
|
43
|
+
@exposes_names << name
|
44
|
+
@exposes_help[name] = help_text if help_text
|
45
|
+
end
|
46
|
+
@exposes_names
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.register_plugin(sub)
|
50
|
+
sub.name =~ /Photostat[:]{2}(\w+)/
|
51
|
+
name = $1
|
52
|
+
|
53
|
+
new_name = ''
|
54
|
+
name.each_char do |ch|
|
55
|
+
if ch != ch.downcase
|
56
|
+
ch = ch.downcase
|
57
|
+
ch = "_" + ch if new_name.length > 0
|
58
|
+
end
|
59
|
+
new_name += ch
|
60
|
+
end
|
61
|
+
|
62
|
+
sub.instance_variable_set :@plugin_name, new_name
|
63
|
+
::Photostat::Plugins.all[new_name] = sub
|
64
|
+
::Photostat::Plugins.all_in_order << sub
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.inherited(sub)
|
68
|
+
register_plugin sub
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.included(sub)
|
72
|
+
register_plugin sub
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.extended(sub)
|
76
|
+
register_plugin sub
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.plugin_name
|
80
|
+
@plugin_name
|
81
|
+
end
|
82
|
+
|
83
|
+
def activate!
|
84
|
+
# blank
|
85
|
+
end
|
86
|
+
|
87
|
+
def help
|
88
|
+
puts "Photostat version #{Photostat::VERSION}"
|
89
|
+
puts "Plugin: " + self.class.plugin_name.upcase
|
90
|
+
puts
|
91
|
+
puts "Usage: photostat <command> [options]*"
|
92
|
+
puts "For help on each individual command: photostat <command> --help"
|
93
|
+
puts
|
94
|
+
puts "Where command is one of the following:"
|
95
|
+
self.class.exposes.each do |name|
|
96
|
+
msg = " #{self.class.plugin_name}:#{name}"
|
97
|
+
if self.class.exposes_help[name]
|
98
|
+
msg += "\t- " + self.class.exposes_help[name]
|
99
|
+
end
|
100
|
+
puts msg
|
101
|
+
end
|
102
|
+
puts
|
103
|
+
end
|
104
|
+
|
105
|
+
def config
|
106
|
+
# blank
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
module Photostat
|
2
|
+
|
3
|
+
class Local < Plugins::Base
|
4
|
+
include OSUtils
|
5
|
+
include FileUtils
|
6
|
+
|
7
|
+
help_text "Manages your local photos repository"
|
8
|
+
|
9
|
+
exposes :config, "Configures your local database, repository path and Flickr login"
|
10
|
+
exposes :import, "Imports images from a directory path (recursively) to your Photostat repository"
|
11
|
+
|
12
|
+
def activate!
|
13
|
+
unless @activated
|
14
|
+
@db = Photostat::DB.instance
|
15
|
+
Photostat::DB.migrate!
|
16
|
+
@activated = true
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def import
|
21
|
+
opts = Trollop::options do
|
22
|
+
opt :path, "Local path to import", :required => true, :type => :string, :short => "-p"
|
23
|
+
opt :tags, "List of tags to classify imported pictures", :type => :strings
|
24
|
+
opt :visibility, "Choices are 'private', 'protected' and 'public'", :required => true, :type => :string
|
25
|
+
opt :move, "Move, instead of copy (better performance, defaults to false, careful)", :type => :boolean
|
26
|
+
opt :dry, "Just fake it and print the resulting files", :type => :boolean
|
27
|
+
end
|
28
|
+
|
29
|
+
Trollop::die :path, "must be a valid directory" unless File.directory? opts[:path]
|
30
|
+
Trollop::die :visibility, "is invalid. Choices are: private, protected and public" unless ['private', 'protected', 'public'].member? opts[:visibility]
|
31
|
+
opts[:tags] ||= []
|
32
|
+
|
33
|
+
activate!
|
34
|
+
|
35
|
+
source = File.expand_path opts[:path]
|
36
|
+
config = Photostat.config
|
37
|
+
|
38
|
+
files = files_in_dir(source, :match => /(.jpe?g|.mov)$/i, :absolute? => true)
|
39
|
+
count, total = 0, files.length
|
40
|
+
puts
|
41
|
+
|
42
|
+
interrupted = false
|
43
|
+
trap("INT") { interrupted = true }
|
44
|
+
|
45
|
+
files.each do |fpath|
|
46
|
+
break if interrupted
|
47
|
+
count += 1
|
48
|
+
|
49
|
+
STDOUT.print "\r - processed: #{count} / #{total}"
|
50
|
+
STDOUT.flush
|
51
|
+
|
52
|
+
if fpath =~ /.jpe?g/i
|
53
|
+
type = 'jpg'
|
54
|
+
exif = EXIFR::JPEG.new fpath
|
55
|
+
dt = (exif.date_time || File.mtime(fpath)).getgm
|
56
|
+
else
|
57
|
+
type = 'mov'
|
58
|
+
dt = File.mtime(fpath).getgm
|
59
|
+
end
|
60
|
+
|
61
|
+
md5 = partial_file_md5 fpath
|
62
|
+
uid = dt.strftime("%Y%m%d%H%M%S") + "-" + md5[0,6] + "." + type
|
63
|
+
|
64
|
+
local_dir = type == 'jpg' ? dt.strftime("%Y-%m") : 'movies'
|
65
|
+
local_path = File.join(local_dir, uid)
|
66
|
+
dest_dir = File.join(config[:repository_path], local_dir)
|
67
|
+
dest_path = File.join(config[:repository_path], local_path)
|
68
|
+
|
69
|
+
photo = @db[:photos].where(:uid => uid).first
|
70
|
+
photo_id = photo ? photo[:id] : nil
|
71
|
+
|
72
|
+
unless photo || opts[:dry]
|
73
|
+
photo_id = @db[:photos].insert(
|
74
|
+
:uid => uid,
|
75
|
+
:type => type,
|
76
|
+
:local_path => local_path,
|
77
|
+
:visibility => opts[:visibility],
|
78
|
+
:created_at => dt,
|
79
|
+
)
|
80
|
+
end
|
81
|
+
|
82
|
+
opts[:tags].each do |name|
|
83
|
+
next if opts[:dry]
|
84
|
+
next unless @db[:tags].where(:name => name, :photo_id => photo_id).empty?
|
85
|
+
@db[:tags].insert(:name => name, :photo_id => photo_id)
|
86
|
+
end
|
87
|
+
|
88
|
+
next if File.exists? dest_path
|
89
|
+
next if File.expand_path(dest_path) == File.expand_path(fpath)
|
90
|
+
|
91
|
+
unless opts[:dry]
|
92
|
+
mkdir_p dest_dir
|
93
|
+
cp fpath, dest_path unless opts[:move]
|
94
|
+
mv fpath, dest_path if opts[:move]
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
if !files or files.length == 0
|
99
|
+
puts " - nothing to do"
|
100
|
+
end
|
101
|
+
|
102
|
+
if interrupted
|
103
|
+
puts
|
104
|
+
puts " - interrupted by user"
|
105
|
+
puts
|
106
|
+
exit 0
|
107
|
+
end
|
108
|
+
|
109
|
+
puts
|
110
|
+
end
|
111
|
+
|
112
|
+
def config
|
113
|
+
puts
|
114
|
+
config_file = File.expand_path "~/.photostat"
|
115
|
+
|
116
|
+
config = {}
|
117
|
+
config = YAML::load(File.read config_file) if File.exists? config_file
|
118
|
+
|
119
|
+
config[:repository_path] ||= "~/Photos"
|
120
|
+
config[:repository_path] = input(
|
121
|
+
"Wanted location for your Photostat repository",
|
122
|
+
:dir? => true, :default => config[:repository_path])
|
123
|
+
config[:repository_path] = File.expand_path(config[:repository_path])
|
124
|
+
|
125
|
+
puts
|
126
|
+
unless File.directory? config[:repository_path]
|
127
|
+
Dir.mkdir config[:repository_path]
|
128
|
+
puts " >>>> repository #{config[:repository_path]} created"
|
129
|
+
end
|
130
|
+
|
131
|
+
File.open(config_file, 'w') do |fh|
|
132
|
+
fh.write(YAML::dump(config))
|
133
|
+
puts " >>>> generated ~/.photostat config"
|
134
|
+
end
|
135
|
+
|
136
|
+
puts
|
137
|
+
end
|
138
|
+
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
@@ -0,0 +1,225 @@
|
|
1
|
+
module Photostat
|
2
|
+
class Flickr < Plugins::Base
|
3
|
+
include OSUtils
|
4
|
+
|
5
|
+
help_text "Manages your Flickr account"
|
6
|
+
|
7
|
+
exposes :config, "Configures Flickr login"
|
8
|
+
exposes :sync, "Uploads local photos to Flickr, downloads Flickr tags / visibility info"
|
9
|
+
|
10
|
+
def activate!
|
11
|
+
return if @activated
|
12
|
+
@activated = 1
|
13
|
+
|
14
|
+
# authenticating to Flickr
|
15
|
+
require 'flickraw'
|
16
|
+
|
17
|
+
@config = Photostat.config
|
18
|
+
config unless @config[:flickr]
|
19
|
+
|
20
|
+
FlickRaw.api_key = @config[:flickr][:api_key]
|
21
|
+
FlickRaw.shared_secret = @config[:flickr][:shared_secret]
|
22
|
+
|
23
|
+
flickr.access_token = @config[:flickr][:access_token]
|
24
|
+
flickr.access_secret = @config[:flickr][:access_secret]
|
25
|
+
|
26
|
+
begin
|
27
|
+
login = flickr.test.login
|
28
|
+
rescue FlickRaw::FailedResponse => e
|
29
|
+
STDERR.puts "ERROR: Flickr Authentication failed : #{e.msg}"
|
30
|
+
exit 1
|
31
|
+
end
|
32
|
+
|
33
|
+
Photostat::DB.migrate!
|
34
|
+
@db = Photostat::DB.instance
|
35
|
+
end
|
36
|
+
|
37
|
+
def update_md5_info_on_files!
|
38
|
+
activate!
|
39
|
+
|
40
|
+
rs = @db[:photos].where(:type => "jpg", :md5 => nil)
|
41
|
+
|
42
|
+
count = 0
|
43
|
+
total = rs.count
|
44
|
+
puts if total > 0
|
45
|
+
|
46
|
+
@db[:photos].where(:type => "jpg", :md5 => nil).all do |obj|
|
47
|
+
md5 = file_md5 File.join(@config[:repository_path], obj[:local_path])
|
48
|
+
@db[:photos].where(:uid => obj[:uid]).update(:md5 => md5)
|
49
|
+
|
50
|
+
count += 1
|
51
|
+
STDOUT.write("\r - processed md5 hash for local files: #{count} / #{total}")
|
52
|
+
STDOUT.flush
|
53
|
+
end
|
54
|
+
|
55
|
+
puts if total > 0
|
56
|
+
end
|
57
|
+
|
58
|
+
def update_flickr_info!
|
59
|
+
activate!
|
60
|
+
update_md5_info_on_files!
|
61
|
+
|
62
|
+
db = Photostat::DB.instance
|
63
|
+
|
64
|
+
rs = flickr.photos.search(:user_id => "me", :extras => 'machine_tags, tags, date_taken, date_upload', :per_page => 500)
|
65
|
+
pages_nr = rs.pages
|
66
|
+
page_idx = 1
|
67
|
+
|
68
|
+
count = 0
|
69
|
+
not_local = 0
|
70
|
+
not_tagged = 0
|
71
|
+
are_valid = 0
|
72
|
+
total = rs.total.to_i
|
73
|
+
|
74
|
+
valid_ids = []
|
75
|
+
|
76
|
+
puts if total > 0
|
77
|
+
while rs.length > 0
|
78
|
+
|
79
|
+
rs.each do |fphoto|
|
80
|
+
count += 1
|
81
|
+
|
82
|
+
unless fphoto.machine_tags =~ /checksum:md5=(\w+)/
|
83
|
+
not_tagged += 1
|
84
|
+
next
|
85
|
+
end
|
86
|
+
|
87
|
+
md5 = $1
|
88
|
+
obj = db[:photos].where(:md5 => md5).first
|
89
|
+
|
90
|
+
if not obj
|
91
|
+
not_local += 1
|
92
|
+
next
|
93
|
+
end
|
94
|
+
|
95
|
+
db[:tags].where(:photo_id => obj[:id]).delete
|
96
|
+
if fphoto[:tags] and !fphoto.tags.empty?
|
97
|
+
tags = fphoto.tags.split.select{|x| ! ['private', 'sync'].member?(x) && x !~ /checksum:/}
|
98
|
+
tags.each do |tag_name|
|
99
|
+
db[:tags].insert(:name => tag_name, :photo_id => obj[:id])
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
visibility = 'private'
|
104
|
+
visibility = 'protected' if fphoto.isfamily
|
105
|
+
visibility = 'public' if fphoto.ispublic
|
106
|
+
|
107
|
+
db[:photos].where(:md5 => md5).update(:has_flickr_upload => true, :visibility => visibility)
|
108
|
+
|
109
|
+
STDOUT.write("\r - processed #{count} of #{total}, with #{not_tagged} not tagged on flickr, #{not_local} not local")
|
110
|
+
STDOUT.flush
|
111
|
+
end
|
112
|
+
|
113
|
+
page_idx += 1
|
114
|
+
break if page_idx > pages_nr
|
115
|
+
rs = flickr.photos.search(:user_id => "me", :extras => 'machine_tags', :per_page => 500, :page => page_idx)
|
116
|
+
end
|
117
|
+
|
118
|
+
puts("\r - processed #{count} of #{total}, with #{not_tagged} not tagged and #{not_local} not local")
|
119
|
+
end
|
120
|
+
|
121
|
+
def sync
|
122
|
+
activate!
|
123
|
+
|
124
|
+
update_md5_info_on_files!
|
125
|
+
update_flickr_info!
|
126
|
+
|
127
|
+
db = Photostat::DB.instance
|
128
|
+
config = Photostat.config
|
129
|
+
|
130
|
+
rs = db[:photos].where(:type => 'jpg', :has_flickr_upload => false)
|
131
|
+
total = rs.count
|
132
|
+
count = 0
|
133
|
+
|
134
|
+
rs.order('created_at').all do |obj|
|
135
|
+
count += 1
|
136
|
+
STDOUT.write("\r - uploading file: #{count} / #{total}")
|
137
|
+
STDOUT.flush
|
138
|
+
|
139
|
+
src_path = File.join(config[:repository_path], obj[:local_path])
|
140
|
+
|
141
|
+
options = {}
|
142
|
+
options[:title] = obj[:uid]
|
143
|
+
options[:tags] = 'sync checksum:md5=' + obj[:md5]
|
144
|
+
options[:safety_level] = '1'
|
145
|
+
options[:content_type] = '1'
|
146
|
+
options[:is_family] = '0'
|
147
|
+
options[:is_friend] = '0'
|
148
|
+
options[:is_public] = '0'
|
149
|
+
options[:hidden] = '2'
|
150
|
+
|
151
|
+
begin
|
152
|
+
photoid = flickr.upload_photo(src_path, options)
|
153
|
+
rescue
|
154
|
+
sleep 5
|
155
|
+
# retrying again
|
156
|
+
photoid = flickr.upload_photo(src_path, options)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
puts "\n"
|
161
|
+
end
|
162
|
+
|
163
|
+
def config
|
164
|
+
puts
|
165
|
+
config_file = File.expand_path "~/.photostat"
|
166
|
+
|
167
|
+
unless File.exists? config_file
|
168
|
+
Photostat.configure_plugin! :local
|
169
|
+
end
|
170
|
+
|
171
|
+
config = YAML::load(File.read config_file)
|
172
|
+
|
173
|
+
config[:flickr] ||= {
|
174
|
+
:api_key => nil,
|
175
|
+
:shared_secret => nil
|
176
|
+
}
|
177
|
+
|
178
|
+
puts "Configuring Flickr!"
|
179
|
+
puts "-------------------"
|
180
|
+
puts
|
181
|
+
puts "You need to create an app to access your account, see:"
|
182
|
+
puts " http://www.flickr.com/services/apps/create/apply/"
|
183
|
+
puts "Or if you already have an app key available, find it here:"
|
184
|
+
puts " http://www.flickr.com/services/apps/"
|
185
|
+
puts
|
186
|
+
|
187
|
+
config[:flickr][:api_key] = input("Flickr Authentication :: Api Key",
|
188
|
+
:default => config[:flickr][:api_key])
|
189
|
+
config[:flickr][:shared_secret] = input("Flickr Authentication :: Shared Secret",
|
190
|
+
:default => config[:flickr][:shared_secret])
|
191
|
+
|
192
|
+
require 'flickraw'
|
193
|
+
|
194
|
+
FlickRaw.api_key = config[:flickr][:api_key]
|
195
|
+
FlickRaw.shared_secret = config[:flickr][:shared_secret]
|
196
|
+
|
197
|
+
token = flickr.get_request_token
|
198
|
+
auth_url = flickr.get_authorize_url(token['oauth_token'], :perms => 'delete')
|
199
|
+
|
200
|
+
puts
|
201
|
+
puts "Open this url in your process to complete the authication process:"
|
202
|
+
puts " " + auth_url
|
203
|
+
|
204
|
+
verify = input("Copy here the number given when visiting the link above")
|
205
|
+
begin
|
206
|
+
flickr.get_access_token(token['oauth_token'], token['oauth_token_secret'], verify)
|
207
|
+
login = flickr.test.login
|
208
|
+
puts "You are now authenticated as #{login.username} with token #{flickr.access_token} and secret #{flickr.access_secret}"
|
209
|
+
rescue FlickRaw::FailedResponse => e
|
210
|
+
STDERR.puts "ERROR: Flickr Authentication failed : #{e.msg}"
|
211
|
+
exit 1
|
212
|
+
end
|
213
|
+
|
214
|
+
config[:flickr][:access_token] = flickr.access_token
|
215
|
+
config[:flickr][:access_secret] = flickr.access_secret
|
216
|
+
|
217
|
+
File.open(config_file, 'w') do |fh|
|
218
|
+
fh.write(YAML::dump(config))
|
219
|
+
puts
|
220
|
+
puts " >>>> modified ~/.photostat config"
|
221
|
+
puts
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
|
3
|
+
module Photostat
|
4
|
+
module OSUtils
|
5
|
+
def input(msg, options=nil)
|
6
|
+
options ||= {}
|
7
|
+
default = options[:default]
|
8
|
+
is_dir = options[:dir?]
|
9
|
+
|
10
|
+
if default and !default.empty?
|
11
|
+
msg = msg + " (#{default}): "
|
12
|
+
else
|
13
|
+
msg = msg + ": "
|
14
|
+
end
|
15
|
+
|
16
|
+
print msg
|
17
|
+
resp = nil
|
18
|
+
|
19
|
+
while not resp
|
20
|
+
resp = STDIN.readline.strip
|
21
|
+
resp = default if !resp or resp.empty?
|
22
|
+
resp = File.expand_path resp if is_dir and resp and !resp.empty?
|
23
|
+
|
24
|
+
error_msg = "Invalid response"
|
25
|
+
|
26
|
+
is_not_dir = false
|
27
|
+
if resp and !resp.empty? and !File.directory? resp
|
28
|
+
is_not_dir = true if File.exists? resp
|
29
|
+
is_not_dir = true unless File.directory? File.dirname(resp)
|
30
|
+
error_msg = "Invalid directory path"
|
31
|
+
end
|
32
|
+
|
33
|
+
if !resp or resp.empty? or is_not_dir
|
34
|
+
puts "ERROR: #{error_msg}!"
|
35
|
+
print "\nTry again, #{msg}"
|
36
|
+
resp = nil
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
return resp
|
41
|
+
end
|
42
|
+
|
43
|
+
def exec(*params)
|
44
|
+
cmd = Escape.shell_command(params)
|
45
|
+
out = `#{cmd} 2>&1`
|
46
|
+
unless $?.exitstatus == 0
|
47
|
+
raise "Command exit with error!\nCommand: #{cmd}\nOut: #{out}"
|
48
|
+
end
|
49
|
+
return out
|
50
|
+
end
|
51
|
+
|
52
|
+
def files_in_dir(dir, options=nil)
|
53
|
+
files = []
|
54
|
+
dirs = []
|
55
|
+
dir = File.expand_path dir
|
56
|
+
current = dir
|
57
|
+
|
58
|
+
return [] unless File.directory? current
|
59
|
+
|
60
|
+
match = options[:match] if options
|
61
|
+
not_match = options[:not_match] if options
|
62
|
+
non_recursive = options[:non_recursive]
|
63
|
+
is_abs = options[:absolute?] or false
|
64
|
+
|
65
|
+
while current
|
66
|
+
Dir.entries(current).each do |name|
|
67
|
+
next unless name != '.' and name != '..'
|
68
|
+
path = File.join(current, name)
|
69
|
+
|
70
|
+
valid = true
|
71
|
+
valid = path =~ match if match and valid
|
72
|
+
valid = path !~ not_match if not_match and valid
|
73
|
+
|
74
|
+
if valid
|
75
|
+
rpath = path[dir.length+1,path.length]
|
76
|
+
yielded = is_abs ? File.join(dir, rpath) : rpath
|
77
|
+
files.push yielded
|
78
|
+
yield yielded if block_given?
|
79
|
+
end
|
80
|
+
|
81
|
+
dirs.push path if !non_recursive and File.directory? path
|
82
|
+
end
|
83
|
+
|
84
|
+
current = dirs.pop
|
85
|
+
end
|
86
|
+
|
87
|
+
files
|
88
|
+
end
|
89
|
+
|
90
|
+
def file_md5(file)
|
91
|
+
Digest::MD5.file(file).hexdigest
|
92
|
+
end
|
93
|
+
|
94
|
+
def partial_file_md5(file)
|
95
|
+
digest = Digest::MD5.new
|
96
|
+
digest << IO.read(file, 61440)
|
97
|
+
digest.hexdigest
|
98
|
+
end
|
99
|
+
|
100
|
+
def string_md5(string)
|
101
|
+
Digest::MD5.hexdigest(string)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
data/lib/photostat.rb
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
require "benchmark"
|
2
|
+
require "pathname"
|
3
|
+
require "yaml"
|
4
|
+
require "logger"
|
5
|
+
require "exifr"
|
6
|
+
require 'fileutils'
|
7
|
+
require "escape"
|
8
|
+
require 'logger'
|
9
|
+
require 'trollop'
|
10
|
+
|
11
|
+
require "photostat/utils/os"
|
12
|
+
require "photostat/db/base"
|
13
|
+
require "photostat/plugins/00_base"
|
14
|
+
|
15
|
+
module Photostat
|
16
|
+
VERSION = "0.0.1"
|
17
|
+
|
18
|
+
def self.root
|
19
|
+
Pathname.new File.join(File.dirname(__FILE__), 'photostat')
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.config
|
23
|
+
unless @config
|
24
|
+
@config = YAML.load(File.read(File.expand_path "~/.photostat"))
|
25
|
+
end
|
26
|
+
@config
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.activate!
|
30
|
+
Photostat::Plugins.all_in_order.each do |plugin|
|
31
|
+
plugin.new.activate!
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.configure_plugin!(name)
|
36
|
+
Photostat::Plugins.all[name.to_s].new.config
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.activate_plugin!(name)
|
40
|
+
Photostat::Plugin.all[name.to_s].new.activate!
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.execute
|
44
|
+
cmd, subc = nil, :help
|
45
|
+
cmd = ARGV.shift if ARGV and ARGV.length > 0
|
46
|
+
if cmd =~ /^(\w+):(\w+)$/
|
47
|
+
cmd, subc = $1, $2.to_sym
|
48
|
+
end
|
49
|
+
|
50
|
+
all_plugins = Photostat::Plugins.all
|
51
|
+
if cmd and all_plugins[cmd]
|
52
|
+
all_plugins[cmd].new.send subc
|
53
|
+
else
|
54
|
+
show_help
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.show_help
|
59
|
+
puts "Photostat version #{Photostat::VERSION}"
|
60
|
+
puts
|
61
|
+
puts "Getting help for a particular plugin: photostat <plugin_name>"
|
62
|
+
puts "Getting help for a particular command:: photostat <command> --help"
|
63
|
+
puts
|
64
|
+
puts "Available plugins:"
|
65
|
+
|
66
|
+
Photostat::Plugins.all.each_key do |cmd_name|
|
67
|
+
cmd_obj = Photostat::Plugins.all[cmd_name]
|
68
|
+
help_text = "#{cmd_name}"
|
69
|
+
help_text += "\t- #{cmd_obj.help_text}" if cmd_obj.help_text
|
70
|
+
puts " #{help_text}"
|
71
|
+
end
|
72
|
+
|
73
|
+
puts
|
74
|
+
puts "All available commands:"
|
75
|
+
Photostat::Plugins.all.each_key do |cmd_name|
|
76
|
+
cmd_obj = Photostat::Plugins.all[cmd_name]
|
77
|
+
cmd_obj.exposes.each do |name|
|
78
|
+
msg = " #{cmd_obj.plugin_name}:#{name}"
|
79
|
+
if cmd_obj.exposes_help[name]
|
80
|
+
msg += "\t- " + cmd_obj.exposes_help[name]
|
81
|
+
end
|
82
|
+
puts msg
|
83
|
+
end
|
84
|
+
end
|
85
|
+
puts
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
Photostat::Plugins.load_all!
|
data/photostat.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "photostat/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "photostat"
|
7
|
+
s.version = Photostat::VERSION
|
8
|
+
s.authors = ["Alexandru Nedelcu"]
|
9
|
+
s.email = ["me@alexn.org"]
|
10
|
+
s.homepage = "http://github.com/alexandru/photostat"
|
11
|
+
s.summary = %q{Managing Photos For Hackers}
|
12
|
+
s.description = %q{Photostat is a collection of command-line utilities for managing photos / syncronizing with Flickr - first version doesnt do much, it just helps me organize my files and upload them to Flickr}
|
13
|
+
|
14
|
+
s.rubyforge_project = "photostat"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
s.add_development_dependency "bundler"
|
22
|
+
s.add_development_dependency "rake"
|
23
|
+
|
24
|
+
s.add_runtime_dependency 'sqlite3'
|
25
|
+
s.add_runtime_dependency "trollop"
|
26
|
+
s.add_runtime_dependency "escape"
|
27
|
+
s.add_runtime_dependency "flickraw"
|
28
|
+
s.add_runtime_dependency "exifr"
|
29
|
+
s.add_runtime_dependency "json"
|
30
|
+
end
|
metadata
ADDED
@@ -0,0 +1,155 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: photostat
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Alexandru Nedelcu
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-10-29 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: bundler
|
16
|
+
requirement: &74575380 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *74575380
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: rake
|
27
|
+
requirement: &74575170 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *74575170
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: sqlite3
|
38
|
+
requirement: &74574890 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :runtime
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *74574890
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: trollop
|
49
|
+
requirement: &74574610 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :runtime
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *74574610
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: escape
|
60
|
+
requirement: &74574400 !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ! '>='
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
type: :runtime
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: *74574400
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: flickraw
|
71
|
+
requirement: &74574180 !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ! '>='
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
type: :runtime
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: *74574180
|
80
|
+
- !ruby/object:Gem::Dependency
|
81
|
+
name: exifr
|
82
|
+
requirement: &74573940 !ruby/object:Gem::Requirement
|
83
|
+
none: false
|
84
|
+
requirements:
|
85
|
+
- - ! '>='
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
type: :runtime
|
89
|
+
prerelease: false
|
90
|
+
version_requirements: *74573940
|
91
|
+
- !ruby/object:Gem::Dependency
|
92
|
+
name: json
|
93
|
+
requirement: &74573730 !ruby/object:Gem::Requirement
|
94
|
+
none: false
|
95
|
+
requirements:
|
96
|
+
- - ! '>='
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
type: :runtime
|
100
|
+
prerelease: false
|
101
|
+
version_requirements: *74573730
|
102
|
+
description: Photostat is a collection of command-line utilities for managing photos
|
103
|
+
/ syncronizing with Flickr - first version doesnt do much, it just helps me organize
|
104
|
+
my files and upload them to Flickr
|
105
|
+
email:
|
106
|
+
- me@alexn.org
|
107
|
+
executables:
|
108
|
+
- photostat
|
109
|
+
extensions: []
|
110
|
+
extra_rdoc_files: []
|
111
|
+
files:
|
112
|
+
- .gitignore
|
113
|
+
- Gemfile
|
114
|
+
- README.md
|
115
|
+
- Rakefile
|
116
|
+
- bin/photostat
|
117
|
+
- junk/filehash.rb
|
118
|
+
- junk/flickr-sync-old.rb
|
119
|
+
- junk/rack-sample.rb
|
120
|
+
- junk/trash.rb
|
121
|
+
- lib/photostat.rb
|
122
|
+
- lib/photostat/db/001_create_photos_with_tags.rb
|
123
|
+
- lib/photostat/db/002_photos_index.rb
|
124
|
+
- lib/photostat/db/003_photos_add_flickr_info.rb
|
125
|
+
- lib/photostat/db/base.rb
|
126
|
+
- lib/photostat/plugins/00_base.rb
|
127
|
+
- lib/photostat/plugins/01_local.rb
|
128
|
+
- lib/photostat/plugins/02_flickr.rb
|
129
|
+
- lib/photostat/utils/os.rb
|
130
|
+
- photostat.gemspec
|
131
|
+
homepage: http://github.com/alexandru/photostat
|
132
|
+
licenses: []
|
133
|
+
post_install_message:
|
134
|
+
rdoc_options: []
|
135
|
+
require_paths:
|
136
|
+
- lib
|
137
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
138
|
+
none: false
|
139
|
+
requirements:
|
140
|
+
- - ! '>='
|
141
|
+
- !ruby/object:Gem::Version
|
142
|
+
version: '0'
|
143
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
144
|
+
none: false
|
145
|
+
requirements:
|
146
|
+
- - ! '>='
|
147
|
+
- !ruby/object:Gem::Version
|
148
|
+
version: '0'
|
149
|
+
requirements: []
|
150
|
+
rubyforge_project: photostat
|
151
|
+
rubygems_version: 1.8.6
|
152
|
+
signing_key:
|
153
|
+
specification_version: 3
|
154
|
+
summary: Managing Photos For Hackers
|
155
|
+
test_files: []
|