optimize a lot album generation by not regenerating when places are already generated
[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     [ '--no-check',      '-n', GetoptLong::NO_ARGUMENT,       _("Don't check for needed external programs at startup") ],
40
41     [ '--source',        '-s', GetoptLong::REQUIRED_ARGUMENT, _("Directory which contains original images/videos as files or subdirs") ],
42     [ '--destination',   '-d', GetoptLong::REQUIRED_ARGUMENT, _("Directory which will contain the web-album") ],
43 #    [ '--clean',         '-c', GetoptLong::NO_ARGUMENT,       _("Clean destination directory") ],
44
45     [ '--theme',         '-t', GetoptLong::REQUIRED_ARGUMENT, _("Select HTML theme to use") ],
46     [ '--config',        '-C', GetoptLong::REQUIRED_ARGUMENT, _("File containing config listing images and videos within directories with captions") ],
47     [ '--config-skel',   '-k', GetoptLong::REQUIRED_ARGUMENT, _("Filename where the script will output a config skeleton") ],
48     [ '--merge-config',  '-M', GetoptLong::REQUIRED_ARGUMENT, _("File containing config listing, where to merge new images/videos from --source, and change theme info") ],
49     [ '--merge-config-newdirs', '-N', GetoptLong::REQUIRED_ARGUMENT, _("File containing config listing, where to merge new subdirs from --source") ],
50     [ '--merge-config-onedir',  '-O', GetoptLong::REQUIRED_ARGUMENT, _("File containing config listing, for merging the subdir specified with --dir") ],
51     [ '--dir',           '-D', GetoptLong::REQUIRED_ARGUMENT, _("Directory for merge with --merge-config-onedir") ],
52     [ '--use-config',    '-u', GetoptLong::REQUIRED_ARGUMENT, _("File containing config listing, where to change theme info") ],
53     [ '--force',         '-f', GetoptLong::NO_ARGUMENT,       _("Force generation of album even if the GUI markes some directories as already generated") ],
54
55     [ '--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)") ],
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, %-15s %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 '--no-check'
96                 $no_check = true
97
98             when '--source'
99                 $source = File.expand_path(arg.sub(%r|/$|, ''))
100                 if !File.directory?($source)
101                     die _("Argument to --source must be a directory")
102                 end
103             when '--destination'
104                 $dest = File.expand_path(arg.sub(%r|/$|, ''))
105                 if File.exists?($dest) && !File.directory?($dest)
106                     die _("If --destination exists, it must be a directory")
107                 end
108                 if $dest != make_dest_filename($dest)
109                     die _("Sorry, destination directory can't contain non simple alphanumeric characters.")
110                 end
111 #            when '--clean'
112 #                system("rm -rf #{$dest}")
113
114             when '--theme'
115                 $theme = arg
116             when '--config'
117                 arg = File.expand_path(arg)
118                 if File.readable?(arg)
119                     $xmldoc = REXML::Document.new File.new(arg)
120                     $mode = 'use_config'
121                 else
122                     die _('Config file does not exist or is unreadable.')
123                 end
124             when '--config-skel'
125                 arg = File.expand_path(arg)
126                 if File.exists?(arg)
127                     msg 1, _("Config skeleton file already exists, backuping to %s.backup") % arg
128                     File.rename(arg, "#{arg}.backup")
129                 end
130                 $config_writeto = arg
131                 $mode = 'gen_config'
132             when '--merge-config'
133                 arg = File.expand_path(arg)
134                 if File.readable?(arg)
135                     msg 2, _("Merge config notice: backuping current config file to %s.backup") % arg
136                     $xmldoc = REXML::Document.new File.new(arg)
137                     File.rename(arg, "#{arg}.backup")
138                     $config_writeto = arg
139                     $mode = 'merge_config'
140                 else
141                     die _('Config file does not exist or is unreadable.')
142                 end
143             when '--merge-config-newdirs'
144                 arg = File.expand_path(arg)
145                 if File.readable?(arg)
146                     msg 2, _("Merge config notice: backuping current config file to %s.backup") % arg
147                     $xmldoc = REXML::Document.new File.new(arg)
148                     File.rename(arg, "#{arg}.backup")
149                     $config_writeto = arg
150                     $mode = 'merge_config_newdirs'
151                 else
152                     die _('Config file does not exist or is unreadable.')
153                 end
154             when '--merge-config-onedir'
155                 arg = File.expand_path(arg)
156                 if File.readable?(arg)
157                     msg 2, _("Merge config notice: backuping current config file to %s.backup") % arg
158                     $xmldoc = REXML::Document.new File.new(arg)
159                     File.rename(arg, "#{arg}.backup")
160                     $config_writeto = arg
161                     $mode = 'merge_config_onedir'
162                 else
163                     die _('Config file does not exist or is unreadable.')
164                 end
165             when '--dir'
166                 arg = File.expand_path(arg)
167                 if !File.readable?(arg)
168                     die _('Specified directory to merge with --dir is not readable')
169                 else
170                     $onedir = arg
171                 end
172             when '--use-config'
173                 arg = File.expand_path(arg)
174                 if File.readable?(arg)
175                     msg 2, _("Use config notice: backuping current config file to %s.backup") % arg
176                     $xmldoc = REXML::Document.new File.new(arg)
177                     File.rename(arg, "#{arg}.backup")
178                     $config_writeto = arg
179                     $mode = 'use_config_changetheme'
180                 else
181                     die _('Config file does not exist or is unreadable.')
182                 end
183
184             when '--sizes'
185                 $limit_sizes = arg
186
187             when '--force'
188                 $force = true
189
190             when '--mproc'
191                 $mproc = arg.to_i
192                 $pids = []
193
194             when '--for-gui'
195                 $forgui = true
196
197             when '--verbose-level'
198                 $verbose_level = arg.to_i
199
200             when '--info-pipe'
201                 $info_pipe = File.open(arg, File::WRONLY)
202                 $info_pipe.sync = true
203             end
204         end
205     rescue
206         puts $!
207         usage
208         exit(1)
209     end
210
211     if !$source && $xmldoc
212         $source = from_utf8($xmldoc.root.attributes['source'])
213         $dest = from_utf8($xmldoc.root.attributes['destination'])
214         $theme = $xmldoc.root.attributes['theme']
215         $limit_sizes ||= $xmldoc.root.attributes['limit-sizes']
216     end
217
218     if $mode == 'merge_config_onedir' && !$onedir
219         die _("Missing --dir for --merge_config_onedir")
220     end
221
222     if !$source
223         die _("Missing --source or --config parameter.")
224     end
225     if !$dest
226         die _("Missing --destination parameter.")
227     end
228     if !$theme
229         $theme = 'simple'
230     end
231
232     select_theme($theme, $limit_sizes)
233
234     if !$xmldoc
235         additional_params = ''
236         if $limit_sizes
237             additional_params += "limit-sizes='#{$limit_sizes}'"
238         end
239         $xmldoc = Document.new "<booh version='#{$VERSION}' source='#{utf8($source)}' destination='#{utf8($dest)}' theme='#{$theme}' #{additional_params}/>"
240         $xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
241         $mode = 'gen_config'
242     end
243
244     if $mode == 'merge_config' || $mode == 'use_config_changetheme'
245         $xmldoc.root.add_attribute('theme', $theme)
246         if $limit_sizes
247             $xmldoc.root.add_attribute('limit-sizes', $limit_sizes)
248         else
249             $xmldoc.root.delete_attribute('limit-sizes')
250         end
251     end
252 end
253
254 def read_config
255     $config = {}
256 end
257
258 def write_config
259 end
260
261 def info(value)
262     if $info_pipe
263         $info_pipe.puts(value)
264     end
265 end
266
267 def check_installation
268     if $no_check
269         return
270     end
271     missing = %w(convert identify exif transcode mencoder).delete_if { |prg| system("which #{prg} >/dev/null 2>/dev/null") }
272     if missing != []
273         die _("The following programs are typically needed: `%s'. Re-run with --no-check if you're sure you're fine without them.") % missing.join(', ')
274     end
275 end
276
277 def replace_line(surround, keyword, line)
278     begin
279         contents = eval "$#{keyword}"
280         line.sub!(/#{surround}#{keyword}#{surround}/, contents)
281     rescue TypeError
282         die _("No `%s' found for substitution") % keyword
283     end
284 end
285
286 def build_html_skeletons
287     $html_images     = File.open("#{$FPATH}/themes/#{$theme}/skeleton_image.html").readlines
288     $html_thumbnails = File.open("#{$FPATH}/themes/#{$theme}/skeleton_thumbnails.html").readlines
289     $html_index      = File.open("#{$FPATH}/themes/#{$theme}/skeleton_index.html").readlines
290     for line in $html_images + $html_thumbnails + $html_index
291         while line =~ /~~~(\w+)~~~/
292             replace_line('~~~', $1, line)
293         end
294     end
295 end
296
297 def find_caption_value(xmldir, filename)
298     if cap = xmldir.elements["[@filename='#{utf8(filename)}']"].attributes['caption']
299         return cap.gsub("\n", '<br/>')
300     else
301         return nil
302     end
303 end
304
305 def find_captions(xmldir, images)
306     return images.collect { |img| find_caption_value(xmldir, img) }
307 end
308
309 #- stolen from CVSspam
310 def urlencode(text)
311   text.sub(/[^a-zA-Z0-9\-,.*_\/]/) do
312     "%#{sprintf('%2X', $&[0])}"
313   end
314 end
315
316 def html_reload_to_thumbnails
317     html_reload_to_thumbnails = $preferred_size_reloader.clone
318     html_reload_to_thumbnails.gsub!(/~~theme~~/, $theme)
319     html_reload_to_thumbnails.gsub!(/~~default_size~~/, $default_size['name'])
320     return html_reload_to_thumbnails
321 end
322
323 def discover_iterations(iterations, line)
324     if line =~ /~~iterate(\d)_open(_max(\d+))?~~/
325         for iter in iterations.values
326             if iter['open']
327                 iter['open'] = false
328                 iter['close_wait'] = $1.to_i
329             end
330         end
331         iterations[$1.to_i] = { 'open' => true, 'max' => $3, 'opening' => '', 'closing' => '' }
332         if $1.to_i == 1
333             line.sub!(/.*/, '~~thumbnails~~')
334         else
335             line.sub!(/.*/, '')
336         end
337     elsif line =~ /~~iterate(\d)_close~~/
338         iterations[$1.to_i]['open']  = false;
339         iterations[$1.to_i]['close'] = true;
340         line.sub!(/.*/, '')
341     else
342         for iter in iterations.values
343             if iter['open']
344                 iter['opening'] += line
345                 line.sub!(/.*/, '')
346             end
347             if !iter['close'] && iter['close_wait'] && iterations[iter['close_wait']]['close']
348                 iter['closing'] += line
349                 line.sub!(/.*/, '')
350             end
351         end
352     end
353 end
354
355 def reset_iterations(iterations)
356     for iter in iterations.values
357         iter['value'] = 1
358     end
359 end
360
361 def run_iterations(iterations)
362     html = ''
363     for level in iterations.keys.sort
364         if iterations[level]['value'] == 1 || level == iterations.keys.max
365             html += iterations[level]['opening']
366         end
367         iterations[level]['value'] += 1
368         if iterations[level]['max'] && iterations[level]['value'] > iterations[level]['max'].to_i
369             iterations[level]['value'] = 1
370             iterations[level-1]['value'] = 1
371             html += iterations[level-1]['closing']
372         end
373     end
374     return html
375 end
376
377 def close_iterations(iterations)
378     html = ''
379     for level in iterations.keys.sort.reverse
380         html += iterations[level]['closing']
381     end
382     return html
383 end
384
385 def img_element(fullpath)
386     if size = get_image_size(fullpath)
387         sizespec = 'width="' + size[:x].to_s + '" height="' + size[:y].to_s + '"'
388     else
389         sizespec = ''
390     end
391     return '<img src="' + File.basename(fullpath) + '" ' + sizespec + ' border="0"/>'
392 end
393
394 def size2js(name)
395     return name.gsub(/-/, '')
396 end
397
398 def substitute_html_sizes(html, sizeobj, type)
399     sizestrings = []
400     if $images_size.length > 1 || (type == 'image' && $limit_sizes =~ /original/)
401         for sizeobj2 in $images_size
402             if sizeobj != sizeobj2
403                 if type == 'thumbnails'
404                     sizestrings << '<a href="thumbnails-' + sizeobj2['name'] + '.html">' + sizename(sizeobj2['name']) + '</a>'
405                 else
406                     sizestrings << '<a id="link' + size2js(sizeobj2['name']) + '">' + sizename(sizeobj2['name']) + '</a>'
407                 end
408             else
409                 sizestrings << sizename(sizeobj2['name'])
410             end
411         end
412         if type == 'image' && $limit_sizes =~ /original/
413             sizestrings << '<a id="linkoriginal" target="newframe">' + sizename('original') + '</a>'
414         end
415     end
416     html.sub!(/~~sizes~~(.+)~~/) { sizestrings.join($1) }
417 end
418
419 def xmldir2destdir(xmldir)
420     return make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))
421 end
422
423 def find_previous_album(xmldir)
424     relative_pos = ''
425     begin
426         #- move to previous dir element if exists
427         if xmldir.previous_element_byname('dir')
428             xmldir = xmldir.previous_element_byname('dir')
429             relative_pos += '../' + xmldir2destdir(xmldir) + '/'
430             child = nil
431             #- after having moved to previous dir, we need to go down last subdir until the last one
432             while child = xmldir.elements['dir']
433                 while nextchild = child.next_element_byname('dir')
434                     child = nextchild
435                 end
436                 relative_pos += xmldir2destdir(child) + '/'
437                 xmldir = child
438             end
439         else
440             #- previous dir doesn't exist, move to previous dir element if exists
441             xmldir = xmldir.parent
442             if xmldir.name == 'dir'
443                 relative_pos += '../'
444             else
445                 return nil
446             end
447         end
448     end while !xmldir.elements['image'] && !xmldir.elements['video']
449     return File.reduce_path(relative_pos)
450 end
451
452 def find_next_album(xmldir)
453     relative_pos = ''
454     begin
455         #- first child dir element (catches when initial xmldir has both thumbnails and subdirs)
456         if firstchild = xmldir.elements['dir']
457             xmldir = firstchild
458             relative_pos += xmldir2destdir(xmldir) + '/'
459         #- next brother
460         elsif nextbro = xmldir.next_element_byname('dir')
461             xmldir = nextbro
462             relative_pos += '../' + xmldir2destdir(xmldir) + '/'
463         else
464             #- go up until we have a next brother or we are finished
465             begin
466                 xmldir = xmldir.parent
467                 relative_pos += '../'
468             end while xmldir && !xmldir.next_element_byname('dir')
469             if xmldir
470                 xmldir = xmldir.next_element_byname('dir')
471                 relative_pos += '../' + xmldir2destdir(xmldir) + '/'
472             else
473                 return nil
474             end
475         end
476     end while !xmldir.elements['image'] && !xmldir.elements['video']
477     return File.reduce_path(relative_pos)
478 end
479
480 def sub_previous_next_album(previous_album, next_album, html)
481     if previous_album
482         html.gsub!(/~~previous_album~~/, '<a href="' + previous_album + 'thumbnails.html">' + utf8(_('previous album')) + '</a>')
483         html.gsub!(/~~ifprevious_album\?~~(.+?)~~fi~~/) { $1 }
484     else
485         html.gsub!(/~~previous_album~~/, '')
486         html.gsub!(/~~ifprevious_album\?~~(.+?)~~fi~~/, '')
487     end
488     if next_album
489         html.gsub!(/~~next_album~~/, '<a href="' + next_album + 'thumbnails.html">' + utf8(_('next album')) + '</a>')
490         html.gsub!(/~~ifnext_album\?~~(.+?)~~fi~~/) { $1 }
491     else
492         html.gsub!(/~~next_album~~/, '')
493         html.gsub!(/~~ifnext_album\?~~(.+?)~~fi~~/, '')
494     end
495 end
496
497 def walk_source_dir
498
499     #- preprocess the path->dir, rexml is very slow with that; we seem to improve speed by 7%
500     optxpath = {}
501     $xmldoc.elements.each('//dir') { |elem|
502         optxpath[elem.attributes['path']] = elem
503     }
504
505     examined_dirs = $mode == 'merge_config_onedir' ? [ $onedir ] : `find '#{$source}' -type d`.sort
506     info("directories: #{examined_dirs.length}, sizes: #{$images_size.length}")
507
508     examined_dirs.each { |dir|
509         dir.chomp!
510         if File.basename(dir) =~ /^\./
511             msg 1, _("Ignoring directory %s, begins with a dot (indicating a hidden directory)") % dir
512             next
513         end
514
515         if dir =~ /'/
516             die _("Source directory or sub-directories can't contain a single-quote character, sorry: %s") % dir
517         end
518
519         #- place xml document on proper node if exists, else create
520         newly_created_dir = false
521         xmldir = optxpath[utf8(dir)]
522         if $mode == 'use_config' || $mode == 'use_config_changetheme'
523             if !xmldir || (xmldir.attributes['already-generated'] && !$force)
524                 info("walking: #{dir}, 0 elements")
525                 next
526             end
527         else
528             if $mode == 'gen_config' || (($mode == 'merge_config' || $mode == 'merge_config_newdirs') && !xmldir)
529                 #- add the <dir..> element if necessary
530                 newly_created_dir = true
531                 parent = File.dirname(dir)
532                 xmldir = $xmldoc.elements["//dir[@path='#{utf8(parent)}']"]
533                 if !xmldir
534                     xmldir = $xmldoc.root
535                 end
536                 xmldir = optxpath[utf8(dir)] = xmldir.add_element('dir', { 'path' => utf8(dir) })
537             end
538         end
539         xmldir.delete_attribute('already-generated')
540
541         #- read images/videos entries from config or from directories depending on mode
542         entries = []
543         if $mode == 'use_config' || $mode == 'use_config_changetheme' || ($mode == 'merge_config_newdirs' && !newly_created_dir)
544             msg 2, _("Handling %s from config list...") % dir
545             xmldir.elements.each { |element|
546                 if %w(image video).include?(element.name)
547                     entries << from_utf8(element.attributes['filename'])
548                 end
549             }
550         else
551             msg 2, _("Examining %s...") % dir
552             entries = Dir.entries(dir).sort
553             #- populate config in case of gen_config, add new files in case of merge_config
554             for file in entries
555                 if file =~ /['"\[\]]/
556                     msg 1, _("Ignoring %s, contains one of forbidden characters: '\"[]") % "#{dir}/#{file}"
557                 else
558                     type = entry2type(file)
559                     if type && !xmldir.elements["#{type}[@filename='#{utf8(file)}']"]
560                         xmldir.add_element type, { "filename" => utf8(file), "caption" => utf8(file.sub(/\.[^\.]+$/, '')[0..17]) }
561                     end
562                 end
563             end
564             if $mode != 'gen_config'
565                 #- cleanup removed files from config and reread entries from config to get proper ordering
566                 entries = []
567                 xmldir.elements.each { |element|
568                     fullpath = "#{dir}/#{from_utf8(element.attributes['filename'])}"
569                     if %w(image video).include?(element.name)
570                         if !File.readable?(fullpath)
571                             msg 1, _("Config merge: removing %s from config; use the backup file to retrieve caption info if this was a mistake") % fullpath
572                             xmldir.delete(element)
573                         else
574                             entries << from_utf8(element.attributes['filename'])
575                         end
576                     end
577                 }
578             end
579         end
580         images = entries.find_all { |e| entry2type(e) == 'image' }
581         msg 3, _("\t%s images") % images.length
582         videos = entries.find_all { |e| entry2type(e) == 'video' }
583         msg 3, _("\t%s videos") % videos.length
584         info("walking: #{dir}, #{images.length + videos.length} elements")
585
586         dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote($source)}/, $dest))
587         system("mkdir -p '#{dest_dir}'")
588
589         #- pass through if there are no images and videos
590         if images.size == 0 && videos.size == 0
591             next
592         end
593
594         msg 2, _("Outputting in %s...") % dest_dir
595
596         #- populate data structure with sizes from theme
597         for sizeobj in $images_size
598             fullscreen_images ||= {}
599             fullscreen_images[sizeobj['name']] = []
600             thumbnail_images ||= {}
601             thumbnail_images[sizeobj['name']] = []
602             thumbnail_videos ||= {}
603             thumbnail_videos[sizeobj['name']] = []
604         end
605         if $limit_sizes =~ /original/
606             fullscreen_images['original'] = []
607         end
608
609         images.size >= 1 and msg 3, _("\tcreating images thumbnails...")
610
611         #- create thumbnails for images
612         images.each { |img|
613             info("processing element")
614             base_dest_img = dest_dir + '/' + make_dest_filename(img.sub(/\.[^\.]+$/, ''))
615             if $forgui
616                 thumbnail_dest_img = base_dest_img + "-#{$default_size['thumbnails']}.jpg"
617                 gen_thumbnails_element("#{dir}/#{img}", xmldir, true, [ { 'filename' => thumbnail_dest_img, 'size' => $default_size['thumbnails'] } ])
618             else
619                 for sizeobj in $images_size
620                     size_fullscreen = sizeobj['fullscreen']
621                     size_thumbnails = sizeobj['thumbnails']
622                     fullscreen_dest_img = base_dest_img + "-#{size_fullscreen}.jpg"
623                     thumbnail_dest_img  = base_dest_img + "-#{size_thumbnails}.jpg"
624                     fullscreen_images[sizeobj['name']] << File.basename(fullscreen_dest_img)
625                     thumbnail_images[sizeobj['name']]  << File.basename(thumbnail_dest_img)
626                     gen_thumbnails_element("#{dir}/#{img}", xmldir, true, [ { 'filename' => fullscreen_dest_img, 'size' => size_fullscreen },
627                                                                             { 'filename' => thumbnail_dest_img, 'size' => size_thumbnails } ])
628                 end
629                 if $limit_sizes =~ /original/
630                     fullscreen_images['original'] << img
631                 end
632                 destimg = "#{dest_dir}/#{img}"
633                 if $limit_sizes =~ /original/ && !File.exists?(destimg)
634                     psys("cp '#{dir}/#{img}' '#{destimg}'")
635                 end
636             end
637         }
638
639         videos.size >= 1 and msg 3, _("\tcreating videos thumbnails...")
640
641         #- create thumbnails for videos
642         videos.each { |video|
643             info("processing element")
644             thumbnail_ok = true
645             if $forgui
646                 thumbnail_dest_img = dest_dir + '/' + make_dest_filename(video.sub(/\.[^\.]+$/, '')) + "-#{$default_size['thumbnails']}.jpg"
647                 thumbnail_ok &&= gen_thumbnails_element("#{dir}/#{video}", xmldir, true, [ { 'filename' => thumbnail_dest_img, 'size' => $default_size['thumbnails'] } ])
648             else
649                 for sizeobj in $images_size
650                     size_thumbnails = sizeobj['thumbnails']
651                     thumbnail_dest_img = dest_dir + '/' + make_dest_filename(video.sub(/\.[^\.]+$/, '')) + "-#{size_thumbnails}.jpg"
652                     thumbnail_videos[sizeobj['name']] << File.basename(thumbnail_dest_img)
653                     thumbnail_ok &&= gen_thumbnails_element("#{dir}/#{video}", xmldir, true, [ { 'filename' => thumbnail_dest_img, 'size' => size_thumbnails } ])
654                 end
655             end
656             destvideo = "#{dest_dir}/#{video}"
657             if !File.exists?(destvideo)
658                 psys("cp '#{dir}/#{video}' '#{destvideo}'")
659             end
660             #- cleanup temp
661             system("rm -f #{dest_dir}/screenshot.jpg000000.jpg")
662         }
663
664         if !$forgui
665             #- copy any resource file that goes with the theme (css, images..)
666             for entry in Dir.entries("#{$FPATH}/themes/#{$theme}")
667                 if !%w(. .. skeleton_image.html skeleton_thumbnails.html skeleton_index.html metadata CVS).include?(entry)
668                     if !File.exists?("#{dest_dir}/#{entry}")
669                         psys("cp '#{$FPATH}/themes/#{$theme}/#{entry}' '#{dest_dir}'")
670                     end
671                 end
672             end
673
674             msg 3, _("\tgenerating HTML pages...")
675
676             previous_album = find_previous_album(xmldir)
677             next_album = find_next_album(xmldir)
678             
679             #- generate thumbnails.html (page with thumbnails)
680             for sizeobj in $images_size
681                 info("processing size")
682                 html = $html_thumbnails.collect { |l| l.clone }
683                 iterations = {}
684                 for i in html
685                     i.sub!(/~~run_slideshow~~/, images.size <= 1 ? '' : '<a href="image-' + sizeobj['name'] + '.html?run_slideshow">' + utf8(_('Run slideshow!')) + '</a>')
686                     i.sub!(/~~title~~/, xmldir.attributes['thumbnails-caption'] || utf8(File.basename(dir)))
687                     substitute_html_sizes(i, sizeobj, 'thumbnails')
688                     discover_iterations(iterations, i)
689                 end
690                 html_thumbnails = ''
691                 reset_iterations(iterations)
692                 for file in entries
693                     type = images.include?(file) ? 'image' : videos.include?(file) ? 'video' : nil
694                     if type
695                         html_thumbnails += run_iterations(iterations)
696                         if type == 'image'
697                             index = images.index(file)
698                             html_thumbnails.gsub!(/~~image_iteration~~/,
699                                                   '<a href="image-' + size2js(sizeobj['name']) + '.html?current=' + fullscreen_images[sizeobj['name']][index] +
700                                                       '" name="' + fullscreen_images[sizeobj['name']][index] + '">' +
701                                                       img_element("#{dest_dir}/#{thumbnail_images[sizeobj['name']][index]}") + '</a>')
702                             html_thumbnails.gsub!(/~~caption_iteration~~/,
703                                                   find_caption_value(xmldir, images[index]) || utf8(images[index]))
704                             html_thumbnails.gsub!(/~~ifimage\?~~(.+?)~~fi~~/) { $1 }
705                             html_thumbnails.gsub!(/~~ifvideo\?~~(.+?)~~fi~~/, '')
706                         elsif type == 'video'
707                             index = videos.index(file)
708                             if File.exists?("#{dest_dir}/#{thumbnail_videos[sizeobj['name']][index]}")
709                                 html_thumbnails.gsub!(/~~image_iteration~~/,
710                                                       '<a href="' + videos[index] + '">' + img_element("#{dest_dir}/#{thumbnail_videos[sizeobj['name']][index]}") + '</a>')
711                             else
712                                 html_thumbnails.gsub!(/~~image_iteration~~/,
713                                                       '<a href="' + videos[index] + '">' + utf8(_("(no preview)")) + '</a>')
714                             end
715                             html_thumbnails.gsub!(/~~caption_iteration~~/,
716                                                   find_caption_value(xmldir, videos[index]) || utf8(videos[index]))
717                             html_thumbnails.gsub!(/~~ifimage\?~~(.+?)~~fi~~/, '')
718                             html_thumbnails.gsub!(/~~ifvideo\?~~(.+?)~~fi~~/) { $1 }
719                         end
720                     end
721                 end
722                 html_thumbnails += close_iterations(iterations)
723                 for i in html
724                     i.sub!(/~~thumbnails~~/, html_thumbnails)
725                     i.gsub!(/~~theme~~/, $theme)
726                     i.gsub!(/~~current_size~~/, sizeobj['name'])
727                     sub_previous_next_album(previous_album, next_album, i)
728                 end
729                 ios = File.open("#{dest_dir}/thumbnails-#{sizeobj['name']}.html", "w")
730                 ios.write(html)
731                 ios.close
732             end
733
734             info("finished processing sizes")
735
736             #- generate "main" thumbnails.html page that will reload to correct size thanks to cookie
737             ios = File.open("#{dest_dir}/thumbnails.html", "w")
738             ios.write(html_reload_to_thumbnails)
739             ios.close
740
741             #- generate image.html (page with fullscreen images)
742             if images.size > 0
743                 captions4js = find_captions(xmldir, images).collect { |e| e ? '"' + e.gsub('"', '\"') + '"' : '""' }.join(', ')
744
745                 for sizeobj in $images_size
746                     html = $html_images.collect { |l| l.clone }
747                     images4js = fullscreen_images[sizeobj['name']].collect { |e| "\"#{e}\"" }.join(', ')
748                     otherimages4js = ''
749                     othersizes = []
750                     all_images_sizes = $limit_sizes =~ /original/ ? $images_size + [ { 'name' => 'original' } ] : $images_size
751                     for sizeobj2 in all_images_sizes
752                         if sizeobj != sizeobj2
753                             otherimages4js += "var images_#{size2js(sizeobj2['name'])} = new Array(" + fullscreen_images[sizeobj2['name']].collect { |e| "\"#{e}\"" }.join(', ') + ")\n"
754                             othersizes << "\"#{size2js(sizeobj2['name'])}\""
755                         end
756                     end
757                     for i in html
758                         i.gsub!(/~~images~~/, images4js)
759                         i.gsub!(/~~other_images~~/, otherimages4js)
760                         i.gsub!(/~~other_sizes~~/, othersizes.join(', '))
761                         i.gsub!(/~~captions~~/, captions4js)
762                         i.gsub!(/~~title~~/, xmldir.attributes['thumbnails-caption'] || utf8(File.basename(dir)))
763                         i.gsub!(/~~thumbnails~~/, '<a href="thumbnails-' + sizeobj['name'] + '.html" id="thumbnails">' + utf8(_('return to thumbnails')) + '</a>')
764                         sub_previous_next_album(previous_album, next_album, i)
765                         i.gsub!(/~~theme~~/, $theme)
766                         i.gsub!(/~~current_size~~/, sizeobj['name'])
767                         substitute_html_sizes(i, sizeobj, 'image')
768                     end
769                     ios = File.open("#{dest_dir}/image-#{size2js(sizeobj['name'])}.html", "w")
770                     ios.write(html)
771                     ios.close
772                 end
773             end
774         end
775     }
776
777     msg 3, ''
778
779     #- add attributes to <dir..> elements needing so
780     if $mode != 'use_config'
781         msg 3, _("\tfixating configuration file...")
782         $xmldoc.elements.each('//dir') { |element|
783             path = captionpath = element.attributes['path']
784             descendant_element = element.elements['descendant::image'] || element.elements['descendant::video']
785             if !descendant_element
786                 msg 3, _("\t\tremoving %s, no element in it") % path
787                 element.remove  #- means we have a directory with nothing interesting in it
788             else
789                 captionfile = "#{descendant_element.parent.attributes['path']}/#{descendant_element.attributes['filename']}"
790                 basename = File.basename(path)
791                 if element.elements['dir']
792                     if !element.attributes['subdirs-caption']
793                         element.add_attribute('subdirs-caption', basename)
794                     end
795                     if !element.attributes['subdirs-captionfile']
796                         element.add_attribute('subdirs-captionfile', captionfile)
797                     end
798                 end
799                 if element.elements['image'] || element.elements['video']
800                     if !element.attributes['thumbnails-caption']
801                         element.add_attribute('thumbnails-caption', basename)
802                     end
803                     if !element.attributes['thumbnails-captionfile']
804                         element.add_attribute('thumbnails-captionfile', captionfile)
805                     end
806                 end
807             end
808         }
809     end
810
811     #- write down to disk config if necessary
812     if $config_writeto
813         ios = File.open($config_writeto, "w")
814         $xmldoc.write(ios, 0)
815         ios.close
816     end
817
818     if $forgui
819         msg 3, _(" completed necessary stuff for GUI, exiting.")
820         return
821     end
822
823     #- second pass to create index.html files
824     info("creating index.html")
825     msg 3, _("\trescanning directories to generate all `index.html' files...")
826
827     examined_dirs.each { |dir|
828         dir.chomp!
829         info("index.html: #{dir}")
830
831         xmldir = optxpath[utf8(dir)]
832         if !xmldir || (xmldir.attributes['already-generated'] && !$force)
833             next
834         end
835         dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote($source)}/, $dest))
836
837         if xmldir.elements['dir']
838             html = $html_index.collect { |l| l.clone }
839             iterations = {}
840             for i in html
841                 caption = xmldir.attributes['subdirs-caption']
842                 i.gsub!(/~~title~~/, caption)
843                 if xmldir.parent.name == 'dir'
844                     nav = ''
845                     path = '..'
846                     parent = xmldir.parent
847                     while parent.name == 'dir'
848                         parentcaption = parent.attributes['subdirs-caption']
849                         nav = "<a href='#{path}/index.html'>#{parentcaption}</a> #{utf8(_(" > "))} #{nav}"
850                         path += '/..'
851                         parent = parent.parent
852                     end
853                     i.gsub!(/~~ifnavigation\?~~(.+?)~~fi~~/) { $1 }
854                     i.gsub!(/~~navigation~~/, nav + caption)
855                 else
856                     i.gsub!(/~~ifnavigation\?~~(.+?)~~fi~~/, '')
857                 end
858                 discover_iterations(iterations, i)
859             end
860             
861             html_index = ''
862             reset_iterations(iterations)
863             
864             #- deal with "current" album (directs to "thumbnails" page)
865             if xmldir.attributes['thumbnails-caption']
866                 thumbnail = "#{dest_dir}/thumbnails-thumbnail.jpg"
867                 gen_thumbnails_subdir(from_utf8(xmldir.attributes['thumbnails-captionfile']), xmldir, false,
868                                       [ { 'filename' => thumbnail, 'size' => $albums_thumbnail_size } ], 'thumbnails')
869                 html_index += run_iterations(iterations)
870                 html_index.gsub!(/~~image_iteration~~/, "<a href='thumbnails.html'>" + img_element(thumbnail) + '</a>')
871                 html_index.gsub!(/~~caption_iteration~~/, xmldir.attributes['thumbnails-caption'])
872             end
873             #- cleanup temp for videos
874             system("rm -f #{dest_dir}/screenshot.jpg000000.jpg")
875
876             #- deal with sub-albums (direct to subdirs/index.html pages)
877             xmldir.elements.each('dir') { |child|
878                 subdir = make_dest_filename(from_utf8(File.basename(child.attributes['path'])))
879                 thumbnail = "#{dest_dir}/thumbnails-#{subdir}.jpg"
880                 html_index += run_iterations(iterations)
881                 captionfile, caption = find_subalbum_caption_info(child)
882                 gen_thumbnails_subdir(captionfile, child, false,
883                                       [ { 'filename' => thumbnail, 'size' => $albums_thumbnail_size } ], find_subalbum_info_type(child))
884                 html_index.gsub!(/~~caption_iteration~~/, caption)
885                 html_index.gsub!(/~~image_iteration~~/, "<a href='#{make_dest_filename(subdir)}/index.html'>" + img_element(thumbnail) + '</a>')
886                 #- cleanup temp for videos
887                 system("rm -f #{dest_dir}/screenshot.jpg000000.jpg")
888             }
889
890             html_index += close_iterations(iterations)
891
892             for i in html
893                 i.gsub!(/~~thumbnails~~/, html_index)
894             end
895             
896         else
897             html = html_reload_to_thumbnails
898         end
899
900         ios = File.open("#{dest_dir}/index.html", "w")
901         ios.write(html)
902         ios.close
903
904         #- substitute "return to albums" correctly
905         `find '#{dest_dir}' -maxdepth 1 -name "thumbnails*.html"`.each { |thumbnails|
906             thumbnails.chomp!
907             contents = File.open(thumbnails.chomp).readlines
908             for i in contents
909                 if xmldir.elements['dir']
910                     i.sub!(/~~return_to_albums~~/, '<a href="index.html">' + utf8(_('return to albums')) + '</a>')
911                 else
912                     if xmldir.parent.name == 'dir'
913                         i.sub!(/~~return_to_albums~~/, '<a href="../index.html">' + utf8(_('return to albums')) + '</a>')
914                     else
915                         i.sub!(/~~return_to_albums~~/, '')
916                     end
917                 end
918             end
919             ios = File.open(thumbnails, "w")
920             ios.write(contents)
921             ios.close
922         }
923     }
924
925     msg 3, _(" all done.")
926 end
927
928 handle_options
929 read_config
930 check_installation
931
932 build_html_skeletons
933
934 walk_source_dir