root/grabbers/rex @ 340

Revision 340, 25.3 kB (checked in by max, 7 years ago)

Better handling of failed downloads and parses.

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