#!/usr/bin/env ruby #!/usr/bin/ruby -w # With Ruby 1.8.3, yaml gives a lot of warnings... :-(( # # xmltv2html.rb - A Ruby script to transform the XMLTV output into HTML. # # Version : 0.6.1 # Author : Kurt V. Hindenburg # # Copyright (C) 2003, 2004, 2005 Kurt V. Hindenburg # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # =begin = NAME = SYNOPSIS xmltv2html.rb [arg1 ... argN] < xmltv.xml > listing.html xmltv2html.rb -h|--help|-v|--version = DESCRIPTION ((*xmltv2html*)) is a script that transforms the output from xmltv into HTML. The HTML output has the times horizontally and the shows vertically. The shows' information is displayed via a DHTML popup window. Virtually every aspect of the HTML output can be customized using a configuration file and a CSS file. = OPTIONS : -c, --configfile=FILE Configuration file to use. : --noconfigfile Do not use any configuration file. : --urlprev=URL URL for previous link. : --urlnext=URL URL for next link. : -h or --help Display usage information. : -v or --version Display version information. = AUTHOR Written by Kurt V. Hindenburg = COPYRIGHT Copyright (C) 2003-2005 Kurt V. Hindenburg This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. = FILES * popup.js - The show's information is displayed using this JavaScript DHTML popup window script. * xmltv2html.css - An optional CSS file used in the HTML output. * xmltv2htmlrc - An optional configuration file. = SEE ALSO * ((<"xmltv2html.rb home page - http://xmltv2html.rubyforge.org/"|URL:http://xmltv2html.rubyforge.org/>)) * ((<"xmltv home page - http://sourceforge.net/projects/xmltv"|URL:http://sourceforge.net/projects/xmltv>)) * ((<"REXML home page - http://www.germane-software.com/software/rexml"|URL:http://www.germane-software.com/software/rexml>)) = BUGS * Please process the xmltv.xml file through tv_sort ((*before*)) using xmltv2html.rb =end require 'optparse' require 'yaml' require 'rexml/document' require 'singleton' require 'time' require 'set' XMLTV2HTML_VERSION="0.6.1" XMLTV2HTML_DATE="Dec 21, 2005" class Time class << self ### Time. # time string in format 'yyyymmddhhmmss (+/-xxxx)' def parse_xmltv_time(str) begin Time::parse(str.split(' ')[0]) rescue ArgumentError die "Unable to parse #{str}\n\n" end end end ### object. def round_to_interval self - (self.min % @@options['time_divisor']) * 60 end end module XMLTV2HTML @@options = nil def info(*args) $stderr.print args, "\n" end def die(*args) info args exit(1) end def test # info "The West Wing=", @@options['favorites'].include?('The West Wing') # info "EXTRA=", @@options['favorites'].include?('EXTRA') # info "Lay & Order=", @@options['favorites'].include?("Law & Order") # info "Categories=", @@options['categories'].class # info "Categories=", @@options['categories'] # info "start_time=",@@options['start_time'] # info "stop_time=",@@options['stop_time'] end class ConfReader def initialize @@options = self # set Module variable @cmd_line_options = nil clear set_defaults # info "default config_file=",@@options['config_file'] parse_command_line_options_for_config_file # info "cmd_line config_file=",@@options['config_file'] # info "cmd_line start_time=",@@options['start_time'] # info "cmd_line stop_time=",@@options['stop_time'] read_conf if @@options['use_config_file'] # info "start_time=",@@options['start_time'] # info "stop_time=",@@options['stop_time'] parse_command_line_options # info "start_time=",@@options['start_time'] # info "stop_time=",@@options['stop_time'] # info "config_file=",@@options['config_file'] # convert favorites_list to Set for faster lookup @@options['favorites'] = Set.new @@options['favorites_list' ] test end # Don't edit these, use an external xmltv2htmlrc file. def set_defaults @tree = { 'verbose' => false, 'times_interval' => 4, 'channels_interval' => 4, 'time_format_12' => true, 'use_favorites' => false, 'output_favorites' => false, 'favorites_list' => [], 'css_filename' => 'xmltv2html.css', 'categories' => {}, 'output_date_in_time' => false, 'date_format' => '%a %d', 'use_programme_popup' => true, 'programme_popup_method' => "DHTML", 'popup_title_format' => "%T", 'popup_body_format' => "%S %P
%D
Rating: %R", 'popup_title_color' => "#FFFFFF", 'popup_title_font' => "", 'popup_title_font_size' => "2", 'popup_title_background_color' => "#000099", 'popup_body_color' => "#000000", 'popup_body_font' => "", 'popup_body_font_size' => "1", 'popup_body_background_color' => "#E8E8FF", 'popup_body_width' => "200", 'output_links' => false, 'start_time' => "", 'stop_time' => "", 'start_date' => "", 'stop_date' => "", 'time_divisor' => 10, # Divide programmes' times in X minute slots 'total_hours' => 0, 'config_file' => "", 'use_config_file' => true, } end def [](key) @tree[key] end def []=(key, value) @tree[key] = value end def clear @tree = Hash.new end def read_conf return if @@options['config_file'].empty? f=File.expand_path(@@options['config_file']) if not File.exists?(f) or not File.stat(f).readable_real? info "* Unable to read #{f}\n" return end new_xmltv2htmlrc_format = false File.open(f, "r") do |aFile| first_line = aFile.gets new_xmltv2htmlrc_format = true if first_line =~ /^#config_version: 1/ end if new_xmltv2htmlrc_format == false info "\nWhoa! You are using an old xmltv2htmlrc format file (v0.5.x)." info "Please update your xmltv2htmlrc using the sample file provided" info "with xmltv2html-0.6.0+." die "\n" end info "^ Reading #{f}\n" @tree.merge!(YAML::load(File.open(f))) end # Check to see if --configfile= is present. # TODO: Should be a way to do with with OptionParser def parse_command_line_options_for_config_file ARGV.each do |opts| if opts =~ /--configfile=/ @@options['config_file'] = opts.split(/=/)[1] end end end # TODO: This has to be a better way to do this... def parse_command_line_options ARGV.options do |opts| opts.banner = "Usage: #{File.basename($0)} < xmltv.xml > tv.html\n" # separater opts.on_tail opts.on_tail("common options:") opts.on("--configfile=FILE", String, "config file to use") { |@@options['config_file']|} opts.on("--starttime=YYYYMMDDHHMM", String, "Start time") { |@@options['start_time']|} opts.on("--stoptime=YYYYMMDDHHMM", String, "Stop time") { |@@options['stop_time']|} opts.on("--urlprev=URL", String, "URL for previous link") { |@@options['url_prev']|} opts.on("--urlnext=URL", String, "URL for next link") { |@@options['url_next']|} # no argument, shows at tail opts.on_tail("-h", "--help", "show this message") {puts opts; exit} opts.on_head("specific options:") # no argument opts.on_tail("-v", "--version", "show version") do print < [show1, show2, ...] class Programmes < Hash def []=(id, programme) #info "Adding #{programme} to #{id}" case programme when Programme begin if empty? or not has_key?(id) super id, Array.new << programme else super id, (fetch(id) << programme) end end when Array begin delete id super id, programme end else $stderr.print "^^^ Programmes []= Unknown class=#{programme.class}\n" end end end class Channel attr_reader(:name, :id, :fullname) attr_reader(:programmes) attr_accessor(:totalSpan) def initialize(id, fn) @id = id @fullname = fn @number = "" @name = @fullname @totalSpan = 0 # 24 hours * 4 = 96 @programmes = Programmes.new end def <=>(o) fullname.to_i <=> o.fullname.to_i end def <<(p) # info "#{@id} : Adding #{p}\n" @programmes[@id] = p end def number_of_programmes @programmes[@id].size end def programme_at(i) @programmes[@id][i] end # Verify that each show's STOP date is the next show's START date # Should not be needed if tv_sort was used. # TODO: remove this once we can verify tv_sort was used on input data def verifyStopDate # @programmeList.each_index { |si| # s = @programmeList[si] # next_show = @programmeList[si.succ] # next if next_show == nil # next if s.times.fullStopTime == next_show.times.fullStartTime # # die "\n * A programme's stop time does not match the next \n" + # " * programme's start time. \n" + # " * Use tv_sort from the xmltv distribution to correct!\n" + # " * Exiting...\n\n" if !stop # } end def calc_programme_slots @programmes[@id].each { |p| p.calculateSlots } end def calculateSlots(slist) tinterval = @@options['channels_interval'] * 60 / @@options['time_divisor'] left = [] right = slist.dup index = 0 total = 0 slist.each { |e| cmd = e.slice(0..0).to_s span = e.slice(1..-1).to_i if total + span > tinterval # $stderr.print "removing #{e} ::: " prev = tinterval - total # $stderr.print "adding prev #{prev}; " nxt = total + span - tinterval # $stderr.print "adding next #{nxt}\n" # exit if nxt + prev != span if cmd == "D" # Dummy data left.push "D"+prev.to_s left.push "D"+nxt.to_s else if cmd =="Q" # The removed was a Q left.push "Q"+prev.to_s else left.push "P"+prev.to_s end left.push "Q"+nxt.to_s end right.shift break elsif total + span == tinterval total = 0 left.push e right.shift else left.push e right.shift total += span end index += 1 } left = left.concat(right) # left.each { |i| $stderr.print "#{i}*" }; $stderr.print "\n" left end # Create a slot list to account for displaying channel info # channel_interval = # of hours to display channel info # Handle empty programmes at start/end of channel. def createSlotList l = Array.new span_counter = 0 times_counter = 1 sindex = 0 total = 0 if @@options['channels_interval'] > 0 tinterval = @@options['channels_interval'] * 60 / @@options['time_divisor'] else tinterval = 9999 end ci = tinterval slist = Array.new s = @programmes[@id].first # 1st programme for this channel if s.startSlot() > 0 # Missing programme at start dummy_span = s.startSlot() - span_counter slist.push "D"+dummy_span.to_s end @programmes[@id].each { |s| slist.push "P"+s.spanSlots.to_s } if @@options['channels_interval'] > 0 sl = calculateSlots(slist) while sl != slist slist = sl.dup sl = calculateSlots(sl) end end slist.each { |entry| span = entry.slice(1..-1).to_i l.push entry total += span l.push("C0") if (total % tinterval) == 0 and @@options['channels_interval'] > 0 } slist = l.unshift("C0") # Add left-most channel # Add left-most channel if not already there # slist = l # slist = l.unshift("C0") if l[0] != "C0" # slist = l.unshift("C0") if l[0, 1] != "C0" # $stderr.print slist[0, 3],"\n" # $stderr.print l[0, 3],"\n" dinterval = @@options['total_hours'] * 60 / @@options['time_divisor'] if total < dinterval # Not enough programmes' data at end slist.push("D"+(dinterval - total).to_s) slist.push("C0") if dinterval % tinterval == 0 end slist end end # [channel id] -> Channel class Channels < Hash include Singleton attr_accessor(:output_total_hours) def initialize @output_total_hours = 0 # Set to hours displayed @output_start_hour = 0 # Start of hours displayed @output_stop_hour = 0 # Stop hours displayed end def calc_programmes_slots each { |id, c| c.calc_programme_slots } end end class XmlTV attr_reader :srcInfoName attr_reader :genInfoName attr_reader :HTML_title # Title of HTML page attr_reader :top_title # Title of HTML page attr_accessor(:firstShowStartDate, :lastShowStartDate, :lastShowStopDate) def initialize @firstShowStartDate = "99999999999999" @lastShowStartDate = "0" @lastShowStopDate = "0" file = $stdin @doc = REXML::Document.new file @doc.elements.each("tv") { |e| # Should only be one # The date here is the date/time that the user obtained the tv # listings, NOT the date/time of the actual shows. # @date = e.attributes["date"] @srcInfoName = e.attributes["source-info-name"] @genInfoName = e.attributes["generator-info-name"] @HTML_title = "" } end def setTitle(dates) @top_title = "" @HTML_title = "" @top_title += @srcInfoName + " :: " if @srcInfoName t1 = Time.parse(@firstShowStartDate) t2 = Time.parse(@lastShowStopDate) @time_first = t1.strftime("%a %b %d %I:%M %p") @time_last = t2.strftime("%a %b %d %I:%M %p %Z") if ( (t1.hour == 0) or (t1.hour > 19) ) and (t2.hour < 05) t3 = t1 + (5 * 3600) @top_title += t3.strftime("%A %b %d") @HTML_title = @top_title @top_title += "
" @top_title += "" @top_title += @time_first + " - " + @time_last + "" else @top_title += @time_first + " - " + @time_last @HTML_title = @top_title end end def parseChannels channels = Channels.instance @doc.elements.each("tv/channel") { |element| id = element.attributes["id"] fn = "" element.each_element { |e| if e.name == "display-name" # Use 1st entry fn = e.text() break end } channels[id] = Channel.new(id, fn) } end def parseProgrammes channels = Channels.instance @doc.elements.each("tv/programme") { |element| title="" subtitle="" desc="" rating="" cats = nil rerun = false ndesc = "" start = element.attributes["start"] stop = element.attributes["stop"] die "\n * No stop attribute in this programme...\n" + " * Use tv_sort from the xmltv distribution to correct!\n" + " * Exiting...\n\n" if !stop dstart = start[0..13] dstop = stop[0..13] ext = start[14..-1] # info "start_time=",@@options['start_time'] # info "stop_time=",@@options['stop_time'] # info "empty?" ,@@options['start_time'].empty? if not @@options['start_time'].empty? nstart = @@options['start_time'].clone nstart[10..11] = @@options['time_divisor'].to_s # If programme ends before the desired start time... # Adjust for the time_divisor (round_to_interval) if dstop < nstart # info "Delete programme - Desired start : #{@@options['start_time']}, " # $stderr.print "Delete programme - Desired start : #{$params.start_time}, " # $stderr.print "programme stop : #{dstop}\n" # $stderr.print "Old start time = #{$params.start_time}, new=#{nstart}\n" next end end # If programme starts after the desired stop time... if (not @@options['stop_time'].empty?) and (dstart >= @@options['stop_time']) # $stderr.print "Delete programme - Desired stop : #{$params.stop_time}, " # $stderr.print "programme start : #{dstart}\n" # info "programme start : #{dstart}\n" next end # If programme starts before the desired start time, change start if (not @@options['start_time'].empty?) and (dstart < @@options['start_time']) # info "Change Start - new start : " # $stderr.print "Change Start - new start : #{$params.start_time}, " # $stderr.print "old start: #{dstart}\n" # ndesc = "(" + start[8..9] + ":" + start[10..11] + ") " start = @@options['start_time'] + ext dstart = start[0..13] end # If programme ends after the desired stop time, change stop if (not @@options['stop_time'].empty?) and (dstop > @@options['stop_time']) stop = @@options['stop_time'] + ext # info "Change Stop - new stopt : " dstop = stop[0..13] end @firstShowStartDate = dstart if @firstShowStartDate > dstart @lastShowStartDate = dstart if @lastShowStartDate < dstart @lastShowStopDate = dstop if @lastShowStopDate < dstop channel = element.attributes["channel"] element.each_element {|e| title = e.text() if e.name == "title" subtitle = e.text() if e.name == "sub-title" desc=e.text() if e.name == "desc" rerun=true if e.name == "previously-shown" if e.name == "rating" rv = e.elements[1] rating = rv.text() if rv.name == "value" end # Check to see if user want to use special CSS class for category # FIXME: What happens when more than 1 category is triggered? if e.name == "category" if @@options['categories'].has_key?(e.text()) cats = @@options['categories'][e.text()]; end end } title.gsub!(/[\"\'\`]/,'') # Remove "'` title = title.unpack("U*").pack("C*") subtitle = "" if not subtitle subtitle.gsub!(/[\"\'\`]/,'') # Remove "'` desc = "" if not desc desc.gsub!(/[\"\'\`]/,'') # Remove "'` desc = desc.unpack("U*").pack("C*") desc = ndesc + desc if ndesc rating = "" if not rating rating.gsub!(/[\"\'\`]/,'') # Remove "'` #$stderr.print "title=#{title}, desc=#{desc}, rating=#{rating}\n" p = Programme.new( title, subtitle, channel, start, stop, desc, rating, cats, rerun) # plist[channel] = p channels[channel] << p } end end class XMLTV2HTML2 attr_accessor(:dates) def initialize @xml = XmlTV.new @out = Html.new @dates = Array.new end def parseXML @xml.parseChannels @xml.parseProgrammes @@options['start_date'] = @xml.firstShowStartDate @@options['stop_date'] = @xml.lastShowStopDate # Force start/stop time on hour @@options['start_date'][10,4] = "0000" # info "start_date=",@@options['start_date'] # info "stop_date= ",@@options['stop_date'] if @@options['stop_date'][10,2] != "00" hour = (@@options['stop_date'][8,2]).to_i + 1 if hour > 23 $stderr.print "yuck #{hour}\n" end @@options['stop_date'][8,2] = hour.to_s.rjust(2).sub(/\s/,'0') end @@options['stop_date'][10,4] = "0000" pstart = Time.parse(@@options['start_date']) pstop = Time.parse(@@options['stop_date']) @@options['total_hours'] = ((pstop - pstart) / 3600).to_i channels = Channels.instance channels.calc_programmes_slots fdate = @@options['start_date'][0,8] ldate = @@options['stop_date'][0,8] @xml.setTitle(@dates) end def generatePopups i = 0 @out.outputPopupHTMLBegin channels = Channels.instance channels.each { |id, c| i = @out.outputPopupDescs(c, i) } @out.outputPopupHTMLEnd end def generateHTML sindex =-1 times_counter = 1 favorites_list = [] @out.dates = @dates @out.doctype @out.header(@xml.HTML_title) generatePopups if @@options['use_programme_popup'] @out.text_before_table(@xml.top_title) @out.table_start @out.table_times channels = Channels.instance sorted_channels = channels.sort { |a, b| a[1]<=>b[1] } sorted_channels.each { |id, c| @out.outputChannelBegin slot_list = c.createSlotList next unless slot_list i = 0 pi = -1 slot_list.each { |entry| cmd = entry.slice(0..0).to_s span = entry.slice(1..-1).to_i case cmd when "D" @out.outputDummyProgramme(span) when "P" # Programme sindex += 1 pi += 1 prog = c.programme_at(pi) @out.outputProgramme(prog, sindex, span) # @out.outputProgramme(c.programme_at(pi), sindex, span) if @@options['use_favorites'] and @@options['output_favorites'] and @@options['favorites'].include?(prog.title) #$stderr.print "found a fav #{prog.title}\n" favorites_list << prog end when "Q" # Use previous Programme's info @out.outputProgramme(c.programme_at(pi), sindex, span) when "C" @out.outputChannel(c) else $stderr.print "Unknown slot entry #{entry}\n" ; exit end i += 1 } @out.outputChannelEnd if @@options['times_interval'] > 0 and ((times_counter % @@options['times_interval']) == 0) @out.table_times end times_counter += 1 } @out.table_end @out.output_favorites_list(favorites_list) if not favorites_list.empty? @out.text_after_table @out.footer end end class Html attr_accessor(:hours, :dates) def initialize @hours = 0 # hours displayed end def nl print "\n" end def nb print " " end def dots print "../" end def doctype print '' nl end def header(title) print ''; nl print ''; nl print ''; nl print '' print title if title print ''; nl print ''; nl if @@options['use_programme_popup'] if @@options['programme_popup_method'] == "DHTML" print ''; nl else print ''; nl end end print ''; nl; print ''; nl end def outputPopupHTMLBegin print ''; nl print ''; nl nl end def outputPopupDescs(c, cindex) # The descriptions go here...Text[#]=["title","text"] c.programmes[c.id()].each { |s| title = @@options['popup_title_format'].sub(/\%T/, s.title) title.sub!(/\%R/, s.rating) desc = @@options['popup_body_format'].gsub(/%T/, s.title) desc.sub!(/\%S/, s.subtitle) if (s.previouslyShown) # rerun desc.sub!(/\%P/, "(R)") else desc.sub!(/\%P/, "") end desc.sub!(/%D/, s.desc) desc.sub!(/%R/, s.rating) if s.desc != "" or s.rating != "" print 'Text[',cindex,']=["',title,'","',desc,'"]'; nl s.popupIndex = cindex cindex += 1 end } nl cindex end def footer print '' nl end def text_before_table(text) if text print '

