root/grabbers/rex @ 584

Revision 584, 28.6 kB (checked in by max, 6 years ago)

Rex support for regions 71, 79, and 90

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