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