multistockphoto 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/History.txt CHANGED
@@ -1,3 +1,14 @@
1
+ == 0.8.0 2008-06-28
2
+ * 1 major enhancement
3
+ * neue Site Bigstockphoto erstellt
4
+ * Option --plist in bin/multistockphoto erstellt
5
+ * Option --count=N in Verbindung mit --no-tags und --tags_only erstellt
6
+ * 1 minor enhancement
7
+ * Absicherung/Fehlermeldung wenn Config-Datei nicht richtig gelesen werden kann
8
+ * Methode self.accept?/accept? je Site Klasse neu, fuer zulaessige File-Typen
9
+ * Optimierungen in bin/multistockphoto
10
+ * 1 bug fix
11
+ * --stats Fehler bei deaktivierten Sites behoben
1
12
  == 0.7.1 2008-06-16
2
13
  * 1 bug fix
3
14
  * entfernen von ueberfluessiger aldi-site fuehrte anderweitig zu Fehler
data/Manifest.txt CHANGED
@@ -21,6 +21,7 @@ lib/multistockphoto/site_zoonar.rb
21
21
  lib/multistockphoto/mock_zoonar.rb
22
22
  lib/multistockphoto/site_photocase.rb
23
23
  lib/multistockphoto/site_dreamstime.rb
24
+ lib/multistockphoto/site_bigstockphoto.rb
24
25
  lib/multistockphoto/photo.rb
25
26
  lib/multistockphoto/upload_exception.rb
26
27
  script/console
data/bin/multistockphoto CHANGED
@@ -6,10 +6,18 @@ require 'multistockphoto'
6
6
  require 'RMagick'
7
7
  include Magick
8
8
  require 'exifr'
9
+ require 'grep'
10
+ include Grep
9
11
 
10
12
  SENDELISTE = 'sendeliste.dat'
13
+ ROT_PREFIX = 'rot_'
11
14
  MAX_ERRORS = 3
12
- PICTURE_FILES = /.PNG|.JPG|.GIF|.JPEG/
15
+ PICTURE_FILES = /(.PNG|.JPG|.GIF|.JPEG|.EPS|.AI|.PSD|.PDF|.TIF|.TIFF)$/
16
+
17
+ require 'fcntl'
18
+
19
+ #STDOUT.sync = true
20
+ #STDOUT.fcntl(Fcntl::F_SETFL, Fcntl::O_NONBLOCK)
13
21
 
14
22
  # alle Sites komplett bearbeitet?
15
23
  def all_sites_done(sites,n)
@@ -170,11 +178,33 @@ Choice.options do
170
178
  short '-q'
171
179
  long '--no-tags'
172
180
  desc 'List photos without tags file' #TODO: eigentlich muss man auch auf vorhandene IPTC in Photo selbst testen
181
+ #cast Integer
182
+ #default 0
173
183
  action do
174
184
  $no_tags = true
175
185
  end
176
186
  end
177
187
 
188
+ option :count do
189
+ short '-3'
190
+ long '--count'
191
+ desc 'count'
192
+ cast Integer
193
+ default 0
194
+ action do
195
+ $count_flag = true
196
+ end
197
+ end
198
+
199
+ option :plist do
200
+ short '-2'
201
+ long '--plist'
202
+ desc 'Show details sending status for each picture'
203
+ action do
204
+ $plist = true
205
+ end
206
+ end
207
+
178
208
  option :dont_send do
179
209
  long '--dont-send'
180
210
  action do
@@ -200,51 +230,49 @@ def formatted_minutes(d)
200
230
  mm = (d / 60.0).floor
201
231
  d = d-mm*60
202
232
  ss = d.round
203
- # return sprintf("%02d:%02d:%02d",hh,mm,ss)
204
233
  return sprintf("%d:%02d",mm,ss)
205
234
  end
206
235
 
236
+ # Hilfsmethode fuer send_all
237
+ # false, falls keine Tags-Datei existiert oder laut Aufrufparameter --count zu wenige
238
+ # Keywords hat.
239
+ def not_enough_tags?(photo)
240
+ if $tags_only
241
+ if ! tagsfile?(photo.filename)
242
+ return true
243
+ else
244
+ if $count_flag
245
+ photo.set_keywords
246
+ if photo.tags.size < $count
247
+ return true
248
+ end
249
+ end
250
+ end
251
+ end # tags_only
252
+ false
253
+ end
254
+
207
255
  # Senden aller noch nicht gesendeten Photos an alle Sites
