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