support no identify and no transcode/mencoder
[booh] / bin / booh-backend
1 #! /usr/bin/ruby
2 #
3 #                         *  BOOH  *
4 #
5 # A.k.a `Best web-album Of the world, Or your money back, Humerus'.
6 #
7 # The acronyn sucks, however this is a tribute to Dragon Ball by
8 # Akira Toriyama, where the last enemy beaten by heroes of Dragon
9 # Ball is named "Boo". But there was already a free software project
10 # called Boo, so this one will be it "Booh". Or whatever.
11 #
12 #
13 # Copyright (c) 2005 Guillaume Cottenceau <gc3 at bluewin.ch>
14 #
15 # This software may be freely redistributed under the terms of the GNU
16 # public license version 2.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with this program; if not, write to the Free Software
20 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
21
22 require 'getoptlong'
23 require 'gettext'
24 include GetText
25 require 'rexml/document'
26 include REXML
27
28 require 'booh/booh-lib'
29 require 'booh/html-merges'
30
31 #- bind text domain as soon as possible because some _() functions are called early to build data structures
32 bindtextdomain("booh")
33
34 #- options
35 $options = [
36     [ '--help',          '-h', GetoptLong::NO_ARGUMENT,       _("Get help message") ],
37     [ '--version',       '-V', GetoptLong::NO_ARGUMENT,       _("Print version and exit") ],
38
39     [ '--source',        '-s', GetoptLong::REQUIRED_ARGUMENT, _("Directory which contains original images/videos as files or subdirs") ],
40     [ '--destination',   '-d', GetoptLong::REQUIRED_ARGUMENT, _("Directory which will contain the web-album") ],
41 #    [ '--clean',         '-c', GetoptLong::NO_ARGUMENT,       _("Clean destination directory") ],
42
43     [ '--theme',         '-t', GetoptLong::REQUIRED_ARGUMENT, _("Select HTML theme to use") ],
44     [ '--config',        '-C', GetoptLong::REQUIRED_ARGUMENT, _("File containing config listing images and videos within directories with captions") ],
45     [ '--config-skel',   '-k', GetoptLong::REQUIRED_ARGUMENT, _("Filename where the script will output a config skeleton") ],
46     [ '--merge-config',  '-M', GetoptLong::REQUIRED_ARGUMENT, _("File containing config listing, where to merge new images/videos from --source, and change theme info") ],
47     [ '--merge-config-onedir',  '-O', GetoptLong::REQUIRED_ARGUMENT, _("File containing config listing, for merging the subdir specified with --dir") ],
48     [ '--dir',           '-D', GetoptLong::REQUIRED_ARGUMENT, _("Directory for merge with --merge-config-onedir") ],
49     [ '--use-config',    '-u', GetoptLong::REQUIRED_ARGUMENT, _("File containing config listing, where to change theme info") ],
50     [ '--force',         '-f', GetoptLong::NO_ARGUMENT,       _("Force generation of album even if the GUI marked some directories as already generated") ],
51
52     [ '--sizes',         '-S', GetoptLong::REQUIRED_ARGUMENT, _("Specify the list of images sizes to use instead of all specified in the theme (this is a comma-separated list)") ],
53     [ '--thumbnails-per-row', '-T', GetoptLong::REQUIRED_ARGUMENT, _("Specify the amount of thumbnails per row in the thumbnails page (if applicable in theme)") ],
54     [ '--optimize-for-32', '-o', GetoptLong::NO_ARGUMENT,       _("Resize images with optimized sizes for 3/2 aspect ratio rather than 4/3 (typical aspect ratio of pictures from non digital cameras are 3/2 when pictures from digital cameras are 4/3)") ],
55     [ '--empty-comments','-e', GetoptLong::NO_ARGUMENT,       _("Prefer empty comments over filename when creating new albums") ],
56
57     [ '--mproc',         '-m', GetoptLong::REQUIRED_ARGUMENT, _("Specify the number of processors for multi-processors machines") ],
58
59     [ '--for-gui',       '-g', GetoptLong::NO_ARGUMENT,       _("Do the minimum work to be able to see the album under the GUI (don't generate all thumbnails)") ],
60
61     [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, _("Set max verbosity level (0: errors, 1: warnings, 2: important messages, 3: other messages)") ],
62     [ '--info-pipe',     '-i', GetoptLong::REQUIRED_ARGUMENT, _("Name a file where to write information about what's going on (used by the GUI)") ],
63 ]
64
65 #- default values for some globals 
66 $switches = []
67 $stdout.sync = true
68
69 def usage
70     puts _("Usage: %s [OPTION]...") % File.basename($0)
71     $options.each { |ary|
72         printf " %3s, %-18s %s\n", ary[1], ary[0], ary[3]
73     }
74 end
75
76 def handle_options
77     parser = GetoptLong.new
78     parser.set_options(*$options.collect { |ary| ary[0..2] })
79     begin
80         parser.each_option do |name, arg|
81             case name
82             when '--help'
83                 usage
84                 exit(0)
85
86             when '--version'
87                 puts _("Booh version %s
88
89 Copyright (c) 2005 Guillaume Cottenceau.
90 This is free software; see the source for copying conditions.  There is NO
91 warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.") % $VERSION
92
93                 exit(0)
94
95             when '--source'
96                 $source = File.expand_path(arg.sub(%r|/$|, ''))
97                 if !File.directory?($source)
98                     die _("Argument to --source must be a directory")
99                 end
100             when '--destination'
101                 $dest = File.expand_path(arg.sub(%r|/$|, ''))
102                 if File.exists?($dest) && !File.directory?($dest)
103                     die _("If --destination exists, it must be a directory")
104                 end
105                 if $dest != make_dest_filename($dest)
106                     die _("Sorry, destination directory can't contain non simple alphanumeric characters.")
107                 end
108 #            when '--clean'
109 #                system("rm -rf #{$dest}")
110
111             when '--theme'
112                 $theme = arg
113             when '--config'
114                 arg = File.expand_path(arg)
115                 if File.readable?(arg)
116                     $xmldoc = REXML::Document.new File.new(arg)
117                     $mode = 'use_config'
118                 else
119                     die _('Config file does not exist or is unreadable.')
120                 end
121             when '--config-skel'
122                 arg = File.expand_path(arg)
123                 if File.exists?(arg)
124                     if File.directory?(arg)
125                         die _("Config skeleton file (%s) already exists and is a directory! Please change the filename.") % arg
126                     else
127                         msg 1, _("Config skeleton file already exists, backuping to %s.backup") % arg
128                         File.rename(arg, "#{arg}.backup")
129                     end
130                 end
131                 $config_writeto = arg
132                 $mode = 'gen_config'
133             when '--merge-config'
134                 arg = File.expand_path(arg)
135                 if File.readable?(arg)
136                     msg 2, _("Merge config notice: backuping current config file to %s.backup") % arg
137                     $xmldoc = REXML::Document.new File.new(arg)
138                     File.rename(arg, "#{arg}.backup")
139                     $config_writeto = arg
140                     $mode = 'merge_config'
141                 else
142                     die _('Config file does not exist or is unreadable.')
143                 end
144             when '--merge-config-onedir'
145                 arg = File.expand_path(arg)
146                 if File.readable?(arg)
147                     msg 2, _("Merge config notice: backuping current config file to %s.backup") % arg
148                     $xmldoc = REXML::Document.new File.new(arg)
149                     File.rename(arg, "#{arg}.backup")
150                     $config_writeto = arg
151                     $mode = 'merge_config_onedir'
152                 else
153                     die _('Config file does not exist or is unreadable.')
154                 end
155             when '--dir'
156                 arg = File.expand_path(arg)
157                 if !File.readable?(arg)
158                     die _('Specified directory to merge with --dir is not readable')
159                 else
160                     $onedir = arg
161                 end
162             when '--use-config'
163                 arg = File.expand_path(arg)
164                 if File.readable?(arg)
165                     msg 2, _("Use config notice: backuping current config file to %s.backup") % arg
166                     $xmldoc = REXML::Document.new File.new(arg)
167                     File.rename(arg, "#{arg}.backup")
168                     $config_writeto = arg
169                     $mode = 'use_config_changetheme'
170                 else
171                     die _('Config file does not exist or is unreadable.')
172                 end
173
174             when '--sizes'
175                 $limit_sizes = arg
176
177             when '--thumbnails-per-row'
178                 $N_per_row = arg
179
180             when '--optimize-for-32'
181                 $optimize_for_32 = true
182
183             when '--empty-comments'
184                 $empty_comments = true
185
186             when '--force'
187                 $force = true
188
189             when '--mproc'
190                 $mproc = arg.to_i
191                 $pids = []
192
193             when '--for-gui'
194                 $forgui = true
195
196             when '--verbose-level'
197                 $verbose_level = arg.to_i
198
199             when '--info-pipe'
200                 $info_pipe = File.open(arg, File::WRONLY)
201                 $info_pipe.sync = true
202             end
203         end
204     rescue
205         puts $!
206         usage
207         exit(1)
208     end
209
210     if !$source && $xmldoc
211         $source = from_utf8($xmldoc.root.attributes['source'])
212         $dest = from_utf8($xmldoc.root.attributes['destination'])
213         $theme ||= $xmldoc.root.attributes['theme']
214         $limit_sizes ||= $xmldoc.root.attributes['limit-sizes']
215         if $mode == 'use_config' || $mode =~ /^merge_config/
216             $optimize_for_32 = !$xmldoc.root.attributes['optimize-for-32'].nil?
217             $N_per_row = $xmldoc.root.attributes['thumbnails-per-row']
218         end
219     end
220
221     if $mode == 'merge_config_onedir' && !$onedir
222         die _("Missing --dir for --merge_config_onedir")
223     end
224
225     if !$source
226         usage
227         exit(0)
228     end
229     if !$dest
230         die _("Missing --destination parameter.")
231     end
232     if !$theme
233         $theme = 'simple'
234     end
235
236     select_theme($theme, $limit_sizes, $optimize_for_32, $N_per_row)
237
238     if !$xmldoc
239         additional_params = ''
240         if $limit_sizes
241             additional_params += "limit-sizes='#{$limit_sizes}'"
242         end
243         if $optimize_for_32
244             additional_params += " optimize-for-32='true'"
245         end
246         if $N_per_row
247             additional_params += " thumbnails-per-row='#{$N_per_row}'"
248         end
249         $xmldoc = Document.new "<booh version='#{$VERSION}' source='#{utf8($source)}' destination='#{utf8($dest)}' theme='#{$theme}' #{additional_params}/>"
250         $xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
251         $mode = 'gen_config'
252     end
253
254     if $mode == 'merge_config' || $mode == 'use_config_changetheme'
255         $xmldoc.root.add_attribute('theme', $theme)
256         $xmldoc.root.add_attribute('version', $VERSION)
257         if $limit_sizes
258             $xmldoc.root.add_attribute('limit-sizes', $limit_sizes)
259         else
260             $xmldoc.root.delete_attribute('limit-sizes')
261         end
262         if $optimize_for_32
263             $xmldoc.root.add_attribute('optimize-for-32', 'true')
264         else
265             $xmldoc.root.delete_attribute('optimize-for-32')
266         end
267         if $N_per_row
268             $xmldoc.root.add_attribute('thumbnails-per-row', $N_per_row)
269         else
270             $xmldoc.root.delete_attribute('thumbnails-per-row')
271         end
272     end
273 end
274
275 def read_config
276     $config = {}
277 end
278
279 def write_config
280 end
281
282 def info(value)
283     if $info_pipe
284         $info_pipe.puts(value)
285     end
286 end
287
288 def check_installation
289     if !system("which convert >/dev/null 2>/dev/null")
290         die _("The program 'convert' is needed. Please install it.
291 It is generally available with the 'ImageMagick' software package.")
292     end
293     if !system("which identify >/dev/null 2>/dev/null")
294         msg 1, _("The program 'identify' is needed to get images sizes and EXIF data. Please install it.
295 It is generally available with the 'ImageMagick' software package.")
296         $no_identify = true
297     end
298     missing = %w(transcode mencoder).delete_if { |prg| system("which #{prg} >/dev/null 2>/dev/null") }
299     if missing != []
300         msg 1, _("The following program(s) are needed to handle videos: '%s'. Videos will be ignored.") % missing.join(', ')
301         $ignore_videos = true
302     end
303 end
304
305 def replace_line(surround, keyword, line)
306     begin
307         contents = eval "$#{keyword}"
308         line.sub!(/#{surround}#{keyword}#{surround}/, contents)
309     rescue TypeError
310         die _("No '%s' found for substitution") % keyword
311     end
312 end
313
314 def build_html_skeletons
315     $html_images     = File.open("#{$FPATH}/themes/#{$theme}/skeleton_image.html").readlines
316     $html_thumbnails = File.open("#{$FPATH}/themes/#{$theme}/skeleton_thumbnails.html").readlines
317     $html_index      = File.open("#{$FPATH}/themes/#{$theme}/skeleton_index.html").readlines
318     for line in $html_images + $html_thumbnails + $html_index
319         while line =~ /~~~(\w+)~~~/
320             replace_line('~~~', $1, line)
321         end
322     end
323 end
324
325 def find_caption_value(xmldir, filename)
326     if cap = xmldir.elements["*[@filename='#{utf8(filename)}']"].attributes['caption']
327         return cap.gsub("\n", '<br/>')
328     else
329         return nil
330     end
331 end
332
333 def find_captions(xmldir, images)
334     return images.collect { |img| find_caption_value(xmldir, img) }
335 end
336
337 #- stolen from CVSspam
338 def urlencode(text)
339   text.sub(/[^a-zA-Z0-9\-,.*_\/]/) do
340     "%#{sprintf('%2X', $&[0])}"
341   end
342 end
343
344 def all_images_sizes
345     return $limit_sizes =~ /original/ ? $images_size + [ { 'name' => 'original' } ] : $images_size
346 end
347
348 def html_reload_to_thumbnails
349     html_reload_to_thumbnails = $preferred_size_reloader.clone
350     html_reload_to_thumbnails.gsub!(/~~theme~~/, $theme)
351     html_reload_to_thumbnails.gsub!(/~~default_size~~/, $default_size['name'])
352     html_reload_to_thumbnails.gsub!(/~~all_sizes~~/, all_images_sizes.collect { |s| "\"#{size2js(s['name'])}\"" }.join(', '))
353     return html_reload_to_thumbnails
354 end
355
356 def discover_iterations(iterations, line)
357     if line =~ /~~iterate(\d)_open(_max(\d+|N))?~~/
358         for iter in iterations.values
359             if iter['open']
360                 iter['open'] = false
361                 iter['close_wait'] = $1.to_i
362             end
363         end
364         max = $3 == 'N' ? ($N_per_row || $default_N) : $3
365         iterations[$1.to_i] = { 'open' => true, 'max' => max, 'opening' => '', 'closing' => '' }
366         if $1.to_i == 1
367             line.sub!(/.*/, '~~thumbnails~~')
368         else
369             line.sub!(/.*/, '')
370         end
371     elsif line =~ /~~iterate(\d)_close~~/
372         iterations[$1.to_i]['open']  = false;
373         iterations[$1.to_i]['close'] = true;
374         line.sub!(/.*/, '')
375     else
376         for iter in iterations.values
377             if iter['open']
378                 iter['opening'] += line
379                 line.sub!(/.*/, '')
380             end
381             if !iter['close'] && iter['close_wait'] && iterations[iter['close_wait']]['close']
382                 iter['closing'] += line
383                 line.sub!(/.*/, '')
384             end
385         end
386     end
387 end
388
389 def reset_iterations(iterations)
390     for iter in iterations.values
391         iter['value'] = 1
392     end
393 end
394
395 def run_iterations(iterations)
396     html = ''
397     for level in iterations.keys.sort
398         if iterations[level]['value'] == 1 || level == iterations.keys.max
399             html += iterations[level]['opening']
400         end
401         iterations[level]['value'] += 1
402         if iterations[level]['max'] && iterations[level]['value'] > iterations[level]['max'].to_i
403             iterations[level]['value'] = 1
404             iterations[level-1]['value'] = 1
405             html += iterations[level-1]['closing']
406         end
407     end
408     return html
409 end
410
411 def close_iterations(iterations)
412     html = ''
413     for level in iterations.keys.sort.reverse
414         html += iterations[level]['closing']
415     end
416     return html
417 end
418
419 def img_element(fullpath)
420     if size = get_image_size(fullpath)
421         sizespec = 'width="' + size[:x].to_s + '" height="' + size[:y].to_s + '"'
422     else
423         sizespec = ''
424     end
425     return '<img src="' + File.basename(fullpath) + '" ' + sizespec + ' class="image"/>'
426 end
427
428 def size2js(name)
429     return name.gsub(/-/, '')
430 end
431
432 def substitute_html_sizes(html, sizeobj, type)
433     sizestrings = []
434     if $images_size.length > 1 || (type == 'image' && $limit_sizes =~ /original/)
435         for sizeobj2 in $images_size
436             if sizeobj != sizeobj2
437                 if type == 'thumbnails'
438                     sizestrings << '<a href="thumbnails-' + size2js(sizeobj2['name']) + '.html">' + sizename(sizeobj2['name']) + '</a>'
439                 else
440                     sizestrings << '<a id="link' + size2js(sizeobj2['name']) + '">' + sizename(sizeobj2['name']) + '</a>'
441                 end
442             else
443                 sizestrings << sizename(sizeobj2['name'])
444             end
445         end
446         if type == 'image' && $limit_sizes =~ /original/
447             sizestrings << '<a id="linkoriginal" target="newframe">' + sizename('original') + '</a>'
448         end
449     end
450     html.sub!(/~~sizes~~(.+)~~/) { sizestrings.join($1) }
451 end
452
453 def xmldir2destdir(xmldir)
454     return make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))
455 end
456
457 def find_previous_album(xmldir)
458     relative_pos = ''
459     begin
460         #- move to previous dir element if exists
461         if prevelem = xmldir.previous_element_byname_notattr('dir', 'deleted')
462             xmldir = prevelem
463             relative_pos += '../' + xmldir2destdir(xmldir) + '/'
464             child = nil
465             #- after having moved to previous dir, we need to go down last subdir until the last one
466             while child = xmldir.elements['dir']
467                 while nextchild = child.next_element_byname_notattr('dir', 'deleted')
468                     child = nextchild
469                 end
470                 relative_pos += xmldir2destdir(child) + '/'
471                 xmldir = child
472             end
473         else
474             #- previous dir doesn't exist, move to previous dir element if exists
475             xmldir = xmldir.parent
476             if xmldir.name == 'dir' && !xmldir.attributes['deleted']
477                 relative_pos += '../'
478             else
479                 return nil
480             end
481         end
482     end while !xmldir.child_byname_notattr('image', 'deleted') && !xmldir.child_byname_notattr('video', 'deleted')
483     return File.reduce_path(relative_pos)
484 end
485
486 def find_next_album(xmldir)
487     relative_pos = ''
488     begin
489         #- first child dir element (catches when initial xmldir has both thumbnails and subdirs)
490         if firstchild = xmldir.child_byname_notattr('dir', 'deleted')
491             xmldir = firstchild
492             relative_pos += xmldir2destdir(xmldir) + '/'
493         #- next brother
494         elsif nextbro = xmldir.next_element_byname_notattr('dir', 'deleted')
495             xmldir = nextbro
496             relative_pos += '../' + xmldir2destdir(xmldir) + '/'
497         else
498             #- go up until we have a next brother or we are finished
499             begin
500                 xmldir = xmldir.parent
501                 relative_pos += '../'
502             end while xmldir && !xmldir.next_element_byname_notattr('dir', 'deleted')
503             if xmldir
504                 xmldir = xmldir.next_element_byname('dir')
505                 relative_pos += '../' + xmldir2destdir(xmldir) + '/'
506             else
507                 return nil
508             end
509         end
510     end while !xmldir.child_byname_notattr('image', 'deleted') && !xmldir.child_byname_notattr('video', 'deleted')
511     return File.reduce_path(relative_pos)
512 end
513
514 def sub_previous_next_album(previous_album, next_album, html)
515     if previous_album
516         html.gsub!(/~~previous_album~~/, '<a href="' + previous_album + 'thumbnails.html">' + utf8(_('previous album')) + '</a>')
517         html.gsub!(/~~ifprevious_album\?~~(.+?)~~fi~~/) { $1 }
518     else
519         html.gsub!(/~~previous_album~~/, '')
520         html.gsub!(/~~ifprevious_album\?~~(.+?)~~fi~~/, '')
521     end
522     if next_album
523         html.gsub!(/~~next_album~~/, '<a href="' + next_album + 'thumbnails.html">' + utf8(_('next album')) + '</a>')
524         html.gsub!(/~~ifnext_album\?~~(.+?)~~fi~~/) { $1 }
525     else
526         html.gsub!(/~~next_album~~/, '')
527         html.gsub!(/~~ifnext_album\?~~(.+?)~~fi~~/, '')
528     end
529     return html
530 end
531
532 def walk_source_dir
533
534     #- preprocess the path->dir, rexml is very slow with that; we seem to improve speed by 7%
535     optxpath = {}
536     $xmldoc.elements.each('//dir') { |elem|
537         optxpath[elem.attributes['path']] = elem
538     }
539
540     examined_dirs = $mode == 'merge_config_onedir' ? [ $onedir ] : `find '#{$source}' -type d -follow`.sort
541     info("directories: #{examined_dirs.length}, sizes: #{$images_size.length}")
542
543     examined_dirs.each { |dir|
544         dir.chomp!
545         if File.basename(dir) =~ /^\./
546             msg 1, _("Ignoring directory %s, begins with a dot (indicating a hidden directory)") % dir
547             next
548         end
549
550         if dir =~ /'/
551             die _("Source directory or sub-directories can't contain a single-quote character, sorry: %s") % dir
552         end
553
554         #- place xml document on proper node if exists, else create
555         xmldir = optxpath[utf8(dir)]
556         if $mode == 'use_config' || $mode == 'use_config_changetheme'
557             if !xmldir || (xmldir.attributes['already-generated'] && !$force) || xmldir.attributes['deleted']
558                 info("walking: #{dir}|#{$source}, 0 elements")
559                 next
560             end
561         else
562             if $mode == 'gen_config' || ($mode == 'merge_config' && !xmldir)
563                 #- add the <dir..> element if necessary
564                 parent = File.dirname(dir)
565                 xmldir = $xmldoc.elements["//dir[@path='#{utf8(parent)}']"]
566                 if !xmldir
567                     xmldir = $xmldoc.root
568                 end
569                 xmldir = optxpath[utf8(dir)] = xmldir.add_element('dir', { 'path' => utf8(dir) })
570             end
571         end
572         xmldir.delete_attribute('already-generated')
573
574         #- read images/videos entries from config or from directories depending on mode
575         entries = []
576         if $mode == 'use_config' || $mode == 'use_config_changetheme'
577             msg 2, _("Handling %s from config list...") % dir
578             xmldir.elements.each { |element|
579                 if %w(image video).include?(element.name) && !element.attributes['deleted']
580                     entries << from_utf8(element.attributes['filename'])
581                 end
582             }
583         else
584             msg 2, _("Examining %s...") % dir
585             entries = Dir.entries(dir).sort
586             #- populate config in case of gen_config, add new files in case of merge_config
587             for file in entries
588                 if file =~ /['"\[\]]/
589                     msg 1, _("Ignoring %s, contains one of forbidden characters: '\"[]") % "#{dir}/#{file}"
590                 else
591                     type = entry2type(file)
592                     if type && !xmldir.elements["#{type}[@filename='#{utf8(file)}']"]
593                         xmldir.add_element type, { "filename" => utf8(file), "caption" => $empty_comments ? '' : utf8(file.sub(/\.[^\.]+$/, '')[0..17]) }
594                     end
595                 end
596             end
597             if $mode != 'gen_config'
598                 #- cleanup removed files from config and reread entries from config to get proper ordering
599                 entries = []
600                 xmldir.elements.each { |element|
601                     fullpath = "#{dir}/#{from_utf8(element.attributes['filename'])}"
602                     if %w(image video).include?(element.name)
603                         if !File.readable?(fullpath)
604                             msg 1, _("Config merge: removing %s from config; use the backup file to retrieve caption info if this was a mistake") % fullpath
605                             xmldir.delete(element)
606                         elsif !element.attributes['deleted']
607                             entries << from_utf8(element.attributes['filename'])
608                         end
609                     end
610                 }
611                 #- if there is no more elements here, there is no album here anymore
612                 if !xmldir.child_byname_notattr('image', 'deleted') && !xmldir.child_byname_notattr('video', 'deleted')
613                     xmldir.delete_attribute('thumbnails-caption')
614                     xmldir.delete_attribute('thumbnails-captionfile')
615                 end
616             end
617         end
618         images = entries.find_all { |e| entry2type(e) == 'image' }
619         msg 3, _("\t%s images") % images.length
620         videos = entries.find_all { |e| entry2type(e) == 'video' }
621         msg 3, _("\t%s videos") % videos.length
622         info("walking: #{dir}|#{$source}, #{images.length + videos.length} elements")
623
624         dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote($source)}/, $dest))
625         system("mkdir -p '#{dest_dir}'")
626
627         #- pass through if there are no images and videos
628         if images.size == 0 && videos.size == 0
629             if !$forgui
630                 #- cleanup old images/videos, especially if this directory contained images/videos previously.
631                 themestuff = Dir.entries("#{$FPATH}/themes/#{$theme}").
632                                 find_all { |e| !%w(. .. skeleton_image.html skeleton_thumbnails.html skeleton_index.html metadata CVS).include?(e) }
633                 if $mode != 'gen_config'
634                     rightful_images = []
635                     if xmldir.attributes['thumbnails-caption']
636                         rightful_images << 'thumbnails-thumbnail.jpg'
637                     end
638                     xmldir.elements.each('dir') { |child|
639                         if child.attributes['deleted']
640                             next
641                         end
642                         subdir = make_dest_filename(from_utf8(File.basename(child.attributes['path'])))
643                         rightful_images << "thumbnails-#{subdir}.jpg"
644                     }
645                     to_del = Dir.entries(dest_dir).find_all { |e| !File.directory?("#{dest_dir}/#{e}") && !rightful_images.include?(e) } - themestuff
646                     if to_del.size > 0
647                         system("rm -f " + to_del.collect { |e| "#{dest_dir}/#{e}" }.join(' '))
648                     end
649                 end
650                 
651                 #- copy any resource file that goes with the theme (css, images..)
652                 themestuff.each { |entry|
653                     if !File.exists?("#{dest_dir}/#{entry}")
654                         psys("cp '#{$FPATH}/themes/#{$theme}/#{entry}' '#{dest_dir}'")
655                     end
656                 }
657             end
658             next
659         end
660
661         msg 2, _("Outputting in %s...") % dest_dir
662
663         #- populate data structure with sizes from theme
664         for sizeobj in $images_size
665             fullscreen_images ||= {}
666             fullscreen_images[sizeobj['name']] = []
667             thumbnail_images ||= {}
668             thumbnail_images[sizeobj['name']] = []
669             thumbnail_videos ||= {}
670             thumbnail_videos[sizeobj['name']] = []
671         end
672         if $limit_sizes =~ /original/
673             fullscreen_images['original'] = []
674         end
675
676         images.size >= 1 and msg 3, _("\tcreating images thumbnails...")
677
678         #- create thumbnails for images
679         images.each { |img|
680             info("processing element")
681             base_dest_img = dest_dir + '/' + make_dest_filename(img.sub(/\.[^\.]+$/, ''))
682             if $forgui
683                 thumbnail_dest_img = base_dest_img + "-#{$default_size['thumbnails']}.jpg"
684                 gen_thumbnails_element("#{dir}/#{img}", xmldir, true, [ { 'filename' => thumbnail_dest_img, 'size' => $default_size['thumbnails'] } ])
685             else
686                 todo = []
687                 for sizeobj in $images_size
688                     size_fullscreen = sizeobj['fullscreen']
689                     size_thumbnails = sizeobj['thumbnails']
690                     fullscreen_dest_img = base_dest_img + "-#{size_fullscreen}.jpg"
691                     thumbnail_dest_img  = base_dest_img + "-#{size_thumbnails}.jpg"
692                     fullscreen_images[sizeobj['name']] << File.basename(fullscreen_dest_img)
693                     thumbnail_images[sizeobj['name']]  << File.basename(thumbnail_dest_img)
694                     todo << { 'filename' => fullscreen_dest_img, 'size' => size_fullscreen }
695                     todo << { 'filename' => thumbnail_dest_img,  'size' => size_thumbnails }
696                 end
697                 gen_thumbnails_element("#{dir}/#{img}", xmldir, true, todo)
698                 if $limit_sizes =~ /original/
699                     fullscreen_images['original'] << img
700                 end
701                 destimg = "#{dest_dir}/#{img}"
702                 if $limit_sizes =~ /original/ && !File.exists?(destimg)
703                     psys("cp '#{dir}/#{img}' '#{destimg}'")
704                 end
705             end
706         }
707
708         videos.size >= 1 and msg 3, _("\tcreating videos thumbnails...")
709
710         #- create thumbnails for videos
711         videos.each { |video|
712             info("processing element")
713             if $forgui
714                 thumbnail_dest_img = dest_dir + '/' + make_dest_filename(video.sub(/\.[^\.]+$/, '')) + "-#{$default_size['thumbnails']}.jpg"
715                 gen_thumbnails_element("#{dir}/#{video}", xmldir, true, [ { 'filename' => thumbnail_dest_img, 'size' => $default_size['thumbnails'] } ])
716             else
717                 todo = []
718                 for sizeobj in $images_size
719                     size_thumbnails = sizeobj['thumbnails']
720                     thumbnail_dest_img = dest_dir + '/' + make_dest_filename(video.sub(/\.[^\.]+$/, '')) + "-#{size_thumbnails}.jpg"
721                     thumbnail_videos[sizeobj['name']] << File.basename(thumbnail_dest_img)
722                     todo << { 'filename' => thumbnail_dest_img, 'size' => size_thumbnails }
723                 end
724                 gen_thumbnails_element("#{dir}/#{video}", xmldir, true, todo)
725             end
726             destvideo = "#{dest_dir}/#{video}"
727             if !File.exists?(destvideo)
728                 psys("cp '#{dir}/#{video}' '#{destvideo}'")
729             end
730         }
731
732         if !$forgui
733             themestuff = Dir.entries("#{$FPATH}/themes/#{$theme}").
734                              find_all { |e| !%w(. .. skeleton_image.html skeleton_thumbnails.html skeleton_index.html metadata CVS).include?(e) }
735
736             #- cleanup old images/videos (for when removing elements or sizes)
737             all_elements = fullscreen_images.collect { |e| e[1] }.flatten.
738                      concat(thumbnail_images.collect { |e| e[1] }.flatten).
739                      concat(thumbnail_videos.collect { |e| e[1] }.flatten).
740                      concat(videos)
741             to_del = Dir.entries(dest_dir).find_all { |e| !File.directory?("#{dest_dir}/#{e}") && !all_elements.include?(e) && e !~ /^thumbnails-\w+\.jpg/ } - themestuff
742             if to_del.size > 0
743                 system("rm -f " + to_del.collect { |e| "#{dest_dir}/#{e}" }.join(' '))
744             end
745
746             #- copy any resource file that goes with the theme (css, images..)
747             themestuff.each { |entry|
748                 if !File.exists?("#{dest_dir}/#{entry}")
749                     psys("cp '#{$FPATH}/themes/#{$theme}/#{entry}' '#{dest_dir}'")
750                 end
751             }
752
753             msg 3, _("\tgenerating HTML pages...")
754
755             #- generate thumbnails.html (page with thumbnails)
756             for sizeobj in $images_size
757                 info("processing size")
758                 html = $html_thumbnails.collect { |l| l.clone }
759                 iterations = {}
760                 for i in html
761                     i.sub!(/~~run_slideshow~~/, images.size <= 1 ? '' : '<a href="image-' + size2js(sizeobj['name']) + '.html#run_slideshow=1">' + utf8(_('Run slideshow!'))+'</a>')
762                     i.sub!(/~~title~~/, xmldir.attributes['thumbnails-caption'] || utf8(File.basename(dir)))
763                     substitute_html_sizes(i, sizeobj, 'thumbnails')
764                     discover_iterations(iterations, i)
765                 end
766                 html_thumbnails = ''
767                 reset_iterations(iterations)
768                 for file in entries
769                     type = images.include?(file) ? 'image' : videos.include?(file) ? 'video' : nil
770                     if type
771                         html_thumbnails += run_iterations(iterations)
772                         if type == 'image'
773                             index = images.index(file)
774                             html_thumbnails.gsub!(/~~image_iteration~~/,
775                                                   '<a href="image-' + size2js(sizeobj['name']) + '.html#current=' + fullscreen_images[sizeobj['name']][index] +
776                                                       '" name="' + fullscreen_images[sizeobj['name']][index] + '">' +
777                                                       img_element("#{dest_dir}/#{thumbnail_images[sizeobj['name']][index]}") + '</a>')
778                             html_thumbnails.gsub!(/~~caption_iteration~~/,
779                                                   find_caption_value(xmldir, images[index]) || utf8(images[index]))
780                             html_thumbnails.gsub!(/~~ifimage\?~~(.+?)~~fi~~/) { $1 }
781                             html_thumbnails.gsub!(/~~ifvideo\?~~(.+?)~~fi~~/, '')
782                         elsif type == 'video'
783                             index = videos.index(file)
784                             if File.exists?("#{dest_dir}/#{thumbnail_videos[sizeobj['name']][index]}")
785                                 html_thumbnails.gsub!(/~~image_iteration~~/,
786                                                       '<a href="' + videos[index] + '">' + img_element("#{dest_dir}/#{thumbnail_videos[sizeobj['name']][index]}") + '</a>')
787                             else
788                                 html_thumbnails.gsub!(/~~image_iteration~~/,
789                                                       '<a href="' + videos[index] + '">' + utf8(_("(no preview)")) + '</a>')
790                             end
791                             html_thumbnails.gsub!(/~~caption_iteration~~/,
792                                                   find_caption_value(xmldir, videos[index]) || utf8(videos[index]))
793                             html_thumbnails.gsub!(/~~ifimage\?~~(.+?)~~fi~~/, '')
794                             html_thumbnails.gsub!(/~~ifvideo\?~~(.+?)~~fi~~/) { $1 }
795                         end
796                     end
797                 end
798                 html_thumbnails += close_iterations(iterations)
799                 for i in html
800                     i.sub!(/~~thumbnails~~/, html_thumbnails)
801                     i.gsub!(/~~theme~~/, $theme)
802                     i.gsub!(/~~current_size~~/, sizeobj['name'])
803                     i.gsub!(/~~current_size_js~~/, size2js(sizeobj['name']))
804                 end
805                 ios = File.open("#{dest_dir}/thumbnails-#{size2js(sizeobj['name'])}.html", "w")
806                 ios.write(html)
807                 ios.close
808             end
809
810             info("finished processing sizes")
811
812             #- generate "main" thumbnails.html page that will reload to correct size thanks to cookie
813             ios = File.open("#{dest_dir}/thumbnails.html", "w")
814             ios.write(html_reload_to_thumbnails)
815             ios.close
816
817             #- generate image.html (page with fullscreen images)
818             if images.size > 0
819                 captions4js = find_captions(xmldir, images).collect { |e| e ? '"' + e.gsub('"', '\"') + '"' : '""' }.join(', ')
820
821                 for sizeobj in $images_size
822                     html = $html_images.collect { |l| l.clone }
823                     images4js = fullscreen_images[sizeobj['name']].collect { |e| "\"#{e}\"" }.join(', ')
824                     otherimages4js = ''
825                     othersizes = []
826                     for sizeobj2 in all_images_sizes
827                         if sizeobj != sizeobj2
828                             otherimages4js += "var images_#{size2js(sizeobj2['name'])} = new Array(" + fullscreen_images[sizeobj2['name']].collect { |e| "\"#{e}\"" }.join(', ') + ")\n"
829                             othersizes << "\"#{size2js(sizeobj2['name'])}\""
830                         end
831                     end
832                     for i in html
833                         i.gsub!(/~~images~~/, images4js)
834                         i.gsub!(/~~other_images~~/, otherimages4js)
835                         i.gsub!(/~~other_sizes~~/, othersizes.join(', '))
836                         i.gsub!(/~~captions~~/, captions4js)
837                         i.gsub!(/~~title~~/, xmldir.attributes['thumbnails-caption'] || utf8(File.basename(dir)))
838                         i.gsub!(/~~thumbnails~~/, '<a href="thumbnails-' + size2js(sizeobj['name']) + '.html" id="thumbnails">' + utf8(_('return to thumbnails')) + '</a>')
839                         i.gsub!(/~~theme~~/, $theme)
840                         i.gsub!(/~~current_size~~/, size2js(sizeobj['name']))
841                         substitute_html_sizes(i, sizeobj, 'image')
842                     end
843                     ios = File.open("#{dest_dir}/image-#{size2js(sizeobj['name'])}.html", "w")
844                     ios.write(html)
845                     ios.close
846                 end
847             end
848         end
849     }
850
851     msg 3, ''
852
853     #- add attributes to <dir..> elements needing so
854     if $mode != 'use_config'
855         msg 3, _("\tfixating configuration file...")
856         $xmldoc.elements.each('//dir') { |element|
857             path = captionpath = element.attributes['path']
858             descendant_element = element.elements['descendant::image'] || element.elements['descendant::video']
859             if !descendant_element
860                 msg 3, _("\t\tremoving %s, no element in it") % path
861                 element.remove  #- means we have a directory with nothing interesting in it
862             else
863                 captionfile = "#{descendant_element.parent.attributes['path']}/#{descendant_element.attributes['filename']}"
864                 basename = File.basename(path)
865                 if element.elements['dir']
866                     if !element.attributes['subdirs-caption']
867                         element.add_attribute('subdirs-caption', basename)
868                     end
869                     if !element.attributes['subdirs-captionfile']
870                         element.add_attribute('subdirs-captionfile', captionfile)
871                     end
872                 end
873                 if element.child_byname_notattr('image', 'deleted') || element.child_byname_notattr('video', 'deleted')
874                     if !element.attributes['thumbnails-caption']
875                         element.add_attribute('thumbnails-caption', basename)
876                     end
877                     if !element.attributes['thumbnails-captionfile']
878                         element.add_attribute('thumbnails-captionfile', captionfile)
879                     end
880                 end
881             end
882         }
883     end
884
885     #- write down to disk config if necessary
886     if $config_writeto
887         ios = File.open($config_writeto, "w")
888         $xmldoc.write(ios, 0)
889         ios.close
890     end
891
892     if $forgui
893         msg 3, _(" completed necessary stuff for GUI, exiting.")
894         return
895     end
896
897     #- second pass to create index.html files and previous/next links
898     info("creating index.html")
899     msg 3, _("\trescanning directories to generate all `index.html' files...")
900
901     #- recompute the memoization because elements mights have been removed (the ones with no element in them)
902     optxpath = {}
903     $xmldoc.elements.each('//dir') { |elem|
904         optxpath[elem.attributes['path']] = elem
905     }
906
907     examined_dirs.each { |dir|
908         dir.chomp!
909         info("index.html: #{dir}|#{$source}")
910
911         xmldir = optxpath[utf8(dir)]
912         if !xmldir || (xmldir.attributes['already-generated'] && !$force) || xmldir.attributes['deleted']
913             next
914         end
915         dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote($source)}/, $dest))
916
917         previous_album = find_previous_album(xmldir)
918         next_album = find_next_album(xmldir)
919
920         if xmldir.elements['dir']
921             html = $html_index.collect { |l| l.clone }
922             iterations = {}
923             for i in html
924                 caption = xmldir.attributes['subdirs-caption']
925                 i.gsub!(/~~title~~/, caption)
926                 if xmldir.parent.name == 'dir'
927                     nav = ''
928                     path = '..'
929                     parent = xmldir.parent
930                     while parent.name == 'dir'
931                         parentcaption = parent.attributes['subdirs-caption']
932                         nav = "<a href='#{path}/index.html'>#{parentcaption}</a> #{utf8(_(" > "))} #{nav}"
933                         path += '/..'
934                         parent = parent.parent
935                     end
936                     i.gsub!(/~~ifnavigation\?~~(.+?)~~fi~~/) { $1 }
937                     i.gsub!(/~~navigation~~/, nav + caption)
938                 else
939                     i.gsub!(/~~ifnavigation\?~~(.+?)~~fi~~/, '')
940                 end
941                 discover_iterations(iterations, i)
942             end
943             
944             html_index = ''
945             reset_iterations(iterations)
946             
947             #- deal with "current" album (directs to "thumbnails" page)
948             if xmldir.attributes['thumbnails-caption']
949                 thumbnail = "#{dest_dir}/thumbnails-thumbnail.jpg"
950                 gen_thumbnails_subdir(from_utf8(xmldir.attributes['thumbnails-captionfile']), xmldir, false,
951                                       [ { 'filename' => thumbnail, 'size' => $albums_thumbnail_size } ], 'thumbnails')
952                 html_index += run_iterations(iterations)
953                 html_index.gsub!(/~~image_iteration~~/, "<a href='thumbnails.html'>" + img_element(thumbnail) + '</a>')
954                 html_index.gsub!(/~~caption_iteration~~/, xmldir.attributes['thumbnails-caption'])
955             end
956
957             #- deal with sub-albums (direct to subdirs/index.html pages)
958             xmldir.elements.each('dir') { |child|
959                 if child.attributes['deleted']
960                     next
961                 end
962                 subdir = make_dest_filename(from_utf8(File.basename(child.attributes['path'])))
963                 thumbnail = "#{dest_dir}/thumbnails-#{subdir}.jpg"
964                 html_index += run_iterations(iterations)
965                 captionfile, caption = find_subalbum_caption_info(child)
966                 gen_thumbnails_subdir(captionfile, child, false,
967                                       [ { 'filename' => thumbnail, 'size' => $albums_thumbnail_size } ], find_subalbum_info_type(child))
968                 html_index.gsub!(/~~caption_iteration~~/, caption)
969                 html_index.gsub!(/~~image_iteration~~/, "<a href='#{make_dest_filename(subdir)}/index.html'>" + img_element(thumbnail) + '</a>')
970             }
971
972             html_index += close_iterations(iterations)
973
974             for i in html
975                 i.gsub!(/~~thumbnails~~/, html_index)
976             end
977             
978         else
979             html = html_reload_to_thumbnails
980         end
981
982         ios = File.open("#{dest_dir}/index.html", "w")
983         ios.write(html)
984         ios.close
985
986         #- substitute "return to albums" and previous/next correctly
987         if xmldir.child_byname_notattr('image', 'deleted') || xmldir.child_byname_notattr('video', 'deleted')
988             for sizeobj in $images_size
989                 substInFile("#{dest_dir}/thumbnails-#{size2js(sizeobj['name'])}.html") { |line|
990                     sub_previous_next_album(previous_album, next_album, line)
991                     if xmldir.elements['dir']
992                         line.sub!(/~~return_to_albums~~/, '<a href="index.html">' + utf8(_('return to albums')) + '</a>')
993                     else
994                         if xmldir.parent.name == 'dir'
995                             line.sub!(/~~return_to_albums~~/, '<a href="../index.html">' + utf8(_('return to albums')) + '</a>')
996                         else
997                             line.sub!(/~~return_to_albums~~/, '')
998                         end
999                     end
1000                     line
1001                 }
1002                 if xmldir.child_byname_notattr('image', 'deleted')
1003                     substInFile("#{dest_dir}/image-#{size2js(sizeobj['name'])}.html") { |line|
1004                         sub_previous_next_album(previous_album, next_album, line)
1005                     }
1006                 end
1007             end
1008         end
1009     }
1010
1011     msg 3, _(" all done.")
1012 end
1013
1014 handle_options
1015 read_config
1016 check_installation
1017
1018 build_html_skeletons
1019
1020 walk_source_dir