1cf0533edc092e0b9f38416e604aaf5cea21fef9
[booh] / booh
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) 2004 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 'html_merges'
24
25 #- install location
26 $FPATH = '.'
27
28 #- options
29 $options = [
30     [ '--help',          '-h', GetoptLong::NO_ARGUMENT,       "Get help message" ],
31
32     [ '--no-check',      '-n', GetoptLong::NO_ARGUMENT,       "Don't check for needed external programs at startup" ],
33
34     [ '--source',        '-s', GetoptLong::REQUIRED_ARGUMENT, "Directory which contains original images/videos as files or subdirs" ],
35     [ '--destination',   '-d', GetoptLong::REQUIRED_ARGUMENT, "Directory which will contain the web-album" ],
36     [ '--clean',         '-c', GetoptLong::NO_ARGUMENT,       "Clean destination directory" ],
37
38     [ '--theme',         '-t', GetoptLong::REQUIRED_ARGUMENT, "Select HTML theme to use" ],
39     [ '--size',          '-z', GetoptLong::REQUIRED_ARGUMENT, "Size of images for `fullscreen' slideshow" ],
40     [ '--captions',      '-C', GetoptLong::REQUIRED_ARGUMENT, "File containing captions of images" ],
41     [ '--captions-skel', '-k', GetoptLong::REQUIRED_ARGUMENT, "Filename where the script will output a captions skeleton" ],
42
43     [ '--mproc',         '-m', GetoptLong::REQUIRED_ARGUMENT, "Specify the number of processors for multi-processors machines" ],
44
45     [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, "Set max verbosity level (0: warnings, 1: important messages, " +
46                                                               "2: other messages)" ],
47 ]
48
49 #- default values for some globals 
50 $convert = 'convert -interlace line +profile "*"'
51 $size = 'medium'
52 $verbose_level = 2
53 $captions = {}
54
55 def usage
56     puts "Usage: #{File.basename($0)} [OPTION]..."
57     $options.each { |ary|
58         printf " %3s, %-15s %s\n", ary[1], ary[0], ary[3]
59     }
60 end
61
62 def msg(verbose_level, msg)
63     if verbose_level <= $verbose_level
64         if verbose_level == 0
65             warn "***ERROR***: #{msg}"
66         elsif verbose_level == 1
67             warn "Warning: #{msg}"
68         else
69             puts msg
70         end
71     end
72 end
73
74 def die(msg)
75     puts msg
76     exit 1
77 end
78
79 def handle_options
80     parser = GetoptLong.new
81     parser.set_options(*$options.collect { |ary| ary[0..2] })
82     begin
83         parser.each_option do |name, arg|
84             case name
85             when '--help'
86                 usage
87                 exit(0)
88
89             when '--no-check'
90                 $no_check = true
91
92             when '--source'
93                 $source = arg.sub(%r|/$|, '')
94                 if !File.directory?($source)
95                     die "Argument to --source must be a directory"
96                 end
97             when '--destination'
98                 $dest = arg.sub(%r|/$|, '')
99                 if File.exists?($dest) && !File.directory?($dest)
100                     die "If --destination exists, it must be a directory"
101                 end
102             when '--clean'
103                 system("rm -rf #{$dest}")
104
105             when '--theme'
106                 select_theme(arg)
107             when '--size'
108                 $size = arg
109             when '--captions'
110                 if File.readable?(arg)
111                     use_captions(arg)
112                 else
113                     die 'Captions file does not exist or is unreadable.'
114                 end
115             when '--captions-skel'
116                 if File.exists?(arg)
117                     die 'Captions skeleton file already exists, won\'t overwrite'
118                 else
119                     $captions_skel = arg
120                     $images_list_for_captions = Array.new
121                 end
122
123             when '--mproc'
124                 $mproc = arg.to_i
125                 $pids = Array.new
126
127             when '--verbose-level'
128                 $verbose_level = arg.to_i
129
130             end
131         end
132     rescue
133         usage
134         exit(1)
135     end
136
137     if !$source
138         die 'Missing --source parameter.'
139     end
140     if !$dest
141         die 'Missing --destination parameter.'
142     end
143
144     if !$theme
145         select_theme('simple')
146     end
147
148     size = $images_size.detect { |e| e['name'] == $size }
149     if !size
150         die "Size #{$size} not found in parameters of theme."
151     else
152         $sizeval = size['size']
153         msg 3, "Using size for fullscreen images of value: #{$sizeval}"
154     end
155 end
156
157 def select_theme(name)
158     $theme = name
159     themedir = "#{$FPATH}/themes/#{$theme}"
160     if !File.directory?(themedir)
161         die "Theme was not found (tried #{themedir} directory)."
162     end
163     require "#{themedir}/parameters.rb"
164 end
165
166 def use_captions(filename)
167     for line in IO.readlines(filename)
168         if line =~ /(\S+): (.+)/
169             $captions[$1] = $2
170         else
171             die "Unrecognized line in #{filename}: #{line}"
172         end
173     end
174 end
175
176 def check_installation
177     if $no_check
178         return
179     end
180     %w(convert transcode exif).each { |prg|
181         if !system("which #{prg} >/dev/null")
182             die "The `#{prg}' program is typically needed. Re-run with --no-check if you're sure you're fine without it."
183         end
184     }
185 end
186
187 def replace_line(surround, keyword, line)
188     begin
189         contents = eval "$#{keyword}"
190         line.sub!(/#{surround}#{keyword}#{surround}/, contents)
191     rescue NameError
192         die "No `#{keyword}' found for substitution"
193     end
194 end
195
196 def build_html_skeleton
197     theme_file = File.open("#{$FPATH}/themes/#{$theme}/skeleton.html").readlines
198     msg 2, "Read theme `#{$theme}'"
199     for line in theme_file
200         while line =~ /~~~(\w+)~~~/
201             replace_line('~~~', $1, line)
202         end
203     end
204     return theme_file
205 end
206
207 #- parallelizable sys
208 def psys(cmd)
209     if $mproc
210         if pid = fork
211             $pids << pid
212         else
213             msg 1, cmd + ' &'
214             system(cmd)
215             exit 0
216         end
217         if $pids.length == $mproc
218             finished = Process.wait2
219             $pids.delete(finished[0])
220             $pids = $pids.find_all { |pid| Process.waitpid(pid, Process::WNOHANG) == nil }
221         end
222     else
223         msg 1, cmd
224         system(cmd)
225     end
226 end
227
228 def find_captions(images)
229     if $captions_skel
230         $images_list_for_captions += images
231     end
232     return images.collect { |img| $captions[img] }
233 end
234
235 def walk_source_dir
236     `find #{$source} -type d`.each { |dir|
237         dir.chomp!
238         msg 2, "Examining #{dir}..."
239         images = Dir["#{dir}/*.{jpg,JPG,jpeg,JPEG,gif,GIF,bmp,BMP,png,PNG}"]
240         msg 2, "\t#{images.length} images"
241         videos = Dir["#{dir}/*.{mov,MOV,avi,AVI,mpg,MPG,mpeg,MPEG,wmv,WMV,asx,ASX}"]
242         msg 2, "\t#{videos.length} videos"
243         
244         dest_dir = dir.sub(/^#{Regexp.quote($source)}/, $dest)
245         system("mkdir -p '#{dest_dir}'")
246         msg 2, "Outputting in #{dest_dir}..."
247
248         final_images = []
249         images.each { |img|
250             dest_img = img.sub(/^#{Regexp.quote($source)}/, $dest).
251                            sub(/\.[^\.]+$/, '') + "-#{$sizeval}.jpg"
252             final_images << File.basename(dest_img)
253             if !File.exists?(dest_img)
254                 convert_options = ''
255                 orientation = `exif #{img}`.detect { |line| line =~ /^Orientation/ }
256                 if orientation =~ /right - top/
257                     convert_options += '-rotate 90 '
258                 end
259                 if orientation =~ /left - bottom/
260                     convert_options += '-rotate -90 '
261                 end
262                 psys("#{$convert} #{convert_options}-geometry #{$sizeval} '#{img}' '#{dest_img}'")
263             end
264         }
265
266         html = $skeleton.collect { |l| l.clone }
267         images4js = final_images.collect { |e| "\"#{e}\"" }.join(', ')
268         captions4js = find_captions(images).collect { |e| "\"#{e}\"" }.join(', ')
269         for i in html
270             i.sub!(/~~images~~/, images4js)
271             i.sub!(/~~captions~~/, captions4js)
272             i.sub!(/~~title~~/, File.basename(dir))
273         end
274
275         ios = File.open("#{dest_dir}/index.html", "w")
276         ios.write(html)
277         ios.close
278     }
279
280     if $captions_skel
281         ios = File.open($captions_skel, "w")
282         ios.write($images_list_for_captions.collect { |img| "#{img}: \n" })
283         ios.close
284     end
285 end
286
287 handle_options
288 check_installation
289
290 $skeleton = build_html_skeleton
291
292 walk_source_dir