xmltv 0.8.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,737 @@
1
+ #!/usr/bin/ruby -w
2
+
3
+ require 'rubygems'
4
+ require 'hpricot'
5
+ require 'rexml/document'
6
+ require 'yaml'
7
+ require 'date'
8
+ require 'fileutils'
9
+ require 'timeout'
10
+ require 'open-uri'
11
+ require 'pp'
12
+ require 'iconv'
13
+ require 'set'
14
+ require 'optparse'
15
+ require 'optparse/time'
16
+ require 'ostruct'
17
+
18
+ class Time
19
+ public :to_date
20
+ end
21
+ module MailSender
22
+ def linuxmail(to, subj, body)
23
+ from = `/usr/bin/id --name --user`.chomp
24
+ header = ["To: #{to}", "From: #{from}", "Subject: #{subj}"].join("\n")
25
+ mess = [header,"", body].join("\n")
26
+ puts mess
27
+ IO.popen("/usr/sbin/sendmail -t", 'w') do |h|
28
+ h.puts mess
29
+ end
30
+ end
31
+ def sendmail(*args)
32
+ if test(?x, '/usr/sbin/sendmail')
33
+ linuxmail(*args)
34
+ else
35
+ raise "FIXME: Don't know how to send mail"
36
+ end
37
+ end
38
+ module_function :sendmail, :linuxmail
39
+ end
40
+ class String
41
+ UTF = 'utf-8'
42
+ ISO = 'iso-8859-15'
43
+ TO_UTF = Iconv.new(UTF, ISO)
44
+ CK_UTF = Iconv.new(UTF, UTF)
45
+ def to_utf
46
+ TO_UTF.iconv(self)
47
+ end
48
+ def ck_utf
49
+ CK_UTF.iconv(self)
50
+ end
51
+ end
52
+
53
+ class Date
54
+ Maanden = [nil] + %w{ januari februari maart april mei juni juli augustus september oktober november december }
55
+ Dagen = %w{ zondag maandag dinsdag woensdag donderdag vrijdag zaterdag }
56
+ def self.dutch(instring)
57
+ nu = today
58
+ defjaar, defmaand, defdag, weekdag = nu.year, nu.month, nu.day, nu.wday
59
+ jaar = maand = dag = nil
60
+ string = instring.gsub(/[[:cntrl:][:punct:]]/, '').downcase
61
+ string.split.each do |elem|
62
+ case elem
63
+ when /[[:alpha:]]+/
64
+ if Maanden.include?(elem)
65
+ maand = Maanden.index(elem)
66
+ if maand < nu.month
67
+ defjaar += 1
68
+ end
69
+ elsif Dagen.include?(elem)
70
+ wd = Dagen.index(elem) - weekdag
71
+ wd += 7 if wd < 0
72
+ nu += wd
73
+ defjaar, defmaand, defdag, weekdag = nu.year, nu.month, nu.day, nu.wday
74
+ end
75
+ when /[[:digit:]]+/
76
+ ent = elem.to_i
77
+ if ent <= 31
78
+ dag = ent
79
+ elsif ent > 1900
80
+ jaar = ent
81
+ end
82
+ end
83
+ end
84
+ new(jaar || defjaar, maand || defmaand,dag || defdag)
85
+ end
86
+ end
87
+
88
+
89
+ module XMLTV
90
+ class ValidateError < Exception; end
91
+ class BadChannelError < Exception; end
92
+ class BadSiteError < Exception; end
93
+ class Progdata < Hash
94
+ def self.new
95
+ super {|h,v| h[v] = Hash.new }
96
+ end
97
+ end
98
+ VERSION = '0.8.1'
99
+ Progdir = ($LOAD_PATH.find {|x| test(?f, "#{x}/xmltv/xmltv.rb")} || 'lib') + '/xmltv/sites'
100
+ Sites = Dir["#{Progdir}/*.rb"].map{|x| x[0..-4]}.map{|x| File.basename(x)}
101
+ class XmltvOptparser
102
+
103
+ #
104
+ # Return a structure describing the options.
105
+ #
106
+ def self.parse(args)
107
+ options = OpenStruct.new
108
+ options.basedir = "#{ENV['HOME']}/.xmltv"
109
+ options.validate = true
110
+
111
+ opts = OptionParser.new do |opts|
112
+ opts.banner = "Usage: #{$PROGRAM_NAME} [options]"
113
+
114
+ opts.separator ""
115
+ opts.separator "Specific options:"
116
+
117
+ opts.on("-a", "--list-all",
118
+ "List all available channels") do |av|
119
+ options.available = av
120
+ options.action = true
121
+ end
122
+
123
+ opts.on("--add x,y,z", Array, "Add channels to config") do |list|
124
+ options.add = list
125
+ options.action = true
126
+ end
127
+ opts.on("--delete x,y,z", Array, "Delete channels from config") do |list|
128
+ options.del = list
129
+ options.action = true
130
+ end
131
+ opts.on("-l", "--list", "List configured channels") do |list|
132
+ options.list = list
133
+ options.action = true
134
+ end
135
+ opts.on("-c", "--config-file FILENAME", "Configuration file") do |list|
136
+ options.config = list
137
+ end
138
+ opts.on( "--basedir DIRNAME", "Basedir (#{options.basedir})") do |list|
139
+ options.basedir = list
140
+ end
141
+ opts.on( "--no-validate", "Do not validate") do |v|
142
+ options.validate = false
143
+ end
144
+ opts.on( "--mailto USER", "Mail exception") do |user|
145
+ options.mailto = user
146
+ end
147
+
148
+ opts.on("-v", "--[no-]verbose", "Run verbosely") do |v|
149
+ options.verbose = v
150
+ end
151
+
152
+ opts.separator ""
153
+ opts.separator "Common options:"
154
+
155
+ opts.on_tail("-h", "--help", "Show this message") do
156
+ STDERR.puts opts
157
+ exit
158
+ end
159
+
160
+ opts.on_tail("--version", "Show version") do
161
+ options.version = true
162
+ end
163
+ end
164
+
165
+ opts.parse!(args)
166
+ options
167
+ end
168
+ end
169
+ XmltvOptions = XmltvOptparser.parse(ARGV)
170
+
171
+
172
+ class XmlWriter
173
+ With_lang = [ 'title','sub-title','desc','category','language','orig-language','country','premiere','last-chance']
174
+ Programme_dtd = [
175
+ ['title' ],
176
+ [ 'sub-title' ],
177
+ [ 'desc' ],
178
+ [ 'credits', nil, [
179
+ 'director',
180
+ 'actor',
181
+ 'writer',
182
+ 'adapter',
183
+ 'producer',
184
+ 'presenter',
185
+ 'commentator',
186
+ 'guest'
187
+ ]
188
+ ],
189
+ ['date'],
190
+ ['category' ] ,
191
+ ['language' ] ,
192
+ ['orig-language' ] ,
193
+ [ 'length', [ 'units' ] ],
194
+ [ 'icon', [ 'src', 'width', 'height' ] ],
195
+ [ 'url'],
196
+ [ 'country' ] ,
197
+ [ 'episode-num', [ 'system' ] ],
198
+ [ 'video', [], [
199
+ 'present',
200
+ 'colour',
201
+ 'aspect',
202
+ 'quality'
203
+ ]
204
+ ],
205
+ [ 'audio' , [], [
206
+ 'present',
207
+ 'stereo'
208
+ ]
209
+ ],
210
+ ['previously-shown', ['start', 'channel' ]],
211
+ ['premiere' ],
212
+ [ 'last-chance' ],
213
+ [ 'new' ],
214
+ [ 'subtitles', [ 'type' ], [ 'language' ] ],
215
+ [ 'rating', [ 'system' ] , [ 'value', 'icon' ] ],
216
+ [ 'star-rating', [], [ 'value', 'icon' ] ]
217
+ ]
218
+ def initialize(grabber)
219
+ @grabber = grabber
220
+ @doc = doc = REXML::Document.new
221
+ doc << REXML::XMLDecl.new("1.0", "UTF-8")
222
+ dtd = grabber.config['dtd'] || 'file:///usr/share/xmltv/xmltv.dtd'
223
+ doc << REXML::DocType.new('tv', %Q{SYSTEM "#{dtd}"})
224
+ @el_tv = doc.add_element( 'tv' )
225
+ @el_tv.attributes['generator-info-name'] = 'xmltv.rb'
226
+ end
227
+ def add_dtd(progdata)
228
+ raise ArgumentError.new("add_dtd must have a Progdata") unless progdata.is_a?(Progdata)
229
+ p = @el_tv.add_element('programme')
230
+ %w{ start stop }.each do |word|
231
+ p.attributes[word] = progdata[word].strftime( '%Y%m%d%H%M%S %Z')
232
+ end
233
+ p.attributes['channel'] = progdata['channel']
234
+ Programme_dtd.each do |el|
235
+ name , attrs , elements = el
236
+ # STDERR.puts name
237
+ next if (cur = progdata[name]).empty?
238
+ el = p.add_element(name)
239
+ el.text = cur if cur.is_a? String
240
+ if With_lang.include? name
241
+ el.attributes['lang'] = @grabber.lang
242
+ end
243
+ if attrs
244
+ attrs.each do |a|
245
+ el.attributes[a] = cur[a] if cur[a]
246
+ end
247
+ end
248
+ if elements
249
+ elements.each do |e|
250
+ case cur[e]
251
+ when String
252
+ el.add_element(e).text = cur[e]
253
+ when Array
254
+ cur[e].each do |r|
255
+ el.add_element(e).text = r
256
+ end
257
+ end
258
+ end
259
+ end
260
+ end
261
+ end
262
+ def write_file(pda, chan_id)
263
+ pda.each do |progdata|
264
+ begin
265
+ add_dtd(progdata)
266
+ rescue StandardError => exc
267
+ STDERR.puts exc, exc.message, exc.backtrace, '===='
268
+ PP.pp(progdata, STDERR)
269
+ raise
270
+ end
271
+ end
272
+ file = ">#{@grabber.outputfile(chan_id)}"
273
+ errorfile = '/var/tmp/xmllint-errors'
274
+ if XmltvOptions.validate
275
+ IO.popen(" ( xmllint --valid - | tv_sort #{file} ) 2>#{errorfile}", 'w') do |h|
276
+ write_xml h
277
+ end
278
+ if test(?s, errorfile)
279
+ raise ValidateError.new("zie #{errorfile}")
280
+ end
281
+ else
282
+ File.open("#{@grabber.outputfile(chan_id)}", 'w') do |h|
283
+ write_xml h
284
+ end
285
+ end
286
+ end
287
+ def write_xml(h, indent = -1)
288
+ @doc.write(h, indent)
289
+ h.puts
290
+ end
291
+
292
+ end
293
+ class Grabber
294
+ Dag = 24 * 60 * 60
295
+ Vandaag = Date.today
296
+ attr_accessor :myname, :chnbasedir, :spooldir, :channel_list, :lang, :generator, :base_url, :config_file_name
297
+ attr_accessor :config, :all_channels, :reject_file_name
298
+ mtv =
299
+ begin
300
+ YAML.load_file("#{XmltvOptions.basedir}/mythtv_chns.yaml")
301
+ rescue Errno::ENOENT
302
+ Hash.new
303
+ end
304
+ MythTV = Hash.new
305
+ mtv.each do |myth, info|
306
+ info[0].each do |file|
307
+ MythTV[file] = myth
308
+ end
309
+ end
310
+
311
+ def grab_channel(chan_id)
312
+ STDERR.puts "Grabber must implement grab_channel(chan_id)"
313
+ exit
314
+ end
315
+ def transform(chan_id)
316
+ STDERR.puts "Grabber must implement transform(chan_id)"
317
+ exit
318
+ end
319
+
320
+ def initialize
321
+ @hits = 0
322
+ # @myname = File.basename($PROGRAM_NAME).gsub(/\..*/, '')
323
+ @myname = self.class.to_s[7..-8].downcase
324
+ @chnbasedir = "#{XmltvOptions.basedir}/#{myname}"
325
+ @config_file_name = XmltvOptions.config || "#{chnbasedir}/config"
326
+ @reject_file_name = "#{chnbasedir}/rejects"
327
+ @config = load_config_file
328
+ @lang = 'nl'
329
+ @generator = "#{myname}.#{lang}"
330
+ @base_url = "http://www.#{generator}"
331
+ @spooldir = config['spooldir'] || "#{chnbasedir}/spool"
332
+ [ chnbasedir, spooldir ].each do |dir|
333
+ FileUtils.mkdir_p(dir) unless test(?d, dir)
334
+ end
335
+ @channel_list = "#{chnbasedir}/channel_list"
336
+ @all_channels = load_channel_file(channel_list)
337
+ end
338
+ def version
339
+ "XMLTV::Grabber version 0.8"
340
+ end
341
+
342
+ def outputfile(chan_id)
343
+ "#{spooldir}/#{channel_name(chan_id)}.xml"
344
+ end
345
+ def channel_name(chan_id)
346
+ "#{chan_id}.#{myname}.#{lang}"
347
+ end
348
+ def channel_display(chan_id)
349
+ all_channels[chan_id]
350
+ end
351
+ def cachefile(chan_id)
352
+ "#{chnbasedir}/#{channel_name(chan_id)}.yaml"
353
+ end
354
+ def load_cachefile(chan_id)
355
+ begin
356
+ YAML.load_file(cachefile(chan_id))
357
+ rescue Errno::ENOENT
358
+ Hash.new
359
+ end
360
+ end
361
+ def check_argv(args)
362
+ case args[0]
363
+ when 'all'
364
+ all_channels.keys
365
+ when 'new'
366
+ XmltvOptions.only_new = true
367
+ all_channels.keys
368
+ else
369
+ args
370
+ end
371
+ end
372
+
373
+ def get_channels
374
+ clean_cache_dir
375
+ if ! ARGV.empty?
376
+ ARGV.replace(check_argv(ARGV))
377
+ else
378
+ config_channels
379
+ end
380
+ end
381
+
382
+ def load_config_file
383
+ begin
384
+ YAML.load_file(config_file_name)
385
+ rescue
386
+ h = Hash.new
387
+ h['channels'] = []
388
+ h
389
+ end
390
+ end
391
+
392
+ def load_channel_file(fn)
393
+ if (fn)
394
+ begin
395
+ YAML.load_file(fn)
396
+ rescue Errno::ENOENT
397
+ fetch_all_channels
398
+ end
399
+ end
400
+ end
401
+ def save_config
402
+ loop do
403
+ if test(?f, config_file_name)
404
+ File.open(config_file_name, 'w') { |h| h.puts config.to_yaml }
405
+ return
406
+ end
407
+ dir = File.dirname(config_file_name)
408
+ if test(?d, dir)
409
+ FileUtils.touch config_file_name
410
+ elsif test(?e, dir)
411
+ raise "#{dir} exists, but isn't a directory"
412
+ else
413
+ FileUtils.mkdir_p dir
414
+ end
415
+ end
416
+ end
417
+
418
+
419
+ def config_channels
420
+ rsl = config['channels']
421
+ if rsl.empty?
422
+ all_channels.keys
423
+ else
424
+ rsl
425
+ end
426
+ end
427
+ def clean_spool_dir
428
+ xmlfiles = config_channels.map{|x| outputfile(x) }
429
+ Dir["#{spooldir}/*.xml"].each do |fn|
430
+ unless xmlfiles.include? fn
431
+ STDERR.puts("removed #{fn} -- not configured")
432
+ File.unlink fn
433
+ end
434
+ end
435
+ end
436
+
437
+
438
+ def clean_cache_dir
439
+ clean_spool_dir
440
+ last_time = config['cleaned']
441
+ if last_time.is_a?(Date) && last_time >= Vandaag
442
+ return
443
+ end
444
+ count = 0
445
+ channels = config_channels.map{|x| cachefile(x) }
446
+ Dir["#{chnbasedir}/*.yaml"].each do |fn|
447
+ unless channels.include? fn
448
+ STDERR.puts("removed #{fn} -- not configured")
449
+ File.unlink fn
450
+ next
451
+ end
452
+ cache = YAML.load_file(fn)
453
+ dels = clean_cache(cache)
454
+ File.open(fn, 'w') {|h| h.puts cache.to_yaml} if dels > 0
455
+ count += dels
456
+ config['cleaned'] = Vandaag
457
+ end
458
+ STDERR.puts("#{myname}: Removed #{count} entries") if count > 0
459
+
460
+ end
461
+
462
+
463
+
464
+
465
+ def proghash(entry, chan_id)
466
+ progdata = Progdata.new
467
+ entry.keys.each do |k|
468
+ dtd = XmlWriter::Programme_dtd.assoc(k)
469
+ if dtd && dtd.size == 1
470
+ progdata[k] = entry[k]
471
+ end
472
+ end
473
+ cnm = channel_name(chan_id)
474
+ progdata['channel'] = MythTV[cnm] || cnm
475
+ progdata
476
+ end
477
+
478
+ def reject(*args)
479
+ File.open(reject_file_name, 'a') do |h|
480
+ h.puts Time.now
481
+ args.each do |arg|
482
+ case arg
483
+ when String, Time, Integer
484
+ h.puts arg
485
+ else
486
+ PP.pp(arg, h)
487
+ end
488
+ end
489
+ h.puts '===='
490
+ end
491
+ end
492
+
493
+
494
+
495
+
496
+ def fix_times(pda)
497
+ errors = 0
498
+ startwith = pda.size
499
+ pda.delete_if do |x|
500
+ rsl = x.has_key?('stop') && x['stop'] <= x['start']
501
+ reject('invalid', x) if rsl
502
+ rsl
503
+ end
504
+ starttimes = pda.sort do |x, y|
505
+ if (a = x['start']) != (b = y['start'])
506
+ a <=> b
507
+ else
508
+ a = x.has_key?('stop') ? x['stop'] : x['start']
509
+ b = y.has_key?('stop') ? y['stop'] : y['start']
510
+ b <=> a
511
+ end
512
+ end
513
+ starttimes.each_with_index do |entry, idx|
514
+ # dump(entry, '++++++++++++++') if entry.has_key?('title') && entry['title'] == 'NOS Studio Voetbal'
515
+ nxt = starttimes[idx + 1]
516
+ next unless nxt
517
+ unless entry.has_key?('stop') && entry['stop'] <= nxt['start']
518
+ errors += 1
519
+ entry['stop'] = nxt['start'].dup
520
+ end
521
+ end
522
+ unless starttimes[-1].has_key?('stop')
523
+ starttimes[-1]['start'] = nil
524
+ end
525
+ pda.delete_if do |x|
526
+ rsl = x['start'].nil? || x['stop'] <= x['start']
527
+ reject('removed', x) if rsl
528
+ rsl
529
+ end
530
+ @rejects = startwith - pda.size
531
+ errors
532
+ end
533
+ def fetch(url)
534
+ @hits += 1
535
+ tries = 0
536
+ begin
537
+ open(url) { |h| Hpricot(h) }
538
+ rescue Errno::ECONNREFUSED, ::Timeout::Error
539
+ tries += 1
540
+ sleep 2
541
+ retry if tries <= 3
542
+ raise BadSiteError.new
543
+ end
544
+ end
545
+ def save_object(obj, filename)
546
+ File.open(filename, 'w') do |h|
547
+ h.puts obj.to_yaml
548
+ end
549
+ end
550
+ def save(*args)
551
+ file = "/tmp/xmltv_exception.#{$$}"
552
+ args.each_with_index do |el, i|
553
+ File.open("#{file}-#{i}", 'w') do |h|
554
+ h.puts el
555
+ end
556
+ end
557
+ STDERR.puts "See #{file}-*"
558
+ end
559
+
560
+ def dump(*args)
561
+ args.each do |arg|
562
+ PP.pp(arg,STDERR)
563
+ end
564
+ end
565
+ def date_stats(chan, time)
566
+ @days_av[chan].add(time.to_date)
567
+ end
568
+ def check_channel(chan_id)
569
+ all_channels.has_key?(chan_id)
570
+ end
571
+
572
+ def add_channels_to_config
573
+ XmltvOptions.add.each do |item|
574
+ if (f = all_channels.has_key?(item)) && ! config['channels'].include?(item)
575
+ config['channels'] << item
576
+ STDERR.puts("added #{item} #{channel_display(item)}") if XmltvOptions.verbose
577
+ else
578
+ reason = f ? "already included" : "not in channel list"
579
+ STDERR.puts "#{item} #{reason}"
580
+ end
581
+ end
582
+ save_config
583
+ end
584
+
585
+ def delete_channels_from_config
586
+ XmltvOptions.del.each do |item|
587
+ if config['channels'].include?(item)
588
+ config['channels'].delete(item)
589
+ STDERR.puts("deleted #{item} #{channel_display(item)}") if XmltvOptions.verbose
590
+ else
591
+ STDERR.puts "#{item} not in config list"
592
+ end
593
+ end
594
+ save_config
595
+ end
596
+ def printline(it)
597
+ printf("%-5s %s\n", it, channel_display(it))
598
+ end
599
+ def do_list(array)
600
+ array.sort_by{|x| channel_display(x)}.each do |item|
601
+ printline item
602
+ end
603
+ end
604
+ def list_config
605
+ if config['channels'].empty?
606
+ explan = channel_list ? 'yet' : 'needed'
607
+ puts "No configuration #{explan} for #{myname}"
608
+ else
609
+ puts "Configured channels for #{myname}"
610
+ do_list(config['channels'])
611
+ end
612
+ end
613
+ def list_all
614
+ puts "Available channels for #{myname}"
615
+ do_list(all_channels.keys)
616
+ end
617
+ def do_options
618
+ if XmltvOptions.list
619
+ list_config
620
+ elsif XmltvOptions.available
621
+ list_all
622
+ else
623
+ if XmltvOptions.add
624
+ add_channels_to_config
625
+ end
626
+ if XmltvOptions.del
627
+ delete_channels_from_config
628
+ end
629
+ list_config
630
+ end
631
+ exit
632
+ end
633
+
634
+ def report(channel, nprogs, errors)
635
+ STDERR.printf(" %s %-6.6s %-20.20s %5d %5d %5d %5d %8d %5d\n",
636
+ Time.now.strftime("%H:%M:%S"),
637
+ channel,
638
+ channel_display(channel),
639
+ nprogs,
640
+ @hits,
641
+ @days_av[channel].size,
642
+ errors,
643
+ test(?s, outputfile(channel)),
644
+ @rejects)
645
+
646
+ @hits = 0
647
+ end
648
+
649
+ def run
650
+ if XmltvOptions.version
651
+ puts version
652
+ exit
653
+ end
654
+ do_options if XmltvOptions.action
655
+ if channel_list && ! test(?f, config_file_name) # We don't want to grab _all_ channels, surely ?
656
+ list_all
657
+ puts "\n Do #{$PROGRAM_NAME} --add chn1,chn2,chn3... to create a configuration file"
658
+ exit
659
+ end
660
+ ARGV.delete_if do |arg|
661
+ begin
662
+ send(arg)
663
+ true
664
+ rescue NoMethodError
665
+ false
666
+ end
667
+ end
668
+ trap('INT') do
669
+ STDERR.puts "\nInterrupted\n"
670
+ raise ArgumentError.new('interrupt')
671
+ # exit!
672
+ end
673
+
674
+ begin
675
+ channels = get_channels
676
+ STDERR.printf("\n%-40s prgs hts days errs bytes rej\n", "#{Date.today}: #{myname}")
677
+ channels.each do |channel|
678
+ next if XmltvOptions.only_new && test(?f, outputfile(channel))
679
+ @days_av = Hash.new { |h,k| h[k] = Set.new }
680
+ @rejects = 0
681
+ writer = XmlWriter.new(self)
682
+ unless check_channel(channel)
683
+ STDERR.puts "No such channel #{channel}"
684
+ next
685
+ end
686
+ begin
687
+ nprogs = grab_channel(channel)
688
+ pda = transform(channel)
689
+ errors = fix_times(pda)
690
+ rescue BadChannelError
691
+ STDERR.puts "Zie #{reject_file_name}"
692
+ next
693
+ end
694
+ writer.write_file(pda, channel)
695
+ report(channel, nprogs, errors)
696
+ end
697
+ rescue SystemExit
698
+
699
+ rescue ::Exception => exc
700
+ STDERR.puts exc.class, exc.message, exc.backtrace
701
+ if XmltvOptions.mailto
702
+ MailSender.sendmail(XmltvOptions.mailto, "Xmltv exception", [exc.class, exc.message, exc.backtrace].join("\n"))
703
+ end
704
+ raise
705
+ ensure
706
+ end
707
+ save_config
708
+ end
709
+
710
+ end
711
+ def go
712
+ if XmltvOptions.add || XmltvOptions.del || XmltvOptions.config
713
+ STDERR.puts "You must specify a site to configure channels"
714
+ exit
715
+ end
716
+ configs = Dir["#{XmltvOptions.basedir}/*"].select{|x| File::Stat.new(x).directory? }.map{|x| File.basename(x)}
717
+ if configs.empty?
718
+ STDERR.puts "No configs found in #{XmltvOptions.basedir}"
719
+ end
720
+ todo = configs & Sites
721
+ exit if todo.empty?
722
+ todo.sort.each do |grabber|
723
+ # puts grabber
724
+ begin
725
+ require "xmltv/sites/#{grabber}"
726
+ rescue SystemExit => exc
727
+ raise unless exc.success?
728
+ end
729
+ end
730
+ end
731
+
732
+ module_function :go
733
+ end
734
+ if __FILE__ == $PROGRAM_NAME
735
+ XMLTV::go
736
+ end
737
+