208
256
  # unter Berücksichtigung sonstiger Optionsparameter
209
257
  def send_all
258
+ if $count_flag
259
+ $count = Choice.choices[:count]
260
+ end
210
261
  total_transfers = 0
211
262
  total = {}
212
263
  errors = {}
213
- #sites = [:fotolia, :zoonar, :photocase]
214
264
  $active_sites.each {|site|
215
265
  total[site] = 0
216
266
  errors[site] = 0
217
267
  }
218
268
  puts 'sending all unsent photos'
219
269
 
220
- # rotate photos
221
- # - true: we do the rotation
222
- # - false: the site does the rotation automatically
223
- # change to false if a photo site can automatically rotate photos
224
- # rotate_photos = {
225
- # :fotolia => true,
226
- # :zoonar => true,
227
- # :photocase => true,
228
- # }
229
-
230
- # done = {
231
- # :fotolia => false,
232
- # :zoonar => false,
233
- # :photocase => false,
234
- # :dreamstime => false,
235
- # }
236
- # remaining = {
237
- # :fotolia => 999_999_999,
238
- # :zoonar => 999_999_999,
239
- # :photocase => 999_999_999,
240
- # :dreamstime => 999_999_999,
241
- # }
242
270
  remaining = Hash.new
243
271
  done = Hash.new
