flickrup 1.1.0 → 1.1.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/bin/flickrup +1 -1
- data/lib/flickrup/ext/machine_tags.rb +3 -3
- data/lib/flickrup/ext/prefixed_extension.rb +1 -1
- data/lib/flickrup/ext/replacement_tags.rb +2 -2
- data/lib/flickrup/ext/tag_sets.rb +1 -1
- data/lib/flickrup/ext/visibility.rb +1 -1
- data/lib/flickrup/filetype/inifile.rb +568 -0
- data/lib/flickrup/filetype/picasa_ini.rb +29 -0
- data/lib/flickrup/filetype/tagged_file.rb +81 -0
- data/lib/flickrup/filetype/tagged_image.rb +43 -0
- data/lib/flickrup/filetype/tagged_video.rb +38 -0
- data/lib/flickrup/flickr_client.rb +3 -1
- data/lib/flickrup/processing_context.rb +4 -4
- data/lib/flickrup/processor.rb +6 -6
- data/lib/flickrup/slow_listener.rb +1 -1
- data/lib/flickrup/uploader.rb +6 -6
- data/lib/flickrup.rb +1 -1
- metadata +24 -4
- data/lib/flickrup/tagged_image.rb +0 -71
data/bin/flickrup
CHANGED
@@ -5,7 +5,7 @@ class MachineTags
|
|
5
5
|
|
6
6
|
def preupload(ctx)
|
7
7
|
|
8
|
-
tags = ctx.
|
8
|
+
tags = ctx.file.tags
|
9
9
|
|
10
10
|
remapped = tags.map do |x|
|
11
11
|
x.sub(/^([^:]+):([^:]+)::/,"\\1:\\2=")
|
@@ -14,8 +14,8 @@ class MachineTags
|
|
14
14
|
|
15
15
|
|
16
16
|
unless tags == remapped #write back to file
|
17
|
-
ctx.
|
18
|
-
ctx.
|
17
|
+
ctx.file['Keywords'] = remapped
|
18
|
+
ctx.file['Subject'] = remapped #also write Subject XMP tag
|
19
19
|
end
|
20
20
|
|
21
21
|
ctx
|
@@ -9,11 +9,11 @@ class ReplacementTags
|
|
9
9
|
|
10
10
|
if replacements != nil
|
11
11
|
replacements.each do |tag_name, tag_value_replacements|
|
12
|
-
existing = ctx.
|
12
|
+
existing = ctx.file[tag_name]
|
13
13
|
|
14
14
|
tag_value_replacements.each do |key, value|
|
15
15
|
if existing == key
|
16
|
-
ctx.
|
16
|
+
ctx.file[tag_name] = value
|
17
17
|
break
|
18
18
|
end
|
19
19
|
end
|
@@ -16,7 +16,7 @@ class TagSets < PrefixedExtension
|
|
16
16
|
uploaded = @delegate.upload(ctx)
|
17
17
|
|
18
18
|
sets.each do |set|
|
19
|
-
logger.debug("Adding #{ctx.
|
19
|
+
logger.debug("Adding #{ctx.file.filename} to set: #{set}")
|
20
20
|
@flickr.add_to_set(uploaded, set)
|
21
21
|
end
|
22
22
|
end
|
@@ -12,7 +12,7 @@ class Visibility < PrefixedExtension
|
|
12
12
|
visibility = values(:visibilityprefix, ctx)
|
13
13
|
|
14
14
|
config_override = if visibility.length == 0
|
15
|
-
logger.debug("No visibility specified for #{ctx.
|
15
|
+
logger.debug("No visibility specified for #{ctx.file.filename}.")
|
16
16
|
{}
|
17
17
|
elsif visibility.length == 1
|
18
18
|
config_for_visibility(visibility[0])
|
@@ -0,0 +1,568 @@
|
|
1
|
+
#encoding: UTF-8
|
2
|
+
require 'strscan'
|
3
|
+
|
4
|
+
# This class represents the INI file and can be used to parse, modify,
|
5
|
+
# and write INI files.
|
6
|
+
#
|
7
|
+
class IniFile
|
8
|
+
include Enumerable
|
9
|
+
|
10
|
+
class Error < StandardError; end
|
11
|
+
VERSION = '2.0.2'
|
12
|
+
|
13
|
+
# Public: Open an INI file and load the contents.
|
14
|
+
#
|
15
|
+
# filename - The name of the fiel as a String
|
16
|
+
# opts - The Hash of options (default: {})
|
17
|
+
# :comment - String containing the comment character(s)
|
18
|
+
# :parameter - String used to separate parameter and value
|
19
|
+
# :encoding - Encoding String for reading / writing (Ruby 1.9)
|
20
|
+
# :escape - Boolean used to control character escaping
|
21
|
+
# :default - The String name of the default global section
|
22
|
+
#
|
23
|
+
# Examples
|
24
|
+
#
|
25
|
+
# IniFile.load('file.ini')
|
26
|
+
# #=> IniFile instance
|
27
|
+
#
|
28
|
+
# IniFile.load('does/not/exist.ini')
|
29
|
+
# #=> nil
|
30
|
+
#
|
31
|
+
# Returns an IniFile intsnace or nil if the file could not be opened.
|
32
|
+
#
|
33
|
+
def self.load( filename, opts = {} )
|
34
|
+
return unless File.file? filename
|
35
|
+
new(opts.merge(:filename => filename))
|
36
|
+
end
|
37
|
+
|
38
|
+
# Get and set the filename
|
39
|
+
attr_accessor :filename
|
40
|
+
|
41
|
+
# Get and set the encoding (Ruby 1.9)
|
42
|
+
attr_accessor :encoding
|
43
|
+
|
44
|
+
# Enable or disable character escaping
|
45
|
+
attr_accessor :escape
|
46
|
+
|
47
|
+
# Public: Create a new INI file from the given content String which
|
48
|
+
# contains the INI file lines. If the content are omitted, then the
|
49
|
+
# :filename option is used to read in the content of the INI file. If
|
50
|
+
# neither the content for a filename is provided then an empty INI file is
|
51
|
+
# created.
|
52
|
+
#
|
53
|
+
# content - The String containing the INI file contents
|
54
|
+
# opts - The Hash of options (default: {})
|
55
|
+
# :comment - String containing the comment character(s)
|
56
|
+
# :parameter - String used to separate parameter and value
|
57
|
+
# :encoding - Encoding String for reading / writing (Ruby 1.9)
|
58
|
+
# :escape - Boolean used to control character escaping
|
59
|
+
# :default - The String name of the default global section
|
60
|
+
# :filename - The filename as a String
|
61
|
+
#
|
62
|
+
# Examples
|
63
|
+
#
|
64
|
+
# IniFile.new
|
65
|
+
# #=> an empty IniFile instance
|
66
|
+
#
|
67
|
+
# IniFile.new( "[global]\nfoo=bar" )
|
68
|
+
# #=> an IniFile instance
|
69
|
+
#
|
70
|
+
# IniFile.new( :filename => 'file.ini', :encoding => 'UTF-8' )
|
71
|
+
# #=> an IniFile instance
|
72
|
+
#
|
73
|
+
# IniFile.new( "[global]\nfoo=bar", :comment => '#' )
|
74
|
+
# #=> an IniFile instance
|
75
|
+
#
|
76
|
+
def initialize( content = nil, opts = {} )
|
77
|
+
opts, content = content, nil if Hash === content
|
78
|
+
|
79
|
+
@content = content
|
80
|
+
|
81
|
+
@comment = opts.fetch(:comment, ';#')
|
82
|
+
@param = opts.fetch(:parameter, '=')
|
83
|
+
@encoding = opts.fetch(:encoding, nil)
|
84
|
+
@escape = opts.fetch(:escape, true)
|
85
|
+
@default = opts.fetch(:default, 'global')
|
86
|
+
@filename = opts.fetch(:filename, nil)
|
87
|
+
|
88
|
+
@ini = Hash.new {|h,k| h[k] = Hash.new}
|
89
|
+
|
90
|
+
if @content then parse!
|
91
|
+
elsif @filename then read
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# Public: Write the contents of this IniFile to the file system. If left
|
96
|
+
# unspecified, the currently configured filename and encoding will be used.
|
97
|
+
# Otherwise the filename and encoding can be specified in the options hash.
|
98
|
+
#
|
99
|
+
# opts - The default options Hash
|
100
|
+
# :filename - The filename as a String
|
101
|
+
# :encoding - The encoding as a String (Ruby 1.9)
|
102
|
+
#
|
103
|
+
# Returns this IniFile instance.
|
104
|
+
#
|
105
|
+
def write( opts = {} )
|
106
|
+
filename = opts.fetch(:filename, @filename)
|
107
|
+
encoding = opts.fetch(:encoding, @encoding)
|
108
|
+
mode = (RUBY_VERSION >= '1.9' && encoding) ?
|
109
|
+
"w:#{encoding.to_s}" :
|
110
|
+
'w'
|
111
|
+
|
112
|
+
File.open(filename, mode) do |f|
|
113
|
+
@ini.each do |section,hash|
|
114
|
+
f.puts "[#{section}]"
|
115
|
+
hash.each {|param,val| f.puts "#{param} #{@param} #{escape_value val}"}
|
116
|
+
f.puts
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
self
|
121
|
+
end
|
122
|
+
alias :save :write
|
123
|
+
|
124
|
+
# Public: Read the contents of the INI file from the file system and replace
|
125
|
+
# and set the state of this IniFile instance. If left unspecified the
|
126
|
+
# currently configured filename and encoding will be used when reading from
|
127
|
+
# the file system. Otherwise the filename and encoding can be specified in
|
128
|
+
# the options hash.
|
129
|
+
#
|
130
|
+
# opts - The default options Hash
|
131
|
+
# :filename - The filename as a String
|
132
|
+
# :encoding - The encoding as a String (Ruby 1.9)
|
133
|
+
#
|
134
|
+
# Returns this IniFile instance if the read was successful; nil is returned
|
135
|
+
# if the file could not be read.
|
136
|
+
#
|
137
|
+
def read( opts = {} )
|
138
|
+
filename = opts.fetch(:filename, @filename)
|
139
|
+
encoding = opts.fetch(:encoding, @encoding)
|
140
|
+
return unless File.file? filename
|
141
|
+
|
142
|
+
mode = (RUBY_VERSION >= '1.9' && encoding) ?
|
143
|
+
"r:#{encoding.to_s}" :
|
144
|
+
'r'
|
145
|
+
fd = File.open(filename, mode)
|
146
|
+
@content = fd.read
|
147
|
+
|
148
|
+
parse!
|
149
|
+
self
|
150
|
+
ensure
|
151
|
+
fd.close if fd && !fd.closed?
|
152
|
+
end
|
153
|
+
alias :restore :read
|
154
|
+
|
155
|
+
# Returns this IniFile converted to a String.
|
156
|
+
#
|
157
|
+
def to_s
|
158
|
+
s = []
|
159
|
+
@ini.each do |section,hash|
|
160
|
+
s << "[#{section}]"
|
161
|
+
hash.each {|param,val| s << "#{param} #{@param} #{escape_value val}"}
|
162
|
+
s << ""
|
163
|
+
end
|
164
|
+
s.join("\n")
|
165
|
+
end
|
166
|
+
|
167
|
+
# Returns this IniFile converted to a Hash.
|
168
|
+
#
|
169
|
+
def to_h
|
170
|
+
@ini.dup
|
171
|
+
end
|
172
|
+
|
173
|
+
# Public: Creates a copy of this inifile with the entries from the
|
174
|
+
# other_inifile merged into the copy.
|
175
|
+
#
|
176
|
+
# other - The other IniFile.
|
177
|
+
#
|
178
|
+
# Returns a new IniFile.
|
179
|
+
#
|
180
|
+
def merge( other )
|
181
|
+
self.dup.merge!(other)
|
182
|
+
end
|
183
|
+
|
184
|
+
# Public: Merges other_inifile into this inifile, overwriting existing
|
185
|
+
# entries. Useful for having a system inifile with user over-ridable settings
|
186
|
+
# elsewhere.
|
187
|
+
#
|
188
|
+
# other - The other IniFile.
|
189
|
+
#
|
190
|
+
# Returns this IniFile.
|
191
|
+
#
|
192
|
+
def merge!( other )
|
193
|
+
my_keys = @ini.keys
|
194
|
+
other_keys =
|
195
|
+
case other
|
196
|
+
when IniFile; other.instance_variable_get(:@ini).keys
|
197
|
+
when Hash; other.keys
|
198
|
+
else raise "cannot merge contents from '#{other.class.name}'" end
|
199
|
+
|
200
|
+
(my_keys & other_keys).each do |key|
|
201
|
+
@ini[key].merge!(other[key])
|
202
|
+
end
|
203
|
+
|
204
|
+
(other_keys - my_keys).each do |key|
|
205
|
+
@ini[key] = other[key]
|
206
|
+
end
|
207
|
+
|
208
|
+
self
|
209
|
+
end
|
210
|
+
|
211
|
+
# Public: Yield each INI file section, parameter, and value in turn to the
|
212
|
+
# given block.
|
213
|
+
#
|
214
|
+
# block - The block that will be iterated by the each method. The block will
|
215
|
+
# be passed the current section and the parameter / value pair.
|
216
|
+
#
|
217
|
+
# Examples
|
218
|
+
#
|
219
|
+
# inifile.each do |section, parameter, value|
|
220
|
+
# puts "#{parameter} = #{value} [in section - #{section}]"
|
221
|
+
# end
|
222
|
+
#
|
223
|
+
# Returns this IniFile.
|
224
|
+
#
|
225
|
+
def each
|
226
|
+
return unless block_given?
|
227
|
+
@ini.each do |section,hash|
|
228
|
+
hash.each do |param,val|
|
229
|
+
yield section, param, val
|
230
|
+
end
|
231
|
+
end
|
232
|
+
self
|
233
|
+
end
|
234
|
+
|
235
|
+
# Public: Yield each section in turn to the given block.
|
236
|
+
#
|
237
|
+
# block - The block that will be iterated by the each method. The block will
|
238
|
+
# be passed the current section as a Hash.
|
239
|
+
#
|
240
|
+
# Examples
|
241
|
+
#
|
242
|
+
# inifile.each_section do |section|
|
243
|
+
# puts section.inspect
|
244
|
+
# end
|
245
|
+
#
|
246
|
+
# Returns this IniFile.
|
247
|
+
#
|
248
|
+
def each_section
|
249
|
+
return unless block_given?
|
250
|
+
@ini.each_key {|section| yield section}
|
251
|
+
self
|
252
|
+
end
|
253
|
+
|
254
|
+
# Public: Remove a section identified by name from the IniFile.
|
255
|
+
#
|
256
|
+
# section - The section name as a String.
|
257
|
+
#
|
258
|
+
# Returns the deleted section Hash.
|
259
|
+
#
|
260
|
+
def delete_section( section )
|
261
|
+
@ini.delete section.to_s
|
262
|
+
end
|
263
|
+
|
264
|
+
# Public: Get the section Hash by name. If the section does not exist, then
|
265
|
+
# it will be created.
|
266
|
+
#
|
267
|
+
# section - The section name as a String.
|
268
|
+
#
|
269
|
+
# Examples
|
270
|
+
#
|
271
|
+
# inifile['global']
|
272
|
+
# #=> global section Hash
|
273
|
+
#
|
274
|
+
# Returns the Hash of parameter/value pairs for this section.
|
275
|
+
#
|
276
|
+
def []( section )
|
277
|
+
return nil if section.nil?
|
278
|
+
@ini[section.to_s]
|
279
|
+
end
|
280
|
+
|
281
|
+
# Public: Set the section to a hash of parameter/value pairs.
|
282
|
+
#
|
283
|
+
# section - The section name as a String.
|
284
|
+
# value - The Hash of parameter/value pairs.
|
285
|
+
#
|
286
|
+
# Examples
|
287
|
+
#
|
288
|
+
# inifile['tenderloin'] = { 'gritty' => 'yes' }
|
289
|
+
# #=> { 'gritty' => 'yes' }
|
290
|
+
#
|
291
|
+
# Returns the value Hash.
|
292
|
+
#
|
293
|
+
def []=( section, value )
|
294
|
+
@ini[section.to_s] = value
|
295
|
+
end
|
296
|
+
|
297
|
+
# Public: Create a Hash containing only those INI file sections whose names
|
298
|
+
# match the given regular expression.
|
299
|
+
#
|
300
|
+
# regex - The Regexp used to match section names.
|
301
|
+
#
|
302
|
+
# Examples
|
303
|
+
#
|
304
|
+
# inifile.match(/^tree_/)
|
305
|
+
# #=> Hash of matching sections
|
306
|
+
#
|
307
|
+
# Return a Hash containing only those sections that match the given regular
|
308
|
+
# expression.
|
309
|
+
#
|
310
|
+
def match( regex )
|
311
|
+
@ini.dup.delete_if { |section, _| section !~ regex }
|
312
|
+
end
|
313
|
+
|
314
|
+
# Public: Check to see if the IniFile contains the section.
|
315
|
+
#
|
316
|
+
# section - The section name as a String.
|
317
|
+
#
|
318
|
+
# Returns true if the section exists in the IniFile.
|
319
|
+
#
|
320
|
+
def has_section?( section )
|
321
|
+
@ini.has_key? section.to_s
|
322
|
+
end
|
323
|
+
|
324
|
+
# Returns an Array of section names contained in this IniFile.
|
325
|
+
#
|
326
|
+
def sections
|
327
|
+
@ini.keys
|
328
|
+
end
|
329
|
+
|
330
|
+
# Public: Freeze the state of this IniFile object. Any attempts to change
|
331
|
+
# the object will raise an error.
|
332
|
+
#
|
333
|
+
# Returns this IniFile.
|
334
|
+
#
|
335
|
+
def freeze
|
336
|
+
super
|
337
|
+
@ini.each_value {|h| h.freeze}
|
338
|
+
@ini.freeze
|
339
|
+
self
|
340
|
+
end
|
341
|
+
|
342
|
+
# Public: Mark this IniFile as tainted -- this will traverse each section
|
343
|
+
# marking each as tainted.
|
344
|
+
#
|
345
|
+
# Returns this IniFile.
|
346
|
+
#
|
347
|
+
def taint
|
348
|
+
super
|
349
|
+
@ini.each_value {|h| h.taint}
|
350
|
+
@ini.taint
|
351
|
+
self
|
352
|
+
end
|
353
|
+
|
354
|
+
# Public: Produces a duplicate of this IniFile. The duplicate is independent
|
355
|
+
# of the original -- i.e. the duplicate can be modified without changing the
|
356
|
+
# original. The tainted state of the original is copied to the duplicate.
|
357
|
+
#
|
358
|
+
# Returns a new IniFile.
|
359
|
+
#
|
360
|
+
def dup
|
361
|
+
other = super
|
362
|
+
other.instance_variable_set(:@ini, Hash.new {|h,k| h[k] = Hash.new})
|
363
|
+
@ini.each_pair {|s,h| other[s].merge! h}
|
364
|
+
other.taint if self.tainted?
|
365
|
+
other
|
366
|
+
end
|
367
|
+
|
368
|
+
# Public: Produces a duplicate of this IniFile. The duplicate is independent
|
369
|
+
# of the original -- i.e. the duplicate can be modified without changing the
|
370
|
+
# original. The tainted state and the frozen state of the original is copied
|
371
|
+
# to the duplicate.
|
372
|
+
#
|
373
|
+
# Returns a new IniFile.
|
374
|
+
#
|
375
|
+
def clone
|
376
|
+
other = dup
|
377
|
+
other.freeze if self.frozen?
|
378
|
+
other
|
379
|
+
end
|
380
|
+
|
381
|
+
# Public: Compare this IniFile to some other IniFile. For two INI files to
|
382
|
+
# be equivalent, they must have the same sections with the same parameter /
|
383
|
+
# value pairs in each section.
|
384
|
+
#
|
385
|
+
# other - The other IniFile.
|
386
|
+
#
|
387
|
+
# Returns true if the INI files are equivalent and false if they differ.
|
388
|
+
#
|
389
|
+
def eql?( other )
|
390
|
+
return true if equal? other
|
391
|
+
return false unless other.instance_of? self.class
|
392
|
+
@ini == other.instance_variable_get(:@ini)
|
393
|
+
end
|
394
|
+
alias :== :eql?
|
395
|
+
|
396
|
+
|
397
|
+
private
|
398
|
+
|
399
|
+
# Parse the ini file contents. This will clear any values currently stored
|
400
|
+
# in the ini hash.
|
401
|
+
#
|
402
|
+
def parse!
|
403
|
+
return unless @content
|
404
|
+
|
405
|
+
string = ''
|
406
|
+
property = ''
|
407
|
+
|
408
|
+
@ini.clear
|
409
|
+
@_line = nil
|
410
|
+
@_section = nil
|
411
|
+
|
412
|
+
scanner = StringScanner.new(@content)
|
413
|
+
until scanner.eos?
|
414
|
+
|
415
|
+
# keep track of the current line for error messages
|
416
|
+
@_line = scanner.check(%r/\A.*$/) if scanner.bol?
|
417
|
+
|
418
|
+
# look for escaped special characters \# \" etc
|
419
|
+
if @escape && scanner.scan(%r/\\([\[\]#{@param}#{@comment}"])/)
|
420
|
+
string << scanner[1]
|
421
|
+
|
422
|
+
# look for quoted strings
|
423
|
+
elsif scanner.scan(%r/"/)
|
424
|
+
quote = scanner.scan_until(/(?:\A|[^\\])"/)
|
425
|
+
parse_error('Unmatched quote') if quote.nil?
|
426
|
+
|
427
|
+
quote.chomp!('"')
|
428
|
+
string << quote
|
429
|
+
|
430
|
+
# look for comments, empty strings, end of lines
|
431
|
+
elsif scanner.skip(%r/\A\s*(?:[#{@comment}].*)?$/)
|
432
|
+
string << scanner.getch unless scanner.eos?
|
433
|
+
|
434
|
+
process_property(property, string)
|
435
|
+
|
436
|
+
# look for the separator between property name and value
|
437
|
+
elsif scanner.scan(%r/#{@param}/)
|
438
|
+
if property.empty?
|
439
|
+
property = string.strip
|
440
|
+
string.slice!(0, string.length)
|
441
|
+
elsif !@escape
|
442
|
+
scanner.pos = scanner.pos - @param.length
|
443
|
+
string << read_to_next_token(scanner, true)
|
444
|
+
else
|
445
|
+
parse_error
|
446
|
+
end
|
447
|
+
|
448
|
+
# look for the start of a new section
|
449
|
+
elsif scanner.scan(%r/\A\s*\[([^\]]+)\]/)
|
450
|
+
@_section = @ini[scanner[1]]
|
451
|
+
|
452
|
+
# otherwise scan and store characters till we hit the start of some
|
453
|
+
# special section like a quote, newline, comment, etc.
|
454
|
+
else
|
455
|
+
string << read_to_next_token(scanner, false)
|
456
|
+
end
|
457
|
+
|
458
|
+
def read_to_next_token(scanner, read_value)
|
459
|
+
|
460
|
+
scan_regex = if read_value
|
461
|
+
%r/([\n"] | \z | \\[\[\]#{@param}#{@comment}"])/mx
|
462
|
+
elsif @escape then
|
463
|
+
%r/([\n"#{@param}#{@comment}] | \z | \\[\[\]#{@param}#{@comment}"])/mx
|
464
|
+
else
|
465
|
+
%r/([\n"#{@param}#{@comment}] | \z)/mx
|
466
|
+
end
|
467
|
+
|
468
|
+
tmp = scanner.scan_until(scan_regex)
|
469
|
+
parse_error if tmp.nil?
|
470
|
+
|
471
|
+
len = scanner[1].length
|
472
|
+
tmp.slice!(tmp.length - len, len)
|
473
|
+
|
474
|
+
scanner.pos = scanner.pos - len
|
475
|
+
|
476
|
+
tmp
|
477
|
+
end
|
478
|
+
end
|
479
|
+
|
480
|
+
process_property(property, string)
|
481
|
+
end
|
482
|
+
|
483
|
+
# Store the property / value pair in the currently active section. This
|
484
|
+
# method checks for continuation of the value to the next line.
|
485
|
+
#
|
486
|
+
# property - The property name as a String.
|
487
|
+
# value - The property value as a String.
|
488
|
+
#
|
489
|
+
# Returns nil.
|
490
|
+
#
|
491
|
+
def process_property( property, value )
|
492
|
+
value.chomp!
|
493
|
+
return if property.empty? and value.empty?
|
494
|
+
return if value.sub!(%r/\\\s*\z/, '')
|
495
|
+
|
496
|
+
property.strip!
|
497
|
+
value.strip!
|
498
|
+
|
499
|
+
parse_error if property.empty?
|
500
|
+
|
501
|
+
current_section[property.dup] = unescape_value(value.dup)
|
502
|
+
|
503
|
+
property.slice!(0, property.length)
|
504
|
+
value.slice!(0, value.length)
|
505
|
+
|
506
|
+
nil
|
507
|
+
end
|
508
|
+
|
509
|
+
# Returns the current section Hash.
|
510
|
+
#
|
511
|
+
def current_section
|
512
|
+
@_section ||= @ini[@default]
|
513
|
+
end
|
514
|
+
|
515
|
+
# Raise a parse error using the given message and appending the current line
|
516
|
+
# being parsed.
|
517
|
+
#
|
518
|
+
# msg - The message String to use.
|
519
|
+
#
|
520
|
+
# Raises IniFile::Error
|
521
|
+
#
|
522
|
+
def parse_error( msg = 'Could not parse line' )
|
523
|
+
raise Error, "#{msg}: #{@_line.inspect}"
|
524
|
+
end
|
525
|
+
|
526
|
+
# Unescape special characters found in the value string. This will convert
|
527
|
+
# escaped null, tab, carriage return, newline, and backslash into their
|
528
|
+
# literal equivalents.
|
529
|
+
#
|
530
|
+
# value - The String value to unescape.
|
531
|
+
#
|
532
|
+
# Returns the unescaped value.
|
533
|
+
#
|
534
|
+
def unescape_value( value )
|
535
|
+
return value unless @escape
|
536
|
+
|
537
|
+
value = value.to_s
|
538
|
+
value.gsub!(%r/\\[0nrt\\]/) { |char|
|
539
|
+
case char
|
540
|
+
when '\0'; "\0"
|
541
|
+
when '\n'; "\n"
|
542
|
+
when '\r'; "\r"
|
543
|
+
when '\t'; "\t"
|
544
|
+
when '\\\\'; "\\"
|
545
|
+
end
|
546
|
+
}
|
547
|
+
value
|
548
|
+
end
|
549
|
+
|
550
|
+
# Escape special characters.
|
551
|
+
#
|
552
|
+
# value - The String value to escape.
|
553
|
+
#
|
554
|
+
# Returns the escaped value.
|
555
|
+
#
|
556
|
+
def escape_value( value )
|
557
|
+
return value unless @escape
|
558
|
+
|
559
|
+
value = value.to_s.dup
|
560
|
+
value.gsub!(%r/\\([0nrt])/, '\\\\\1')
|
561
|
+
value.gsub!(%r/\n/, '\n')
|
562
|
+
value.gsub!(%r/\r/, '\r')
|
563
|
+
value.gsub!(%r/\t/, '\t')
|
564
|
+
value.gsub!(%r/\0/, '\0')
|
565
|
+
value
|
566
|
+
end
|
567
|
+
|
568
|
+
end # IniFile
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'flickrup/filetype/inifile'
|
2
|
+
|
3
|
+
class PicasaIni
|
4
|
+
|
5
|
+
@@cached_mtime = DateTime.new(1900,1,1)
|
6
|
+
|
7
|
+
def self.open(file)
|
8
|
+
dir = File.dirname(file)
|
9
|
+
inifile = "#{dir}/.picasa.ini"
|
10
|
+
mtime = File.mtime(inifile)
|
11
|
+
|
12
|
+
if @@cached_mtime == mtime then
|
13
|
+
@@cached
|
14
|
+
else
|
15
|
+
@@cached_mtime = mtime
|
16
|
+
@@cached = new(inifile)
|
17
|
+
end
|
18
|
+
|
19
|
+
@@cached[File.basename(file)]
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(file)
|
23
|
+
@inifile = IniFile.load(file, :escape => false)
|
24
|
+
end
|
25
|
+
|
26
|
+
def [](section)
|
27
|
+
@inifile[section]
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'mini_exiftool'
|
2
|
+
|
3
|
+
class TaggedFile
|
4
|
+
|
5
|
+
@@subclasses = {}
|
6
|
+
|
7
|
+
def self.register_reader(exts)
|
8
|
+
exts.map do |ext|
|
9
|
+
@@subclasses[ext] = self
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.create(file)
|
14
|
+
c = @@subclasses[File.extname(file)[1..-1]]
|
15
|
+
|
16
|
+
if c
|
17
|
+
c.new(file)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize(filename)
|
22
|
+
@parsed = MiniExiftool.new filename
|
23
|
+
end
|
24
|
+
|
25
|
+
def doUpload(preupload_handlers, uploader)
|
26
|
+
|
27
|
+
context = preupload_handlers.reduce(ProcessingContext.new(self)) do |ctx, handler|
|
28
|
+
handler.preupload(ctx)
|
29
|
+
end
|
30
|
+
|
31
|
+
pre_upload
|
32
|
+
uploader.upload(context)
|
33
|
+
post_upload
|
34
|
+
end
|
35
|
+
|
36
|
+
def tags
|
37
|
+
keywords = self['keywords']
|
38
|
+
TagSet.new(if keywords == nil
|
39
|
+
[]
|
40
|
+
elsif keywords.class == String
|
41
|
+
[keywords]
|
42
|
+
else
|
43
|
+
keywords
|
44
|
+
end)
|
45
|
+
end
|
46
|
+
|
47
|
+
def archive(to_dir)
|
48
|
+
date_taken = @parsed.date_time_original || @parsed.modifydate
|
49
|
+
archive_by_date(to_dir, date_taken)
|
50
|
+
end
|
51
|
+
|
52
|
+
def pre_upload
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
def post_upload
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def archive_by_date(to_dir, date)
|
63
|
+
archive_file_by_date(@filename, to_dir, date)
|
64
|
+
end
|
65
|
+
|
66
|
+
def archive_file_by_date(file, to_dir, date)
|
67
|
+
target_dir = File.join(
|
68
|
+
to_dir,
|
69
|
+
date.strftime("%Y"),
|
70
|
+
date.strftime("%b")
|
71
|
+
)
|
72
|
+
|
73
|
+
FileUtils.mkdir_p(target_dir)
|
74
|
+
FileUtils.mv(file, File.join(target_dir, File.basename(file)))
|
75
|
+
logger.info("Archived #{File.basename(file)} to #{target_dir}")
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
require 'flickrup/filetype/tagged_image'
|
81
|
+
require 'flickrup/filetype/tagged_video'
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require "flickrup/logging"
|
3
|
+
require "flickrup/tag_set"
|
4
|
+
require "flickrup/processing_context"
|
5
|
+
|
6
|
+
class TaggedImage < TaggedFile
|
7
|
+
include Logging
|
8
|
+
|
9
|
+
attr_reader :filename
|
10
|
+
|
11
|
+
def initialize(filename)
|
12
|
+
super
|
13
|
+
@filename = filename
|
14
|
+
end
|
15
|
+
|
16
|
+
def pre_upload
|
17
|
+
super
|
18
|
+
#maybe save afterwards
|
19
|
+
if @parsed.changed?
|
20
|
+
@parsed.save!
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def post_upload
|
25
|
+
super
|
26
|
+
#maybe save afterwards
|
27
|
+
if @parsed.changed?
|
28
|
+
@parsed.save!
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns the value of a tag.
|
33
|
+
def [] tag
|
34
|
+
@parsed[tag]
|
35
|
+
end
|
36
|
+
|
37
|
+
# Set the value of a tag.
|
38
|
+
def []=(tag, val)
|
39
|
+
@parsed[tag] = val
|
40
|
+
end
|
41
|
+
|
42
|
+
register_reader(%w(jpg JPG jpeg JPEG))
|
43
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'flickrup/filetype/picasa_ini'
|
3
|
+
|
4
|
+
class TaggedVideo < TaggedFile
|
5
|
+
include Logging
|
6
|
+
|
7
|
+
attr_reader :filename
|
8
|
+
|
9
|
+
def initialize(filename)
|
10
|
+
super
|
11
|
+
@filename = filename
|
12
|
+
@properties = PicasaIni.open(filename)
|
13
|
+
end
|
14
|
+
|
15
|
+
def archive(to_dir)
|
16
|
+
super
|
17
|
+
write_sidecar(File.join(target_dir, "#{File.basename(file)}.meta"))
|
18
|
+
end
|
19
|
+
|
20
|
+
def write_sidecar(to_file)
|
21
|
+
File.open(to_file, "w") do |f|
|
22
|
+
f.write({:keywords => keywords}.to_yaml)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Returns the value of a tag.
|
27
|
+
def [] tag
|
28
|
+
@properties[tag]
|
29
|
+
end
|
30
|
+
|
31
|
+
# Set the value of a tag.
|
32
|
+
def []=(tag, val)
|
33
|
+
@properties[tag] = val
|
34
|
+
end
|
35
|
+
|
36
|
+
register_reader(%w(mp4 mts))
|
37
|
+
|
38
|
+
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require "flickraw"
|
2
|
+
require 'etc'
|
2
3
|
|
3
4
|
class FlickrClient
|
4
5
|
|
@@ -7,7 +8,8 @@ class FlickrClient
|
|
7
8
|
FlickRaw.api_key= api_key
|
8
9
|
FlickRaw.shared_secret=shared_secret
|
9
10
|
|
10
|
-
|
11
|
+
home = ENV['HOME'] || Etc.getpwuid.dir
|
12
|
+
@token_file = File.join(home, ".flickrup")
|
11
13
|
set_client(FlickRaw::Flickr.new)
|
12
14
|
|
13
15
|
if File.exist?(@token_file)
|
@@ -1,13 +1,13 @@
|
|
1
1
|
class ProcessingContext
|
2
|
-
attr_reader :
|
2
|
+
attr_reader :file
|
3
3
|
attr_reader :upload_properties
|
4
4
|
|
5
|
-
def initialize(
|
6
|
-
@
|
5
|
+
def initialize(file, props = {})
|
6
|
+
@file = file
|
7
7
|
@upload_properties = props
|
8
8
|
end
|
9
9
|
|
10
10
|
def with_properties(props)
|
11
|
-
ProcessingContext.new(self.
|
11
|
+
ProcessingContext.new(self.file, self.upload_properties.merge(props))
|
12
12
|
end
|
13
13
|
end
|
data/lib/flickrup/processor.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
require "flickrup/
|
1
|
+
require "flickrup/filetype/tagged_file"
|
2
2
|
require "flickrup/uploader"
|
3
3
|
require "flickrup/logging"
|
4
4
|
require "flickrup/ext/tag_sets"
|
@@ -17,9 +17,9 @@ class Processor
|
|
17
17
|
Dir["#{@config[:watch_dir]}/paused"].length > 0
|
18
18
|
end
|
19
19
|
|
20
|
-
def run(
|
20
|
+
def run(all_files = Dir["#{@config[:watch_dir]}/*.*"])
|
21
21
|
|
22
|
-
logger.debug("Processing #{
|
22
|
+
logger.debug("Processing #{all_files.length} files")
|
23
23
|
|
24
24
|
if paused
|
25
25
|
logger.debug("Paused detected, so skipping processing...")
|
@@ -27,15 +27,15 @@ class Processor
|
|
27
27
|
end
|
28
28
|
|
29
29
|
# some files may have been removed by now: https://bitbucket.org/jgilbert/flickrup/issue/1
|
30
|
-
|
30
|
+
files = all_files.map { |x|
|
31
31
|
if File.exist? x
|
32
|
-
|
32
|
+
TaggedFile.create(x)
|
33
33
|
else
|
34
34
|
nil
|
35
35
|
end
|
36
36
|
}.compact
|
37
37
|
|
38
|
-
tagged =
|
38
|
+
tagged = files.select { |x|
|
39
39
|
!(x.tags.nil? || x.tags.empty?)
|
40
40
|
}.sort_by { |x|
|
41
41
|
[x.filename]
|
data/lib/flickrup/uploader.rb
CHANGED
@@ -17,15 +17,15 @@ class Uploader
|
|
17
17
|
def upload(ctx)
|
18
18
|
|
19
19
|
if ctx.upload_properties[UploaderConstants::NO_UPLOAD]
|
20
|
-
logger.info("Skipping upload of #{ctx.
|
20
|
+
logger.info("Skipping upload of #{ctx.file.filename} as marked as NO_UPLOAD")
|
21
21
|
else
|
22
|
-
logger.debug("Uploading #{ctx.
|
22
|
+
logger.debug("Uploading #{ctx.file.filename} with tags #{ctx.file.tags}")
|
23
23
|
|
24
24
|
begin
|
25
|
-
rv = @flickr.upload_photo(ctx.
|
26
|
-
logger.debug("Uploaded #{ctx.
|
25
|
+
rv = @flickr.upload_photo(ctx.file.filename, prepare_upload_properties(ctx))
|
26
|
+
logger.debug("Uploaded #{ctx.file.filename}")
|
27
27
|
rescue StandardError => err
|
28
|
-
logger.error("Failed to upload #{ctx.
|
28
|
+
logger.error("Failed to upload #{ctx.file.filename}: #{err}")
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
@@ -33,7 +33,7 @@ class Uploader
|
|
33
33
|
end
|
34
34
|
|
35
35
|
def prepare_upload_properties(ctx)
|
36
|
-
{:tags => ctx.
|
36
|
+
{:tags => ctx.file.tags.to_s}.merge(ctx.upload_properties)
|
37
37
|
end
|
38
38
|
|
39
39
|
end
|
data/lib/flickrup.rb
CHANGED
@@ -4,7 +4,7 @@ require "trollop"
|
|
4
4
|
require "logger"
|
5
5
|
require "flickrup/quick_listener"
|
6
6
|
require "flickrup/slow_listener"
|
7
|
-
require "flickrup/
|
7
|
+
require "flickrup/filetype/tagged_file"
|
8
8
|
require "flickrup/uploader"
|
9
9
|
require "flickrup/processor"
|
10
10
|
require "flickrup/logging"
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: flickrup
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 17
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 1
|
8
8
|
- 1
|
9
|
-
-
|
10
|
-
version: 1.1.
|
9
|
+
- 1
|
10
|
+
version: 1.1.1
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Jonathan Gilbert
|
@@ -129,6 +129,22 @@ dependencies:
|
|
129
129
|
version: 2.0.0
|
130
130
|
type: :runtime
|
131
131
|
version_requirements: *id007
|
132
|
+
- !ruby/object:Gem::Dependency
|
133
|
+
name: inifile
|
134
|
+
prerelease: false
|
135
|
+
requirement: &id008 !ruby/object:Gem::Requirement
|
136
|
+
none: false
|
137
|
+
requirements:
|
138
|
+
- - ">="
|
139
|
+
- !ruby/object:Gem::Version
|
140
|
+
hash: 15
|
141
|
+
segments:
|
142
|
+
- 2
|
143
|
+
- 0
|
144
|
+
- 0
|
145
|
+
version: 2.0.0
|
146
|
+
type: :runtime
|
147
|
+
version_requirements: *id008
|
132
148
|
description: Flickr auto uploading script
|
133
149
|
email: bb..jgilbert@xoxy.net
|
134
150
|
executables:
|
@@ -143,6 +159,11 @@ files:
|
|
143
159
|
- lib/flickrup/ext/replacement_tags.rb
|
144
160
|
- lib/flickrup/ext/tag_sets.rb
|
145
161
|
- lib/flickrup/ext/visibility.rb
|
162
|
+
- lib/flickrup/filetype/inifile.rb
|
163
|
+
- lib/flickrup/filetype/picasa_ini.rb
|
164
|
+
- lib/flickrup/filetype/tagged_file.rb
|
165
|
+
- lib/flickrup/filetype/tagged_image.rb
|
166
|
+
- lib/flickrup/filetype/tagged_video.rb
|
146
167
|
- lib/flickrup/flickr_client.rb
|
147
168
|
- lib/flickrup/logging.rb
|
148
169
|
- lib/flickrup/processing_context.rb
|
@@ -150,7 +171,6 @@ files:
|
|
150
171
|
- lib/flickrup/quick_listener.rb
|
151
172
|
- lib/flickrup/slow_listener.rb
|
152
173
|
- lib/flickrup/tag_set.rb
|
153
|
-
- lib/flickrup/tagged_image.rb
|
154
174
|
- lib/flickrup/uploader.rb
|
155
175
|
- lib/flickrup.rb
|
156
176
|
- bin/flickrup
|
@@ -1,71 +0,0 @@
|
|
1
|
-
require 'mini_exiftool'
|
2
|
-
require 'fileutils'
|
3
|
-
require "flickrup/logging"
|
4
|
-
require "flickrup/tag_set"
|
5
|
-
require "flickrup/processing_context"
|
6
|
-
|
7
|
-
class TaggedImage
|
8
|
-
include Logging
|
9
|
-
|
10
|
-
attr_reader :filename
|
11
|
-
|
12
|
-
def initialize(filename)
|
13
|
-
@filename = filename
|
14
|
-
@parsed = MiniExiftool.new filename
|
15
|
-
end
|
16
|
-
|
17
|
-
def tags
|
18
|
-
keywords = @parsed.keywords
|
19
|
-
TagSet.new(if keywords == nil
|
20
|
-
[]
|
21
|
-
elsif keywords.class == String
|
22
|
-
[keywords]
|
23
|
-
else
|
24
|
-
keywords
|
25
|
-
end)
|
26
|
-
end
|
27
|
-
|
28
|
-
def doUpload(preupload_handlers, uploader)
|
29
|
-
|
30
|
-
context = preupload_handlers.reduce(ProcessingContext.new(self)) do |ctx, handler|
|
31
|
-
handler.preupload(ctx)
|
32
|
-
end
|
33
|
-
|
34
|
-
#maybe save afterwards
|
35
|
-
if @parsed.changed?
|
36
|
-
@parsed.save!
|
37
|
-
end
|
38
|
-
|
39
|
-
uploader.upload(context)
|
40
|
-
|
41
|
-
#maybe save afterwards
|
42
|
-
if @parsed.changed?
|
43
|
-
@parsed.save!
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
def archive(to_dir)
|
48
|
-
date_taken = @parsed.date_time_original || @parsed.modifydate
|
49
|
-
|
50
|
-
target_dir = File.join(
|
51
|
-
to_dir,
|
52
|
-
date_taken.strftime("%Y"),
|
53
|
-
date_taken.strftime("%b")
|
54
|
-
)
|
55
|
-
|
56
|
-
FileUtils.mkdir_p(target_dir)
|
57
|
-
FileUtils.mv(@filename, File.join(target_dir, File.basename(@filename)))
|
58
|
-
logger.info("Archived #{File.basename(@filename)} to #{target_dir}")
|
59
|
-
end
|
60
|
-
|
61
|
-
# Returns the value of a tag.
|
62
|
-
def [] tag
|
63
|
-
@parsed[tag]
|
64
|
-
end
|
65
|
-
|
66
|
-
# Set the value of a tag.
|
67
|
-
def []=(tag, val)
|
68
|
-
@parsed[tag] = val
|
69
|
-
end
|
70
|
-
|
71
|
-
end
|