flickrup 1.1.0 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
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