244
272
  $active_sites.each {|site|
245
273
  done[site] = false
246
274
  remaining[site] = 999_999_999
247
- }
275
+ }
248
276
  $active_sites.each {|site|
249
277
  # dynamisches Site.new("site")
250
278
  s = eval("#{site.to_s.capitalize}"+".new(#{site.to_s.capitalize})")
@@ -257,25 +285,30 @@ def send_all
257
285
  allfiles.sort!
258
286
  end
259
287
  allfiles.each {|filename|
260
- next if File.basename(filename)[0,4] == 'rot_'
288
+ next if File.basename(filename)[0,4] == ROT_PREFIX
261
289
  #puts "rot_ uebersprungen"
262
290
  if filename.upcase =~ PICTURE_FILES
263
291
  total_per_site_flag = $total_per_site
264
292
  photo = Photo.new(filename)
293
+ next if not_enough_tags? photo
265
294
  $active_sites.each {|site_name|
266
295
  begin
267
296
  Timeout::timeout(10.0*60.0) do
268
297
  t1 = Time.now
269
298
  site = eval(site_name.to_s.capitalize+".new(#{site_name.to_s.capitalize})")
299
+ # if ! site.accept?(File.extname(filename))
300
+ # warn "Warning: Site (#{site_name}) does not support this file extension (#{File.extname(filename)}). Skipped"
301
+ # next
302
+ # end
270
303
  # site.rotate_photos = rotate_photos[site_name]
271
304
  done[site_name] = true if remaining[site_name] <= 0
272
305
  next if done[site_name]
273
- if $tags_only and ! tagsfile?(photo.filename)
274
- # if $verbose
275
- # puts "#{photo.filename}: no tags file. skipped."
276
- # end
277
- next
306
+
307
+ if ! site.accept?(File.extname(filename))
308
+ warn "Warning: Site (#{site_name}) does not support this file extension (#{File.extname(filename)}). Skipped"
309
+ next
278
310
  end
311
+ #end
279
312
  if $send_all or
280
313
  (total_per_site_flag and
281
314
  total[site_name] < Choice.choices[:total_per_site])
@@ -288,8 +321,8 @@ def send_all
288
321
  remaining[site_name] -= 1
289
322
  done[site_name] = true if remaining[site_name] <= 0
290
323
  errors[site_name] = 0
324
+ t2 = Time.now
291
325
  if $verbose
292
- t2 = Time.now
293
326
  if $dont_send and $dont_log
294
327
  puts "1:23"
295
328
  else
@@ -298,17 +331,13 @@ def send_all
298
331
  p total # Summenzeile ausgeben
299
332
  end
300
333
  end
301
- rescue UploadException #Net::FTPTempError #TODO: gefaellt mir noch nicht so ganz
334
+ rescue UploadException
302
335
  errors[site_name] += 1
303
336
  if errors[site_name] >= MAX_ERRORS
304
- done[site_name] = true # nichts mehr uploaden
337
+ done[site_name] = true
305
338
  puts "too many errors. giving up."
306
339
  end
307
- # puts "FTP-Fehler beim Senden an #{site_name} aufgetreten. Ggf. Konfiguration ueberpruefen oder spaeter erneut vesuchen"
308
340
  puts "Upload error at sending to #{site_name} occurred. Check configuration or try again later!"
309
- # rescue SocketError
310
- # puts "SocketError: no net? waiting 60 secs"
311
- # sleep 60
312
341
  rescue Timeout::Error
313
342
  puts "timeout error"
314
343
  #timeout error
@@ -367,7 +396,7 @@ def not_sent
367
396
  }
368
397
  puts "not sent photos:"
369
398
  Dir.glob(File.join($upload_dir,'*')).each {|filename|
370
- next if File.basename(filename)[0,4] == 'rot_'
399
+ next if File.basename(filename)[0,4] == ROT_PREFIX
371
400
  if filename.upcase =~ PICTURE_FILES
372
401
  $active_sites.each {|site|
373
402
  print filename+"\t"+site.to_s+"\t" if $verbose
@@ -428,7 +457,10 @@ def stats
428
457
  }
429
458
  count.each_pair {|k,v|
430
459
  site = k[0]
431
- totals[site.to_sym] += v
460
+ if $active_sites.include? site.to_sym # sonst kommt es zu Fehlern bei
461
+ # deaktivierten Sites
462
+ totals[site.to_sym] += v
463
+ end
432
464
  all += v
433
465
  }
434
466
  print "total | "
@@ -445,7 +477,7 @@ def stats
445
477
  count[:tags] = 0
446
478
  count[:notags] = 0
447
479
  Dir.glob(File.join($upload_dir,'*')).each {|filename|
448
- next if File.basename(filename)[0,4] == 'rot_'
480
+ next if File.basename(filename)[0,4] == ROT_PREFIX
449
481
  if filename.upcase =~ PICTURE_FILES
450
482
  if tagsfile?(filename)
451
483
  count[:tags] += 1
@@ -467,7 +499,7 @@ def list_done
467
499
  $active_sites.each {|site|
468
500
  photo_sent[site] = false
469
501
  }
470
- next if File.basename(filename)[0,4] == 'rot_'
502
+ next if File.basename(filename)[0,4] == ROT_PREFIX
471
503
  if filename.upcase =~ PICTURE_FILES
472
504
  $active_sites.each {|site|
473
505
  print filename+"\t"+site.to_s+"\t" if $verbose
@@ -506,7 +538,7 @@ def purge_done
506
538
  $active_sites.each {|site|
507
539
  photo_sent[site] = false
508
540
  }
509
- next if File.basename(filename)[0,4] == 'rot_'
541
+ next if File.basename(filename)[0,4] == ROT_PREFIX
510
542
  if filename.upcase =~ PICTURE_FILES
511
543
  $active_sites.each {|site|
512
544
  print filename+"\t"+site.to_s+"\t" if $verbose
@@ -533,20 +565,63 @@ end
533
565
 
534
566
  # Listet alle Photo-Dateien auf, zu denen es keine .tags-Datei gibt
535
567
  def no_tags
536
- sender = Sender.new("Thomas")
568
+ if $count_flag
569
+ $count = Choice.choices[:count]
570
+ end
537
571
  allfiles = Dir.glob(File.join($upload_dir,'*'))
538
572
  if $ordered
539
573
  allfiles.sort!
540
574
  end
541
575
  allfiles.each {|fn|
542
- next if File.basename(fn)[0,4] == 'rot_'
576
+ next if File.basename(fn)[0,4] == ROT_PREFIX
543
577
  if fn.upcase =~ PICTURE_FILES
544
578
  if ! tagsfile?(fn)
545
- #if ! sender.sent_to?(fn,:fotolia) #TODO: provisorisch
546
- puts fn
547
- #end
548
-
579
+ puts fn
580
+ else
581
+ # es gibt eine aber zu wenige keywords
582
+ if $count_flag
583
+ photo = Photo.new(fn)
584
+ photo.set_keywords_without_write
585
+ if photo.tags.size < $count
586
+ puts fn + " only #{photo.tags.size} keywords"
587
+ end
588
+ end
589
+ end
590
+
591
+ end
592
+ }
593
+ end
594
+
595
+ def plist
596
+ allfiles = Dir.glob(File.join($upload_dir,'*'))
597
+ if $ordered
598
+ allfiles.sort!
599
+ end
600
+ print ' '+' '*34
601
+ $active_sites.each {|site|
602
+ printf("%-10s",site)
603
+ }
604
+ puts
605
+ allfiles.each {|filename|
606
+ next if File.basename(filename)[0,4] == ROT_PREFIX
607
+ if filename.upcase =~ PICTURE_FILES
608
+ photo = Photo.new(filename)
609
+ photo.set_keywords_without_write
610
+ printf("%-30s ",filename)
611
+ if tagsfile?(filename)
612
+ printf "(t %2d) ", photo.tags.size
613
+ else
614
+ print ' '
549
615
  end
616
+ $active_sites.each {|site|
617
+ g = grep(SENDELISTE, /#{site.to_s}\t#{filename}/)
618
+ if g.size == 0
619
+ print ' '*10
620
+ else
621
+ print '+'+' '*9
622
+ end
623
+ }
624
+ puts
550
625
  end
551
626
  }
552
627
  end
@@ -584,3 +659,6 @@ if $no_tags
584
659
  no_tags
585
660
  end
586
661
 
662
+ if $plist
663
+ plist
664
+ end
data/config.yaml CHANGED
@@ -4,6 +4,7 @@
4
4
  - :zoonar
5
5
  - :photocase
6
6
  - :dreamstime
7
+ - :bigstockphoto
7
8
  :zoonar:
8
9
  :user: 'hugo'
9
10
  :password: 'lalala'
@@ -24,3 +25,9 @@
24
25
  :ftp_user: 'martha12345'
25
26
  :ftp_password: 'ftpgeheimmartha'
26
27
  :total_per_day: 1
28
+ :bigstockphoto:
29
+ :user: 'dennis'
30
+ :password: 'dennisgeheim'
31
+ :ftp_user: 'dennis12345'
32
+ :ftp_password: 'ftpgeheimdennis'
33
+ :total_per_day: 1
@@ -50,30 +50,56 @@ class Photo
50
50
  pic.auto_orient!
51
51
  puts "rotating picture, writing to #{tmpname}"
52
52
  pic.write(tmpname)
53
-
54
53
  end
55
54
  @rotated_filename = tmpname
56
55
  end
57
56
  end
58
57
 
59
58
  # setzt IPTC-Keywords, enthalten in Datei <imagefilename>.tags
60
- def set_keywords
59
+ # wenn from_lang und to_lang uebergeben, dann Uebersetzung der Keywords
60
+ # z.B. set_keywords(:de,:en)
61
+ def set_keywords(from_lang=nil,to_lang=nil)
61
62
  ext = File.extname(@filename)
63
+ if ext.upcase != '.JPG' and ext.upcase != '.JPEG'
64
+ warn "cannot set Exif-Header with non-JPEG file #{@filename}"
65
+ return
66
+ end
62
67
  fn = @filename.sub(ext,".tags")
63
68
  lines=[]
64
69
  begin
65
70
  File.open(fn) {|f|
66
71
  lines = f.read
67
72
  }
68
- puts "tags file #{fn} found"
69
73
  tags = Photo.to_tags(lines)
74
+ if from_lang and to_lang
75
+ tags = translate_tags(tags,from_lang,to_lang)
76
+ end
70
77
  self.tags = tags
71
- print "writing tags to file ... "
72
- $stdout.flush
78
+ #print "writing tags to file ... "
79
+ #$stdout.flush
73
80
  write_keywords
74
- puts "done"
81
+ #puts "done"
75
82
  rescue Errno::ENOENT
76
- warn "WARNING: no tags file #{fn}"
83
+ #warn "WARNING: no tags file #{fn}"
84
+ end
85
+ end
86
+
87
+ def set_keywords_without_write(from_lang=nil,to_lang=nil)
88
+ ext = File.extname(@filename)
89
+ fn = @filename.sub(ext,".tags")
90
+ lines=[]
91
+ begin
92
+ File.open(fn) {|f|
93
+ lines = f.read
94
+ }
95
+ #puts "tags file #{fn} found"
96
+ tags = Photo.to_tags(lines)
97
+ if from_lang and to_lang
98
+ tags = translate_tags(tags,from_lang,to_lang)
99
+ end
100
+ self.tags = tags
101
+ rescue Errno::ENOENT
102
+ #warn "WARNING: no tags file #{fn}"
77
103
  end
78
104
  end
79
105
 
@@ -82,15 +108,6 @@ class Photo
82
108
  tags
83
109
  end
84
110
 
85
- # erstellt Key-Array aus String (getrennt durch newline,',' oder ' ')
86
- def self.to_tags(s)
87
- keys = []
88
- s.split(/[, |\t\n]+/).each {|w|
89
- keys << w
90
- }
91
- keys
92
- end
93
-
94
111
  # ermittelt Keywords aus Datei
95
112
  def file_keywords
96
113
  photo = MiniExiftool.new @filename
@@ -101,18 +118,20 @@ class Photo
101
118
  # schreibt Keywords in Datei-Header
102
119
  def write_keywords
103
120
  photo = MiniExiftool.new @filename
104
- s = ''
105
- tags.each {|tag|
106
- if s == ''
107
- s = s + tag
108
- else
109
- # s = s + ' ' + tag
110
- # lieber durch Komma trennen, da sonst das Formular z.B. bei fotolia nicht
111
- # richtig gefüllt wird
112
- s = s + ',' + tag
113
- end
114
- }
115
- photo['keywords'] = s
121
+ #s = ''
122
+ #tags.each {|tag|
123
+ #if s == ''
124
+ #s = s + tag
125
+ #else
126
+ ## s = s + ' ' + tag
127
+ ## lieber durch Komma trennen, da sonst das Formular z.B. bei fotolia nicht
128
+ ## richtig gefüllt wird
129
+ #s = s + ',' + tag
130
+ #end
131
+ #}
132
+ # lieber durch Komma trennen, da sonst das Formular z.B. bei fotolia nicht
133
+ # richtig gefüllt wird
134
+ photo['keywords'] = tags.join(',')
116
135
 
117
136
  #TODO:
118
137
  photo['title'] = 'Testtitel'
@@ -120,4 +139,51 @@ class Photo
120
139
  photo.save
121
140
  end
122
141
 
142
+ private
143
+
144
+ # erstellt Key-Array aus String (getrennt durch newline,',' oder ' ')
145
+ def self.to_tags(s)
146
+ keys = []
147
+ s.split(/[, |\t\n]+/).each {|w|
148
+ keys << w
149
+ }
150
+ keys
151
+ end
152
+
153
+
154
+ # uebersetzt Tags von 'from_lang' nach 'to_lang'
155
+ # bisher realisiert: :de -> :en
156
+ # Achtung: bisher nur experimentell. Nicht verwenden!
157
+ def translate_tags(tags, from_lang, to_lang)
158
+ new_tags = []
159
+ tags.each {|tag|
160
+ new_tags << translate_tag(tag,from_lang,to_lang)
161
+ }
162
+ return new_tags
163
+ end
164
+
165
+ # uebersetzt ein Tag von 'from_lang' nach 'to_lang'
166
+ # bisher realisiert: :de -> :en
167
+ # Achtung: bisher nur experimentell. Nicht verwenden!
168
+ def translate_tag(tag,from_lang,to_lang)
169
+ if from_lang == :de and to_lang == :en
170
+ case tag
171
+ when 'Baum'
172
+ 'tree'
173
+ when 'Berlin'
174
+ 'Berlin'
175
+ when 'gwb'
176
+ 'gwb'
177
+ when 'Blume'
178
+ 'flower'
179
+ when 'Biene'
180
+ 'bee'
181
+ else
182
+ raise "unknown translation for '#{tag}' (#{from_lang} #{to_lang})"
183
+ end
184
+ else
185
+ raise "unknown translation codes: #{from_lang} #{to_lang}"
186
+ end
187
+ end
188
+
123
189
  end
@@ -28,7 +28,7 @@ class Sender
28
28
  end
29
29
 
30
30
  # wurde dieses File schon an diese Site gesendet?
31
- def sent_to?(p_filename,p_site)
31
+ def sent_to?(p_filename,p_site) #TODO:
32
32
  sendeliste = 'sendeliste.dat' #TODO:
33
33
  result = false
34
34
  File.open(sendeliste) {|f|
@@ -0,0 +1,121 @@
1
+ require 'net/ftp'
2
+
3
+ class Bigstockphoto < GenericSite
4
+
5
+ @@errors = 0
6
+
7
+ attr_reader :total_per_day
8
+ attr_accessor :ftp_user, :ftp_password, :max_errors
9
+
10
+ FTP_HOST = 'bigstockphoto.com'
11
+ SITENAME = 'bigstockphoto'
12
+
13
+ def initialize(name)
14
+ super
15
+ @config = load_config
16
+ begin
17
+ @user = @config[:bigstockphoto][:user]
18
+ @password = @config[:bigstockphoto][:password]
19
+ @ftp_user = @config[:bigstockphoto][:ftp_user]
20
+ @ftp_password = @config[:bigstockphoto][:ftp_password]
21
+ @total_per_day = @config[:bigstockphoto][:total_per_day]
22
+ rescue
23
+ puts 'cannot read configuration entries of '+SITENAME
24
+ end
25
+ end
26
+
27
+ def transfer(photo, dont_send=nil, dont_log=nil)
28
+ # falls nicht in config Datei eingetragen dann nicht senden
29
+ if @user == nil or @password == nil
30
+ return
31
+ end
32
+ unless photo.class == Photo
33
+ raise 'not a Photo object'
34
+ end
35
+ unless File.exist?(photo.filename)
36
+ raise "file #{photo.filename} does not exist"
37
+ end
38
+ if @ftp_user == nil or @ftp_password == nil
39
+ raise "ftp_user/ftp_password not set"
40
+ end
41
+ print "#{SITENAME}:#{photo.filename} ... "
42
+ $stdout.flush
43
+ if already_sent_site?(photo,SITENAME)
44
+ puts "already sent"
45
+ return :duplicate
46
+ end
47
+ if photo.portrait? and ! Bigstockphoto.can_handle_orientation?
48
+ photo.drehen
49
+ end
50
+ if site_can_handle_keywords?
51
+ photo.set_keywords(:de,:en) # Keywords uebersetzen
52
+ end
53
+ if ! dont_send
54
+ begin
55
+ ftp = Net::FTP.new(FTP_HOST)
56
+ ftp.login(@ftp_user,@ftp_password)
57
+ files=ftp.list('*')
58
+ if photo.portrait?
59
+ #p photo.rotated_filename
60
+ res = ftp.putbinaryfile(photo.rotated_filename)
61
+ else
62
+ res = ftp.putbinaryfile(photo.filename)
63
+ end
64
+ files =ftp.list('*')
65
+ #p files
66
+ ftp.close
67
+ rescue Errno::EPIPE
68
+ raise UploadException
69
+ end
70
+ end
71
+ @@errors = 0
72
+ puts 'OK'
73
+ if ! dont_log
74
+ File.open(SENDLIST,'a') {|f|
75
+ f.puts "#{SITENAME}\t#{photo.filename}\t#{Time.now}"
76
+ }
77
+ end
78
+ true
79
+ end
80
+
81
+ # Anzahl heute schon gesendeter Photos
82
+ def sent_today
83
+ site = SITENAME
84
+ return sent_today_site(site)
85
+ end
86
+
87
+ # wurde dieses Photo schon gesendet?
88
+ def already_sent?(photo)
89
+ already_sent_site?(photo,SITENAME)
90
+ end
91
+
92
+ # can site handle orientation?
93
+ def self.can_handle_orientation?
94
+ false #TODO: pruefen!
95
+ end
96
+
97
+ # Site akzeptiert diese Extension
98
+ def self.accept?(ext)
99
+ ext = ext.downcase
100
+ if ext[0,1] == '.'
101
+ ext = ext[1..-1]
102
+ end
103
+ case ext
104
+ when 'jpg','jpeg','eps','ai','psd','png','pdf'
105
+ return true
106
+ else
107
+ return false
108
+ end
109
+ end
110
+
111
+ def accept?(ext)
112
+ Bigstockphoto.accept?(ext)
113
+ end
114
+
115
+ private
116
+
117
+ def site_can_handle_keywords?
118
+ true #TODO: pruefen!
119
+ end
120
+
121
+ end # class