root/grabbers/rex @ 491

Revision 491, 27.1 kB (checked in by max, 6 years ago)

Rex doesn't die on bad web data

Line 
1#!/usr/bin/perl -w
2#
3# "Rex"
4
5my $version  = '3.4.1';
6
7# An Australian TV Guide Grabber (a.k.a. tv_grab_au)
8# by Max Barry
9# http://www.maxbarry.com
10#
11# Based on the long-serving but currently defunct NMSN Australian TV grabber
12# by Michael 'Immir' Smith
13#
14# Use --help for command-line options.
15#
16# A current version of this script, plus a README file, might be here:
17# http://www.whuffy.com/tv_grab_au/
18#
19# To install Perl dependencies (like XMLTV.pm), you generally need to
20# do this (as root): perl -MCPAN -e 'install <whatever>'
21# E.g. perl -MCPAN -e 'install XMLTV::Ask'
22#
23# Changelog:
24# 0.1.0   : Let there be code
25# 0.2.0   : Better caching (fewer HTTP connections)
26# 0.3.0   : Aborted attempt to use proxy caches
27# 0.4.0   : Reverted to non-proxy version; switched datasources
28# 1.0.0   : --configure works; code released
29# 1.0.1   : Bugfix : --config-file now works
30# 1.1.0   : Feature: --stats option
31# 1.1.1   : Bugfix : Download Pay TV show details properly
32# 1.1.2   : Bugfix : Replaced non-working --static with --test;
33#                    put sport and free-to-air movies in correct category
34#                    for MythTV
35# 1.1.3   : Bugfix : Don't die on a failed download, just report
36# 1.2.0   : Feature: --cache-file option
37# 1.2.1   : Bugfix : Better explanation for config-file failure
38# 1.2.2   : Bugfix : Get more than 1 day's data for Pay TV channels
39# 1.2.3   : Bugfix : Translate some category names for MythTV
40# 1.2.4   : Bugfix : Better handling of config file locations
41# 1.2.5   : Bugfix : Corrected typo in a category name
42# 2.0.0   : Datasource change: August 29th, 2006
43# 2.1.0   : Feature: Interpret javascript
44# 2.1.1   : Bugfix : Try to avoid replacing good data with poisoned data
45# 2.1.2   : Bugfix : Minor code tidy
46# 2.2.0   : Feature: --help option
47# 2.2.1   : Bugfix : Don't overwrite cache if no shows found (e.g. net failure)
48# 2.3.0   : Feature: Better detection of changes to pre-fetched guide data
49# 2.3.1   : Bugfix : adapted precache to non-fatal datasource change
50# 2.4.0   : Feature: --quick option
51# 2.4.1   : Bugfix : removed --quick (too hard to accurately parse stop time);
52#                    code tidy
53# 2.4.2   : Bugfix : progress stats on by default
54# 2.5.0   : Feature: altered/added --ignore-cache, --nowrite-cache, and
55#                    --rebuild-cache options; exit on unknown option
56# 3.0.0   : Shepherd compliance
57# 3.1.0   : Feature: --ready option
58# 3.2.0   : Feature: gzip compression, report KB downloaded
59# 3.2.1   : Bugfix : handle failed downloads properly
60# 3.2.2   : Bugfix : no empty sub-titles in cache; no output buffering
61# 3.3.0   : Feature: categorize HDTV, premieres, repeats, original airdates,
62#                    and widescreen broadcats correctly in XMLTV (not all
63#                    supported by MythTV, alas)
64# 3.3.1   : Bugfix : stop dumping cache items unnecessarily
65# 3.3.2   : Bugfix : fixed regexp weirdness
66# 3.3.3   : Bugfix : more regexp tweaking; fixed premiere tag
67# 3.3.4   : Bugfix : tweaked Rex's name in XMLTV output
68# 3.3.5   : Bugfix : support opt_channels (HDTV in Shepherd)
69# 3.3.6   : Bugfix : neater options parsing
70# 3.3.7   : Bugfix : now runs from current dir, not ~/.rex/
71# 3.3.8   : Bugfix : better handling of failed downloads & parses
72# 3.3.9   : Bugfix : more descriptive dies
73# 3.4.0   : Feature: micrograbbing (--gaps_file)
74# 3.4.1   : Bugfix : don't die on bad web data
75
76use strict;
77use Getopt::Long;
78use HTTP::Request::Common;
79use LWP::UserAgent;
80use Date::Manip;
81use File::Path;
82use File::Basename;
83use Data::Dumper;
84use HTML::TreeBuilder;
85use JavaScript;
86use Storable;
87use POSIX;
88
89use XMLTV;
90use XMLTV::Ask;
91
92# ---------------------------------------------------------------------------
93# --- Global Variables
94
95my $progname = "Rex";
96my $lang = "en";
97
98my ($count_dl, $count_detail, $count_bad, $count_cache, $count_changes, $count_kb, $count_bad_parse) = (0) x 7;
99
100my $DATASOURCE             = "http://www.yourtv.com.au";
101my $DATASOURCE_SETUP       = "$DATASOURCE/profile/index.cfm?action=saveRegions";
102my $DATASOURCE_GUIDE       = "$DATASOURCE/guide/index.cfm";
103my $DATASOURCE_GUIDE_TODAY = "$DATASOURCE/guide/index.cfm?action=restofday";
104my $DATASOURCE_DETAIL      = "$DATASOURCE/guide/index.cfm";
105my $WW                     = "http://webwarper.net/ww/";
106
107my $runtime = time();
108my $laststats = $runtime;
109my $firstfetch;
110my $debug = 0;
111my $opt = { };
112my $channels = {};
113my $opt_channels = {};
114my $gaps;
115my %chanid;
116my $cached;
117my $lastpids;
118my $precache;
119my $ua;
120my $sid;
121my $jsc;
122my %shows;
123my $numshows;
124my $dcount;
125
126# ---------------------------------------------------------------------------
127# --- Setup
128
129print "$progname $version\n";
130
131$| = 1;
132
133Getopt::Long::Configure(qw/pass_through/);
134
135get_command_line_options();
136
137print "Ignoring --config-file option.\n" if ($opt->{'config-file'});
138
139exit 0 if ($opt->{version});
140
141help() if ($opt->{help});
142
143set_defaults();
144
145read_channels_file();
146read_gaps_file();
147
148if ($opt->{test})
149{
150  print "Test mode: won't write XML or cache.\n";
151  $opt->{output} = undef;
152}
153       
154build_channel_map();
155
156if ($opt->{'show-channels'})
157{
158  show_channels();
159  exit 0;
160}
161
162if ($debug or $opt->{'show-config'})
163{
164  show_config();
165  exit 0 if ($opt->{'show-config'});
166}
167
168restore_cache();
169
170if ($opt->{dump_cache})
171{
172  dump_cache();
173  exit 0;
174}
175
176setup_javascript();
177
178# ---------------------------------------------------------------------------
179# --- Go!
180
181get_guide_data();
182
183get_details_data();
184
185save_cache() if ($numshows);
186
187write_xml();
188
189print stats(1);
190
191exit 0;
192
193# ---------------------------------------------------------------------------
194# --- Stop!
195
196
197#
198# Subs!
199#
200
201sub get_guide_data
202{
203  print "Grabbing data for days " . ($opt->{offset} + 1) .
204        " - $opt->{days}" . 
205        ($opt->{output} ? " into " . $opt->{output} : '') .
206        ".\n";
207
208  refresh_ua() unless ($ua);
209
210  my ($guidedata, $date);
211
212  for my $day ($opt->{offset} .. $opt->{days} - 1)
213  {
214    $date = Ymd(DateCalc("today", "+ $day days"));
215    print "Day $day.\n" if ($debug);
216
217    if (!$day)
218    {
219      # Special bandwidth-saving URL for day 0
220
221      $guidedata = get_page($DATASOURCE_GUIDE_TODAY);
222      parse_guide($guidedata, $date) if ($guidedata);
223    }
224    else
225    {
226      # Need to grab day in 6-hour chunks.
227      my $rid = $opt->{region};
228      for (1 .. 4)
229      {
230        $guidedata = post_page($DATASOURCE_GUIDE,
231                        [ 'action' => "sessionTimes",
232                          'region_id' => $rid,
233                          'date' => $date,
234                          'period' => $_,
235                          'submit' => 'submit'
236                        ]);
237        parse_guide($guidedata, $date, $_) if ($guidedata);
238      }
239    }
240  }
241  $numshows = scalar(keys %$precache);
242  print "Shows found: $numshows.\n";
243}
244
245#
246# This sub fills up %shows with details, either from the cache or
247# from the web.
248#
249sub get_details_data
250{
251  $firstfetch = time();
252  my $show;
253  foreach my $pid (keys %$precache)
254  {
255    if ($cached->{$pid})
256    {
257      print "Cached: " . $cached->{$pid}->{title}[0][0] . ".\n" if ($debug);
258      if (verify_cache($pid))
259      {
260        $shows{$pid} = $cached->{$pid};
261        $count_cache++;
262      }
263      else
264      {
265        print "Cache has stale data for \"" . $cached->{$pid}->{title}[0][0] .
266              "\": fetching fresh data.\n" if ($debug);
267        $count_changes++;
268      }
269    }
270    unless ($shows{$pid})
271    {
272      $show = download_show($pid);
273      if ($show)
274      {
275        $shows{$pid} = $show;
276        $cached->{$pid} = $show;
277        $count_detail++;
278      }
279      else
280      {
281        print "Failed to parse show $pid.\n";
282      }
283      sleep int(2 + rand(5));
284    }
285    if ($opt->{stats} and time() - $laststats >= $opt->{stats})
286    {
287      print stats();
288    }
289  }
290}
291
292sub verify_cache
293{
294  my $pid = shift;
295
296  my @c = ( 
297            [ 
298              'Title', 
299              $cached->{$pid}->{title}[0][0], 
300              $precache->{$pid}->{title} 
301            ],
302            [ 
303              'Start time',
304              $cached->{$pid}->{start},
305              $precache->{$pid}->{start}
306            ],
307            [
308              'Subtitle',
309              ($cached->{$pid}->{'sub-title'} ? $cached->{$pid}->{'sub-title'}[0][0] : undef),
310              $precache->{$pid}->{'sub-title'}
311            ]
312          ); 
313  foreach my $cmp (@c)
314  {
315    return 0 unless compare_caches($cmp->[0], $cmp->[1], $cmp->[2]);
316  }
317  return 1;
318}
319
320sub compare_caches
321{
322  my ($name, $arg1, $arg2) = @_;
323
324  # Special case for subtitles.
325  return 1 if ($name eq 'Subtitle' and !$arg2);
326 
327  $arg1 = '--missing--' unless $arg1;
328  $arg2 = '--missing--' unless $arg2;
329
330  return 1 if $arg1 eq $arg2;
331
332  print "$name mismatch: \"$arg1\" vs \"$arg2\".\n" if ($debug);
333  return 0;
334}
335
336sub download_show
337{
338  my ($pid, $recurse_count) = @_;
339
340  $recurse_count ||= 0;
341  return undef if ($recurse_count > 2);
342 
343  $dcount++;
344  refresh_ua() if ($dcount % 20 == 0); # don't wait for error page
345
346  my $result;
347
348  print "Downloading # $pid.\n" if ($debug);
349  my $detailsdata = get_page($DATASOURCE_DETAIL .
350                      '?action=session_info&event_id=' . $pid .
351                      '&sid=' . $sid . '&loc=grid');
352  $result = parse_details($detailsdata) if ($detailsdata);
353  unless ($detailsdata and $result)
354  {
355    sleep(5);
356    refresh_ua();
357    return download_show($pid, $recurse_count+1);
358  }
359
360  return $result;
361}
362
363sub save_cache
364{
365  return if ($opt->{test} or $opt->{nowrite_cache});
366  print "Saving cache.\n";
367  Storable::store($cached, $opt->{'cache-file'});
368}
369
370sub write_xml
371{
372  return if ($opt->{test});
373 
374  my %writer_args = ( encoding => 'ISO-8859-1' );
375
376  print "Writing XML.\n";
377
378  if ($opt->{output}) 
379  {
380    my $fh = new IO::File(">" . $opt->{output}) 
381             or die "Can't open " . $opt->{output} . ": $!";
382    $writer_args{OUTPUT} = $fh;
383  }
384
385  my $writer = new XMLTV::Writer(%writer_args);
386
387  $writer->start
388    ( { 'source-info-url'    => $DATASOURCE,
389        'source-info-name'   => "Datasource Name",
390        'generator-info-name' => "$progname $version"} );
391
392  for my $channel (sort keys %$channels) 
393  {
394    my $chanid = $chanid{lc $channel};
395    $writer->write_channel( { 'display-name' => [[$channel, $lang]],
396                              'id' => $chanid } );
397  } 
398
399  foreach my $pid (keys %shows)
400  {
401    print "- " . $shows{$pid}->{'title'}[0][0] . "\n" if ($debug);
402    $writer->write_programme($shows{$pid});
403  }
404
405  $writer->end();
406}
407
408sub refresh_ua
409{
410  print "Refreshing UA.\n" if ($debug);
411 
412  if ($ua)
413  {
414     print stats() if ($debug and $opt->{stats});
415     print "Sleeping...\n" if ($debug);
416     sleep 5 + int(rand(20));
417  }
418
419  my $agent = 
420    ( 
421    'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)',
422    'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.0.4) Gecko/20060508 Firefox/1.5.0.4',
423    'Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.7.6) Gecko/20050512 Firefox',
424    'Opera/9.00 (Windows NT 5.1; U; en)',
425    'Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/412 (KHTML, like Gecko) Safari/412'
426    )[int(rand(5))];
427               
428  $ua = LWP::UserAgent->new
429    ('timeout' => 30,
430     'keep_alive' => 1,
431     'agent' => $agent);
432  $ua->env_proxy;
433  $ua->cookie_jar({});
434
435  # Set initial cookie
436  get_page($DATASOURCE);
437
438  # Set region/service cookie
439  post_page($DATASOURCE_SETUP,
440        [ 'fta_region_id' => $opt->{'region'} ]);
441
442  $ua->cookie_jar()->scan(\&refresh_sid);
443
444  $dcount = 0;
445}
446
447sub refresh_sid
448{
449  my ($version, $key, $val) = @_;
450
451  $sid = $val if ($key eq 'CFID');
452}
453
454sub setup_javascript 
455{
456  print "Initializing JavaScript interpreter.\n" if ($debug);
457  $jsc = new JavaScript::Runtime->create_context();
458  $jsc->set_error_handler( sub { } );
459  $jsc->eval(qq{
460    var doc = '';
461    function Location() { this.href  = '$DATASOURCE'; }
462    function Document() { this.write = function(x) { doc += x; } }
463    function Window()   { this.___ww = 0 }
464    location = new Location;
465    document = new Document;
466    window   = new Window;
467  });
468}
469
470sub restore_cache
471{
472  unless ($opt->{'ignore-cache'} or $opt->{'rebuild-cache'})
473  {
474    if (-r $opt->{'cache-file'})
475    {
476      $cached = Storable::retrieve($opt->{'cache-file'});
477    }
478    else
479    {
480      print "Unable to read cache file: " . $opt->{'cache-file'} . ".\n";
481    }
482  }
483  if ($cached)
484  {
485    print "Retrieved " . keys(%$cached) . " cached items from file.\n";
486    clean_cache();
487  }
488  else
489  {
490    $cached = { };
491    print "Not using cache.\n";
492  }
493}
494
495sub clean_cache
496{
497    my $r = expand_date(localtime(time() - 3600)."");
498    my $c = 0;
499    print "Removing cache items that finish earlier than $r.\n" if ($debug);
500    foreach (keys %$cached)
501    {
502        if (Date_Cmp($r, $cached->{$_}->{stop}) == 1)
503        {
504            print "Removing $cached->{$_}->{title}[0][0].\n" if ($debug);
505            delete $cached->{$_};
506            $c++;
507        }
508    }
509    print "Removed $c stale items from cache.\n";
510}
511
512sub dump_cache
513{
514  print "Cache: " . Dumper($cached) . "\n";
515}
516
517sub stats
518{
519  my $finished = shift;
520
521  my $t = time() - $runtime;
522 
523  my $ret = "$progname $version " . 
524            ($finished ? "finished" : "in progress") .
525            ":\n";
526  $ret .= sprintf(
527        " %d shows grabbed\n" .
528        " %d downloads, including %d detail pages (%d KB)\n" .
529        " %d cache hits, %d changes from cache\n" .
530        " %d failed downloads, %d failed parses\n",
531        scalar(keys %shows),
532        $count_dl, $count_detail, $count_kb, 
533        $count_cache, $count_changes, 
534        $count_bad, $count_bad_parse);
535  $ret .= " Time elapsed: " . timestats($t) . "\n";
536  unless ($finished or !$count_detail)
537  {
538    $t = ((((time() - $firstfetch) * ($numshows - $count_cache)) / $count_detail)) - $t;
539   
540    $ret .= " Estimated time remaining: " . timestats($t) . "\n";
541  }
542  $laststats = time();
543  return $ret;
544}
545
546sub timestats
547{
548  my $t = shift;
549
550  my $ret = '';
551  if ($t >= 3600)
552  {
553    $ret .= sprintf("%d hr ", $t / 3600);
554    $t = $t % 3600;
555  }
556  $ret .= sprintf("%d min %d sec", $t / 60, $t % 60);
557  return $ret;
558}
559
560
561sub get_page
562{
563  my ($url) = @_;
564  my $request = GET $url;
565  return fetch_page($request);
566}
567
568sub post_page
569{
570  my ($url, $headers) = @_;
571  my $request = POST $url, $headers;
572  return fetch_page($request);
573}
574
575sub fetch_page
576{
577  my ($request) = @_;
578 
579  $request->uri() =~ s/^http:\/\//$WW/ if $opt->{warper};
580
581  $request->header('Accept-Encoding' => 'gzip');
582
583  print "Fetching: " . $request->as_string() . "\n" if ($debug);
584  my $response;
585  for my $c (1..2) {
586    print "Attempt #$c.\n" if ($debug);
587    $response = $ua->request($request);
588    last unless ($response->is_error());
589    $count_bad++;
590    print stats() if ($debug);
591    sleep 5;
592  }
593  if ($response->is_error())
594  {
595    print "ERROR! Failed to retrieve page: " . $request->uri() . ".\n";
596    if ($debug and (my $r = $response)->previous) 
597    {
598        print "GET_CONTENT_BASE redirection backtrace:\n";
599        while ($r) { print "    ", $r->base, "\n"; $r = $r->previous }
600    }
601    # Network down
602    if ($count_bad > 10 and $count_dl == 0)
603    {
604        print "ERROR! Unable to download anything useful. Smells like a " .
605              "network problem. Exiting.\n";
606        print stats(1);
607        die "No connectivity";
608    }
609    return undef;
610  }
611  $count_dl++;
612  my $page = $response->content();
613  $count_kb += (do {use bytes; length($page)}) / 1024;
614
615  if ($response->header('Content-Encoding')
616      and
617      $response->header('Content-Encoding') eq 'gzip') 
618  {
619      $page = Compress::Zlib::memGunzip($response->content());
620  }
621
622  $page =~ s/&nbsp;/ /g;
623  return $page;
624}
625
626sub parse_guide
627{
628  my ($guidedata, $date, $phase) = @_;
629
630  print "Parsing guide page.\n" if ($debug);
631
632  $guidedata = readjs(Encode::decode_utf8($guidedata));
633
634  my $tree = HTML::TreeBuilder->new_from_content($guidedata);
635  my $curchan = '';
636  my ($pid, $block, $line, $link, $title);
637  my $c = 0;
638  foreach my $tag ($tree->look_down('_tag' => 'td', 'class' => 'venue'))
639  {
640    $c++;
641    next if ($curchan eq $tag->as_text()); # Ignore repeated Station name
642    $curchan = $tag->as_text();
643    $curchan =~ s/\(.*\)//;
644    if (!$channels->{$curchan})
645    {
646      print "Ignoring unsubscribed channel $curchan.\n" if ($debug);
647      next;
648    }
649    print "Channel: $curchan.\n" if ($debug);
650    $block = $tag->parent();
651    foreach $line ($block->look_down('_tag' => 'td', 'class' => undef))
652    {
653      my ($pid, $title, $start, $pextra);
654      foreach my $bit ($line->look_down('_tag' => 'div'))
655      {
656        if ($bit->attr('class') eq 'ptime')
657        {
658          if ($start)
659          {
660            addme($pid, $title, $start, $pextra, $curchan);
661            $pextra = undef;
662            $title = undef;
663            $pid = undef;
664          }
665          $start = $bit->as_text();
666          if ($phase and $phase == 1 and $start =~ /pm/)
667          {
668            $start = expand_date($start ." ". Ymd(DateCalc($date, "- 1 day")));
669          }
670          else
671          {
672            $start = expand_date("$start $date");
673          }
674        }
675        elsif ($bit->attr('class') eq 'pname')
676        {
677          $title = strip_whitespace($bit->as_text());
678          if (HTML::Entities::decode($bit->as_HTML()) =~ /session_info\('(\d+)/) 
679          {
680            $pid = $1;
681          }
682        }
683        elsif ($bit->attr('class') eq 'pextra')
684        {
685          $pextra = strip_whitespace($bit->as_text());
686        }
687      }
688      if ($pid)
689      {
690        addme($pid, $title, $start, $pextra, $curchan);
691      }
692      else
693      {
694        bad_parse("No pid found in guide data block");
695      }
696    }
697  }
698  bad_parse("Missing data") unless ($c);
699}
700
701sub addme
702{
703  my ($pid, $title, $start, $pextra, $curchan) = @_;
704 
705  if ($precache->{$pid})
706  {
707    print "Duplicate: $title ($pid).\n" if ($debug);
708  }
709  else
710  {
711    print "New: $title " . ($pextra ? "($pextra) " : '') . 
712          "($pid) $start\n" if ($debug);
713    if ($pextra and $pextra =~ /\(\d\d\d\d\)/)
714    {
715      print "Ignoring detail.\n" if ($debug);
716      $pextra = undef;
717    }
718    $precache->{$pid} = { 'title' => $title, 
719                          'start' => $start,
720                          'sub-title' => $pextra
721                        };
722    my $lastpid = $lastpids->{$curchan};
723    if ($lastpid)
724    {
725        $precache->{$lastpid}->{stop} = $start;
726        if ($start < $precache->{$lastpid}->{start}) 
727        {
728            print "Oddness detected in \"" .
729                  $precache->{$lastpid}->{title} .
730                  "\" (pid $pid), starting at " .
731                  $precache->{$lastpid}->{start} .
732                  ".\n";
733            delete $precache->{$lastpid} if ($gaps);
734        }
735        elsif ($gaps)
736        {
737            if (is_outside_gaps($curchan, $precache->{$lastpid}->{start}, $precache->{$lastpid}->{stop}))
738            {
739                delete $precache->{$lastpid};
740            }
741        }
742    }
743    $lastpids->{$curchan} = $pid;
744  }
745}
746
747sub parse_details 
748{
749  my $detailsdata = shift;
750
751  my ($show, $str, @rows, $block, $start, $stop, $date, @extra, @items);
752
753  $detailsdata = readjs(Encode::decode_utf8($detailsdata));
754
755  my $tree = HTML::TreeBuilder->new_from_content($detailsdata);
756
757  $block = $tree->find('h1');
758  return bad_parse("Mising title") unless ($block);  # site is probably sending that block page
759
760  $show->{'title'} = [[ strip_whitespace($block->as_text()), $lang ]];
761  $str = $tree->find('h2');
762  if ($str)
763  {
764    $show->{'sub-title'} = [[ strip_whitespace($str->as_text()), $lang ]];
765  }
766
767  $block = $tree->find('h3') or return bad_parse("Missing section");
768  @rows = $block->look_down('_tag' => 'div');
769  $date = $rows[1]->as_text();
770  if($rows[0]->as_HTML() =~ /(\b[\d\.]+[ap]m)\s+.*?\s+([\d\.]+[ap]m)\b/)
771  {
772    ($start, $stop) = ($1, $2);
773    $show->{'start'} = expand_date("$start $date");
774    $show->{'stop'} = expand_date("$stop $date");
775  }
776  else
777  {
778    return bad_parse("Missing times");  # Failed to parse
779  }
780  if (Date_Cmp($show->{'start'}, $show->{'stop'}) == 1)
781  {
782     $show->{'stop'} = expand_date(DateCalc($show->{'stop'}, "+ 1 day"));
783     print "Adjusted STOP time.\n" if ($debug);
784  }
785  $show->{'channel'} = $chanid{lc($rows[0]->find('span')->as_text())};
786
787  $block = $tree->find('_tag' => 'hr', 'noshade') or return bad_parse("Missing subsection");
788  $block = $block->right();
789  if ($block->as_text())
790  {
791    $show->{'desc'} = [[ strip_whitespace($block->as_text()), $lang ]];
792    $block = $block->right()->right();
793  }
794  else
795  {
796    $block = $block->right();
797  }
798  foreach my $tag ($block->look_down('_tag' => 'tr'))
799  {
800    $str = $tag->as_text();
801    if ($str =~ /Genre:(.+)/)
802    {
803      $str = strip_whitespace($1);
804      if ($str eq 'N/A')
805      {
806        @extra = ( $str );
807      }
808      else
809      {
810        @extra = split(/\//, $str);
811      }
812      $str = $tree->find('h5');
813      if ($str and $str->as_text() =~ /movie/i)
814      {
815        unshift (@extra, 'movie');
816      }
817
818      foreach (@extra)
819      {
820        $_ = [ translate_category($_), $lang ];
821      }
822      $show->{'category'} = [ @extra ];
823    }
824    elsif ($str =~ /Rating:(.+)/)
825    {
826      $str = strip_whitespace($1);
827      if ($str =~ /(.*?)\s*\[(.+)\]/)
828      {
829        @extra = split(/, /, $2);
830        foreach (@extra)
831        {
832          $_ = [ $_, 'advisory', undef ];
833        }
834        $show->{'rating'} = [[ $1, 'ABA', undef],  @extra ];
835      }
836      else
837      {
838        $show->{'rating'} = [[ $1, "ABA", undef]];
839      }
840    }
841    elsif ($str =~ /Cast: (.+)/)
842    {
843      $show->{'credits'}{'actor'} = [ split(/, /, strip_whitespace($1)) ];
844    }
845    elsif ($str =~ /Year:\s*(\d+)/)
846    {
847      $show->{'date'} = strip_whitespace($1);
848    }
849    elsif ($str =~ /Other:(.+)/)
850    {
851      next unless (strip_whitespace($1));
852      $str = $tag->as_HTML();
853      @extra = split(/<br \/>/, $tag->as_HTML());
854      my (@to_add, %video);
855      foreach my $bit (@extra)
856      {
857        $bit =~ s/<.*>//;
858        $bit = strip_whitespace($bit);
859        next unless ($bit);
860        if ($bit eq 'Repeat')
861        {
862            $show->{'previously-shown'} = { };
863            next;   # Don't need this in the desc as well as it's
864                    # highlighted by MythTV
865        }
866        elsif ($bit eq 'Widescreen')
867        {
868            $video{'aspect'} = '16:9';
869        }
870        elsif ($bit eq 'Closed Captions')
871        {
872            $show->{'subtitles'} = [ { 'type' => 'teletext' } ];
873            next;   # Just clutters up the desc
874        }
875        elsif ($bit eq 'Premiere')
876        {
877            $show->{'premiere'} = [ $bit ];
878        }
879        elsif ($bit eq 'High Definition')
880        {
881            $video{'quality'} = 'HDTV';
882        }
883        push (@to_add, $bit);
884        $show->{'video'} = \%video if (%video);
885      }
886      foreach (@to_add)
887      {
888        if ($show->{'desc'})
889        {
890          $show->{'desc'}[0][0] .= " $_.";
891        }
892        else
893        {
894          $show->{'desc'} = [[ "$_.", $lang ]];
895        }
896      }
897    }
898  }
899
900  print Dumper($show) if ($debug);
901  return $show;
902}
903
904sub bad_parse
905{
906  my $msg = shift;
907  print "Parsing error: $msg.\n";
908  $count_bad_parse++;
909  if ($count_bad_parse > 4 and !scalar(keys %shows))
910  {
911      print "ERROR! Unable to parse any shows! Looks like a major problem. " .
912            "Exiting.\n";
913      print stats(1);
914      die "Major parsing error";
915  }
916  return 0;
917}
918
919sub translate_category
920{
921  my %translation = (   'Sport' => 'sports',
922                        'Soap Opera' => 'Soap',
923                        'Science and Technology' => 'Science/Nature',
924                        'Real Life' => 'Reality',
925                        'Cartoon' => 'Animation',
926                        'Family' => 'Children',
927                        'Murder' => 'Crime' );
928                       
929  return $translation{$_} if $translation{$_};
930  return $_;
931}
932
933sub is_outside_gaps
934{
935  my ($ch, $start, $stop) = @_;
936
937  $start = udate_to_epoch($start);
938  $stop = udate_to_epoch($stop);
939
940  foreach my $gap (@{$gaps->{$ch}})
941  {
942      if ($gap =~ /(\d+)-(\d+)/)
943      {
944          return 0 if ($stop > $1 and $start < $2);
945      }
946  }
947  return 1;
948}
949
950sub udate_to_epoch
951{
952    my $udate = shift;
953
954    if ($udate =~ /(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)/)
955    {
956        my ($year, $month, $day, $hour, $minute, $second) = ($1, $2, $3, $4, $5, $6);
957        $month--;
958        $year -= 1900;
959        return POSIX::strftime("%s", $second, $minute, $hour, $day, $month, $year);
960    }
961    die "Unable to parse udate $udate!";
962}
963
964sub readjs {
965  my $data = shift;
966  $data =~ s{<script type="?text/javascript"?[^>]*>(.*?)</script>}{
967    my $x = $1;
968    $jsc->eval(qq{ doc = '' });
969    $jsc->eval($x);
970    $jsc->eval(qq{ doc }) || '';
971  }isge;
972  $data;
973}
974 
975sub read_channels_file 
976{
977    read_config_file('channels', 'channels_file');
978}
979
980sub read_gaps_file
981{
982    read_config_file('gaps', 'gaps_file');
983}
984
985sub read_config_file
986{
987    my ($name, $arg) = @_;
988
989    return unless ($opt->{$arg});
990    print "Reading $name file: $opt->{$arg}\n";
991    if (-r $opt->{$arg})
992    {
993        local (@ARGV, $/) = ($opt->{$arg});
994        no warnings 'all';
995        eval <>;
996        die "Can't parse $name file: $@" if ($@);
997    }
998    else
999    {
1000        print "Unable to read $name file.\n";
1001    }
1002}
1003
1004sub get_command_line_options
1005{
1006  GetOptions($opt, qw(
1007                        help
1008                        debug
1009                        output=s
1010                        days=i
1011                        offset=i
1012                        region=i
1013                        show-config
1014                        show-channels
1015                        rebuild-cache
1016                        ignore-cache
1017                        nowrite-cache
1018                        dump-cache
1019                        cache-file=s
1020                        config-file=s
1021                        channels_file=s
1022                        gaps_file=s
1023                        stats=i
1024                        test
1025                        desc
1026                        version
1027                        warper
1028                    ));
1029  $debug = $opt->{debug};   
1030
1031  if (@ARGV)
1032  {
1033    print "\nERROR: Unknown option(s): @ARGV\n";
1034    help();
1035  }
1036}
1037
1038sub show_config
1039{
1040  my $short = shift;
1041 
1042  print "\nConfiguration\n".
1043          "-------------\n";
1044  unless ($short)
1045  {
1046    print "Debug mode : " . is_set($debug) . "\n" .
1047          "Test mode  : " . is_set($opt->{test}) . "\n" .
1048          "Webwarping : " . is_set($opt->{warper}) . "\n" .
1049          "Cache file : " . $opt->{'cache-file'};
1050    print ' (not reading)' if ($opt->{'ignore-cache'} or $opt->{'rebuild-cache'});
1051    print ' (not updating)' if ($opt->{test} or $opt->{'ignore-cache'} or $opt->{'nowrite-cache'});
1052    print "\n" .
1053          "Output file: " . ($opt->{output} ? $opt->{output} : "None") . "\n" .
1054          "Chann file : $opt->{channels_file}\n" .
1055          ($opt->{gaps_file} ? "Gaps file  : " . $opt->{gaps_file} . "\n" : '') .
1056          "Statistics : " . ($opt->{stats} ? "every " . $opt->{stats} . " seconds" : "off") . "\n";
1057  }
1058  print   "Region ID  : $opt->{region}\n" .
1059          "Days wanted: $opt->{days} (offset: $opt->{offset})\n";
1060  show_channels();
1061  print "\n";
1062}
1063
1064sub is_set
1065{
1066  my $arg = shift;
1067  return $arg ? "Yes" : "No";
1068}
1069
1070sub show_channels
1071{
1072  print "Subscribed channels:\n";
1073  print "    $_ -> $channels->{$_}\n" for sort keys %$channels;
1074}
1075
1076sub build_channel_map
1077{
1078  # --- extract sorted subscribed channel list from config-file hash;
1079  # also compute canonicalised lowercased channel to xmltvid hash
1080  %chanid = map { lc $_, $channels->{$_} } keys %$channels;
1081}
1082
1083sub set_defaults
1084{
1085  my $defaults = {
1086          'days' => 7,
1087          'offset' => 0,
1088          'region' => 94,
1089          'stats' => 300,
1090          'output' => getcwd() . '/output.xmltv',
1091          'cache-file' => getcwd() . '/cache.dat',
1092          'channels_file' => getcwd() . '/channels.conf'
1093  };
1094                                             
1095  foreach (keys %$defaults)
1096  {
1097    unless (defined $opt->{$_})
1098    {
1099      $opt->{$_} = $defaults->{$_};
1100    }
1101  }
1102
1103  $opt->{'days'} = 7 if ($opt->{'days'} > 7);
1104}
1105
1106sub expand_date
1107{
1108  my $t = shift;
1109  $t =~ s/\./:/;
1110  my $ret = ParseDate($t);
1111
1112  # --- append timezone and strip colons
1113#  ($ret .= " $opt->{TZ}") =~ tr/://d;
1114  $ret =~ tr/://d;
1115
1116  return $ret;
1117}
1118
1119sub Ymd { UnixDate($_[0], "%Y-%m-%d") or die "Date problem in Ymd($_[0])" }
1120
1121sub strip_whitespace 
1122{ 
1123  my $str = shift; 
1124  $str =~ s/^\s*(.*?)\s*$/$1/; 
1125  return $str;
1126}
1127
1128sub help
1129{
1130  print q{
1131Command-line options:
1132    --help                 Print this message
1133
1134    --version              Show current version
1135    --show-channels        Show subscribed channels & exit
1136    --show-config          Show configuration details & exit
1137    --dump-cache           Show cache & exit
1138               
1139    --cache-file <file>    Use the specified cache file
1140    --output <file>        Write XML into the specified file
1141    --channels_file <file> Read channel subscriptions from file
1142    --gaps_file <file>     Read gaps from file
1143
1144    --region <n>           Grab data for region code <n>
1145    --days <n>             Grab <n> days of data (today being day 1)
1146    --offset <n>           Skip the first <n> days
1147
1148    --stats <n>            Print stats every n secs (0=off)
1149    --debug                Print lots of debugging output
1150
1151    --warper               Use webwarper.net anonymizer
1152
1153    --test                 Don't write any output or update cache
1154    --ignore-cache         Neither read nor update cache
1155    --nowrite-cache        Read but don't update cache
1156    --rebuild-cache        Destroy and rebuild cache
1157};
1158  exit 0;
1159}
Note: See TracBrowser for help on using the browser.