' print text print '

'; nl end if @@options['url_prev'] print '<<< Previous | ' end if @@options['url_next'] print 'Next >>>' end end def text_after_table if @@options['url_prev'] print '<<< Previous | ' end if @@options['url_next'] print 'Next >>>' end print '
' outputInfo outputLinks if @@options['output_links'] end def table_start print ''; nl end def table_end print '
'; nl end # Space for channel names def output_channel_space print ''; nb; print ''; nl end def outputDate days = @hours/24 colspan = 96 colspan += 96 / @@options['channels_interval'] if @@options['channels_interval'] > 0 print ''; nl (0 .. days-1).each { |d| print '','date here',''; nl } print ''; nl end def table_times intervals = 60 / @@options['time_divisor'] # Need hour to start.... we'll force it to be on an hour later starting_day = @@options['start_date'][6,2].to_i starting_hour = @@options['start_date'][8,2].to_i # $stderr.print "Day to start: #{starting_day}\n" # $stderr.print "Hour to start: #{starting_hour}\n" # $stderr.print "Total hours: #{$params.total_hours}\n" print ''; nl output_channel_space cs = 0 (starting_hour .. starting_hour + @@options['total_hours'] - 1).each { |h| (0 .. intervals-1).each { |hh| print ''; printf "%02d", hh * @@options['time_divisor']; print ''; } cs += 1 output_channel_space if @@options['channels_interval'] > 0 and cs % @@options['channels_interval'] == 0 } print ''; nl print ''; nl output_channel_space days = @hours/24 cs = 0 cdate = Time.parse(@@options['start_date']) (starting_hour .. starting_hour + @@options['total_hours'] - 1).each { |hi| h = hi % 24 print ''; if @@options['output_date_in_time'] nl; print ''; nl print ''; nl; print '
'; end if @@options['time_format_12'] if h < 12 out = "#{h} am" else out = (h - 12).to_s + " pm" end out.gsub!(/^0/, '12') else out = sprintf "%02d", h end print out if @@options['output_date_in_time'] print ''; print cdate.strftime(@@options['date_format']) print '
'; nl else print ''; end cs += 1 cdate += 3600 # Add 1 hour output_channel_space if @@options['channels_interval'] > 0 and cs % @@options['channels_interval'] == 0 } print ''; nl end def outputChannelBegin print ''; nl end def outputChannelEnd print ''; nl end def outputChannel(c) print ''; print c.fullname; print ''; nl end def outputDummyProgramme(span) print 'No Data' end def outputProgramme(s, index, slots) return if s.spanSlots == 0 # Invalid programme - too short if slots > 0 print '' elsif s.category print s.category,'">' else print 'programme">' end # Styles : 12=right; 1=center; 2=left; 3=float if @@options['use_programme_popup'] if s.popupIndex >= 0 if @@options['programme_popup_method'] == "DHTML" print ''; nl else print ''; nl end print s.title ; print ''; nl else print s.title ; nl end else print s.title ; nl end print ''; nl end def output_favorites_list(fav_list) fav_list = fav_list.sort! { |a, b| a.title<=>b.title } print '

