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