root/grabbers/rex @ 516

Revision 516, 27.3 kB (checked in by max, 6 years ago)

More accurate cache verification for Rex

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