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 CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require 'rubygems'
3
+ #require 'rubygems'
4
4
  require 'flickrup'
5
5
 
6
6
  Flickrup.run_cmd(ARGV)
@@ -5,7 +5,7 @@ class MachineTags
5
5
 
6
6
  def preupload(ctx)
7
7
 
8
- tags = ctx.image.tags
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.image['Keywords'] = remapped
18
- ctx.image['Subject'] = remapped #also write Subject XMP tag
17
+ ctx.file['Keywords'] = remapped
18
+ ctx.file['Subject'] = remapped #also write Subject XMP tag
19
19
  end
20
20
 
21
21
  ctx
@@ -8,7 +8,7 @@ class PrefixedExtension
8
8
  prefix = @config[prefix_key]
9
9
 
10
10
  if prefix
11
- all_tags = ctx.image.tags
11
+ all_tags = ctx.file.tags
12
12
 
13
13
  (selected_tags, tags) = all_tags.partition do |x|
14
14
  x.start_with?("#{prefix}")
@@ -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.image[tag_name]
12
+ existing = ctx.file[tag_name]
13
13
 
14
14
  tag_value_replacements.each do |key, value|
15
15
  if existing == key
16
- ctx.image[tag_name] = value
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.image.filename} to set: #{set}")
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.image.filename}.")
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
- @token_file = File.join(ENV['HOME'], ".flickrup")
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 :image
2
+ attr_reader :file
3
3
  attr_reader :upload_properties
4
4
 
5
- def initialize(image, props = {})
6
- @image = image
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.image, self.upload_properties.merge(props))
11
+ ProcessingContext.new(self.file, self.upload_properties.merge(props))
12
12
  end
13
13
  end
@@ -1,4 +1,4 @@
1
- require "flickrup/tagged_image"
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(files = Dir["#{@config[:watch_dir]}/*.{jpg,JPG,jpeg,JPEG}"])
20
+ def run(all_files = Dir["#{@config[:watch_dir]}/*.*"])
21
21
 
22
- logger.debug("Processing #{files.length} files")
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
- images = files.map { |x|
30
+ files = all_files.map { |x|
31
31
  if File.exist? x
32
- TaggedImage.new(x)
32
+ TaggedFile.create(x)
33
33
  else
34
34
  nil
35
35
  end
36
36
  }.compact
37
37
 
38
- tagged = images.select { |x|
38
+ tagged = files.select { |x|
39
39
  !(x.tags.nil? || x.tags.empty?)
40
40
  }.sort_by { |x|
41
41
  [x.filename]
@@ -70,7 +70,7 @@ class SlowListener
70
70
 
71
71
  @processing.set PROCESSING
72
72
 
73
- files = Dir["#{@config[:watch_dir]}/*.{jpg,JPG,jpeg,JPEG}"]
73
+ files = Dir["#{@config[:watch_dir]}/*.*"]
74
74
 
75
75
  old, new = files.partition { |x| Time.now > File.mtime(x) + @wait }
76
76
 
@@ -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.image.filename} as marked as NO_UPLOAD")
20
+ logger.info("Skipping upload of #{ctx.file.filename} as marked as NO_UPLOAD")
21
21
  else
22
- logger.debug("Uploading #{ctx.image.filename} with tags #{ctx.image.tags}")
22
+ logger.debug("Uploading #{ctx.file.filename} with tags #{ctx.file.tags}")
23
23
 
24
24
  begin
25
- rv = @flickr.upload_photo(ctx.image.filename, prepare_upload_properties(ctx))
26
- logger.debug("Uploaded #{ctx.image.filename}")
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.image.filename}: #{err}")
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.image.tags.to_s}.merge(ctx.upload_properties)
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/tagged_image"
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: 19
4
+ hash: 17
5
5
  prerelease:
6
6
  segments:
7
7
  - 1
8
8
  - 1
9
- - 0
10
- version: 1.1.0
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