'; nl print ''; nl print ''; nl print ''; print ''; print ''; print ''; nl fav_list.each { |p| print ''; nl outputProgramme(p, p.popupIndex, 1) print ''; nl print ''; nl print ''; nl } print '
Favorites
TitleStartStop
'; nl print p.times.fullStartTime.strftime("%a %b %d %I:%M %p") print ''; nl print p.times.fullStopTime.strftime("%a %b %d %I:%M %p") print '
'; nl end def outputInfo print '
' print '' nl print "Generated by xmltv2html.rb v#{XMLTV2HTML_VERSION} (#{XMLTV2HTML_DATE})" print " on #{Time.now}" nl print '' print '
' end def outputLinks nl # print 'Links: ' print 'xmltv' nl print 'xmltv2html' nl end end end include XMLTV2HTML trap("INT") do info "\n Received user interrupt...exiting.\n" exit end confreader = ConfReader.new xmltv2html = XMLTV2HTML2.new time1 = Time.new xmltv2html.parseXML time2 = Time.new $stderr.print "^ Parsing XML took #{time2-time1} seconds.\n" time3 = Time.new xmltv2html.generateHTML time4 = Time.new $stderr.print "^ Generating HTML took #{time4-time3} seconds.\n" exit # vim:set ts=3 sw=3 expandtab: