root/trunk/grabbers/news

Revision 1405, 24.1 kB (checked in by max, 2 months ago)

news: Remove JavaScript requirement.

  • Property svn:executable set to *
Line 
1#!/usr/bin/env perl
2#
3# "News"
4#
5
6my $version  = '2.0';
7
8use strict;
9use warnings;
10
11use Getopt::Long;
12use Date::Manip;
13use File::Path;
14use File::Basename;
15use Data::Dumper;
16use HTML::TreeBuilder;
17use POSIX;
18use Encode;
19
20use XMLTV;
21use XMLTV::Ask;
22
23use Shepherd::Common;
24
25# ---------------------------------------------------------------------------
26# --- Global Variables
27
28my $progname = "News";
29my $lang = "en";
30
31my $DATASOURCE             = "http://guides.news.com.au";
32my $DATASOURCE_SETUP       = "$DATASOURCE/television/profile/?action=saveProfile";
33my $DATASOURCE_GUIDE       = "$DATASOURCE/television/guide/";
34my $DATASOURCE_GUIDE_TODAY = "$DATASOURCE/television/guide/?action=restofday";
35my $DATASOURCE_DETAIL      = "$DATASOURCE/television/guide/";
36
37my $runtime = time();
38my $laststats = $runtime;
39my $firstfetch;
40my $debug = 0;
41my $opt = { };
42my $channels;
43my $opt_channels;
44my $gaps;
45my %chanid;
46my $cached;
47my $lastpids;
48my $precache;
49my $ua;
50my %shows;
51my $numshows;
52my %stats;
53my $dcount;
54
55# ---------------------------------------------------------------------------
56# --- Setup
57
58print "$progname $version\n";
59
60$| = 1;
61
62Getopt::Long::Configure(qw/pass_through/);
63
64get_command_line_options();
65
66print "Ignoring --config-file option.\n" if ($opt->{'config-file'});
67
68exit 0 if ($opt->{version});
69
70help() if ($opt->{help});
71
72set_defaults();
73read_channels_file();
74unless ($channels)
75{
76    print "ERROR: No channels requested. Please use --channels_file.\n";
77    exit 2;
78}
79
80read_gaps_file();
81
82if ($opt->{test})
83{
84  print "Test mode: won't write XML or cache.\n";
85  $opt->{output} = undef;
86}
87       
88build_channel_map();
89
90if ($opt->{'show-channels'})
91{
92  show_channels();
93  exit 0;
94}
95
96if ($debug or $opt->{'show-config'})
97{
98  show_config();
99  exit 0 if ($opt->{'show-config'});
100}
101
102restore_cache();
103
104if ($opt->{'dump-cache'})
105{
106  dump_cache();
107  exit 0;
108}
109
110initialize_stats();
111
112# ---------------------------------------------------------------------------
113# --- Go!
114
115get_guide_data();
116
117get_details_data();
118
119save_cache() if ($numshows);
120
121write_xml();
122
123print stats(1);
124
125Shepherd::Common::print_stats($progname, $version, $runtime, %stats);
126
127exit 0;
128
129# ---------------------------------------------------------------------------
130# --- Stop!
131
132
133#
134# Subs!
135#
136
137sub get_guide_data
138{
139  print "Grabbing data for days " . ($opt->{offset} + 1) .
140        " - $opt->{days}" . 
141        ($opt->{output} ? " into " . $opt->{output} : '') .
142        ".\n";
143
144  refresh_ua() unless ($ua);
145
146  my ($guidedata, $date);
147
148  for my $day ($opt->{offset} .. $opt->{days} - 1)
149  {
150    $date = Ymd(DateCalc("now", "+ $day days"));
151    print "Day $day.\n" if ($debug);
152
153    if (!$day)
154    {
155      # Special bandwidth-saving URL for day 0
156
157      my $cacheid = $DATASOURCE_GUIDE_TODAY;
158      if ($cached->{$cacheid}) {
159        $guidedata = $cached->{$cacheid}->{data};
160      } else {
161        $guidedata = get_page($DATASOURCE_GUIDE_TODAY);
162        if ($guidedata) {
163          $cached->{$cacheid}->{data} = $guidedata;
164          $cached->{$cacheid}->{stop} = POSIX::strftime("%Y%m%d%H%M%S", localtime($runtime));
165          $cached->{$cacheid}->{title}[0][0] = $cacheid;
166        }
167      }
168      parse_guide($guidedata, $date) if ($guidedata);
169    }
170    else
171    {
172      # Need to grab day in 6-hour chunks.
173      my $rid = $opt->{region};
174      for (1 .. 4)
175      {
176        my $cacheid = "${DATASOURCE_GUIDE}?r=${rid}&d=${date}&p=$_";
177        if ($cached->{$cacheid}) {
178          $guidedata = $cached->{$cacheid}->{data};
179        } else {
180          $guidedata = get_page(url => $DATASOURCE_GUIDE,
181                        postvars =>
182                        [ 'action' => "grid",
183                          'region_id' => $rid,
184                          'date' => $date,
185                          'period' => $_,
186                          'submit' => 'submit'
187                        ]);
188          if ($guidedata) {
189            $cached->{$cacheid}->{data} = $guidedata;
190            $cached->{$cacheid}->{stop} = POSIX::strftime("%Y%m%d%H%M%S", localtime($runtime));
191            $cached->{$cacheid}->{title}[0][0] = $cacheid;
192          }
193        }
194        parse_guide($guidedata, $date, $_) if ($guidedata);
195      }
196    }
197  }
198  $numshows = scalar(keys %$precache);
199  print "Shows found: $numshows.\n";
200}
201
202#
203# This sub fills up %shows with details, either from the cache or
204# from the web.
205#
206sub get_details_data
207{
208  $firstfetch = time();
209  my $show;
210  foreach my $pid (keys %$precache)
211  {
212    if ($cached->{$pid})
213    {
214      print "Cached: " . $cached->{$pid}->{title}[0][0] . ".\n" if ($debug);
215      if (verify_cache($pid))
216      {
217        $shows{$pid} = $cached->{$pid};
218        $stats{cache_hits}++;
219      }
220      else
221      {
222        print "Cache has stale data for \"" . $cached->{$pid}->{title}[0][0] .
223              "\": fetching fresh data.\n" if ($debug);
224        $stats{cache_changes}++;
225      }
226    }
227    unless ($shows{$pid})
228    {
229      $show = download_show($pid);
230      if ($show)
231      {
232        $show->{channel}=$precache->{$pid}->{channel}; # this is a fix for undistinguished channels
233        $shows{$pid} = $show;
234        $cached->{$pid} = $show;
235        $stats{detail_pages}++;
236      }
237      else
238      {
239        print "Failed to parse show $pid.\n";
240      }
241    }
242    if ($opt->{stats} and time() - $laststats >= $opt->{stats})
243    {
244      print stats();
245    }
246  }
247}
248
249sub verify_cache
250{
251  my $pid = shift;
252
253  my @c = ( 
254            [ 
255              'Title', 
256              $cached->{$pid}->{title}[0][0], 
257              $precache->{$pid}->{title} 
258            ],
259            [ 
260              'Start time',
261              $cached->{$pid}->{start},
262              $precache->{$pid}->{start}
263            ],
264            [
265              'Stop time',
266              $cached->{$pid}->{stop},
267              $precache->{$pid}->{stop}
268            ],
269            [
270              'Subtitle',
271              ($cached->{$pid}->{'sub-title'} ? $cached->{$pid}->{'sub-title'}[0][0] : undef),
272              $precache->{$pid}->{'sub-title'}
273            ]
274          ); 
275  foreach my $cmp (@c)
276  {
277    return 0 unless compare_caches($cmp->[0], $cmp->[1], $cmp->[2]);
278  }
279  return 1;
280}
281
282sub compare_caches
283{
284  my ($name, $arg1, $arg2) = @_;
285
286  # Special case for subtitles.
287  return 1 if ($name eq 'Subtitle' and !$arg2);
288  # Special case for no precache stop.
289  return 1 if ($name eq 'Stop time' and !$arg2);
290 
291  $arg1 = '--missing--' unless $arg1;
292  $arg2 = '--missing--' unless $arg2;
293
294  return 1 if $arg1 eq $arg2;
295
296  print "$name mismatch: \"$arg1\" vs \"$arg2\".\n" if ($debug);
297  return 0;
298}
299
300sub download_show
301{
302  my ($pid, $recurse_count) = @_;
303
304  $recurse_count ||= 0;
305  return undef if ($recurse_count > 2);
306 
307  $dcount++;
308#  refresh_ua() if ($dcount % 20 == 0); # don't wait for error page
309
310  my $result;
311
312  print "Downloading # $pid.\n" if ($debug);
313  my $detailsdata = get_page($DATASOURCE_DETAIL .
314                      '?action=event&event_id=' . $pid .
315                      '&refresh=&region_id=' . $opt->{region});
316  $result = parse_details($detailsdata) if ($detailsdata);
317  unless ($detailsdata and $result)
318  {
319    refresh_ua();
320    return download_show($pid, $recurse_count+1);
321  }
322
323  return $result;
324}
325
326sub save_cache
327{
328  return if ($opt->{test} or $opt->{nowrite_cache});
329  print "Saving cache.\n";
330  Shepherd::Common::write_cache($opt->{'cache-file'}, $cached);
331}
332
333sub write_xml
334{
335  return if ($opt->{test});
336 
337  my %writer_args = ( encoding => 'ISO-8859-1' );
338
339  print "Writing XML.\n";
340
341  if ($opt->{output}) 
342  {
343    my $fh = new IO::File(">" . $opt->{output}) 
344             or die "Can't open " . $opt->{output} . ": $!";
345    $writer_args{OUTPUT} = $fh;
346  }
347
348  my $writer = new XMLTV::Writer(%writer_args);
349
350  $writer->start
351    ( { 'source-info-url'    => $DATASOURCE,
352        'source-info-name'   => "Datasource Name",
353        'generator-info-name' => "$progname $version"} );
354
355  for my $channel (sort keys %$channels) 
356  {
357    my $chanid = $chanid{lc $channel};
358    $writer->write_channel( { 'display-name' => [[$channel, $lang]],
359                              'id' => $chanid } );
360  } 
361
362  foreach my $pid (keys %shows)
363  {
364    print "- " . $shows{$pid}->{'title'}[0][0] . "\n" if ($debug);
365    $writer->write_programme($shows{$pid});
366  }
367
368  $writer->end();
369}
370
371sub refresh_ua
372{
373  print "Refreshing UA.\n" if ($debug);
374 
375  if ($ua)
376  {
377     print stats() if ($debug and $opt->{stats});
378     print "Sleeping...\n" if ($debug);
379     sleep_for(2 + int(rand(10)));
380  }
381
382  $ua = Shepherd::Common::setup_ua( cookie_jar => 1 );
383
384  # Set region/service cookie
385  unless (get_page(url => $DATASOURCE_SETUP, retries => 4, postvars => [ 'fta_region_id' => $opt->{'region'} ]))
386  {
387      print "Unable to fetch profile page. No network? Exiting.\n";
388      print stats(1);
389      die "Unable to fetch front page (no network?)";
390  }
391
392  $dcount = 0;
393}
394
395sub restore_cache
396{
397  unless ($opt->{'ignore-cache'} or $opt->{'rebuild-cache'})
398  {
399    $cached = Shepherd::Common::read_cache(\$opt->{'cache-file'});
400  }
401  if ($cached)
402  {
403    print "Retrieved " . keys(%$cached) . " cached items from file.\n";
404    clean_cache();
405  }
406  else
407  {
408    $cached = { };
409    print "Not using cache.\n";
410  }
411}
412
413sub clean_cache
414{
415    my $r = expand_date(localtime(time() - 3*3600)."");
416    my $c = 0;
417    print "Removing cache items that finish earlier than $r.\n" if ($debug);
418    foreach (keys %$cached)
419    {
420        if (Date_Cmp($r, $cached->{$_}->{stop}) == 1)
421        {
422            print "Removing $cached->{$_}->{title}[0][0].\n" if ($debug);
423            delete $cached->{$_};
424            $c++;
425        }
426    }
427    print "Removed $c stale items from cache.\n";
428}
429
430sub dump_cache
431{
432  print "Cache: " . Dumper($cached) . "\n";
433}
434
435sub initialize_stats
436{
437    %stats = ( );
438    foreach (qw( successful_requests failed_requests slept_for bytes detail_pages cache_hits cache_changes failed_parses ))
439    {
440        $stats{$_} = 0;
441    }
442}
443
444sub stats
445{
446  my $finished = shift;
447
448  my $t = time() - $runtime;
449 
450  my $ret = "$progname $version " . 
451            ($finished ? "finished" : "in progress") .
452            ":\n";
453  $ret .= sprintf
454        " %d shows grabbed\n" .
455        " %d downloads, including %d detail pages (%d KB)\n" .
456        " %d cache hits, %d changes from cache\n" .
457        " %d failed downloads, %d failed parses\n",
458        scalar(keys %shows),
459        $stats{successful_requests}, $stats{detail_pages}, ($stats{bytes}/1024),
460        $stats{cache_hits}, $stats{cache_changes},
461        $stats{failed_requests}, $stats{failed_parses};
462  $ret .= sprintf " Time elapsed: %s (spent sleeping: %s)\n",
463                  timestats($t), timestats($stats{slept_for});
464  unless ($finished or !$stats{detail_pages})
465  {
466    $t = ((((time() - $firstfetch) * ($numshows - $stats{cache_hits})) / $stats{detail_pages})) - $t;
467   
468    $ret .= " Estimated time remaining: " . timestats($t) . "\n";
469  }
470  $laststats = time();
471  return $ret;
472}
473
474sub timestats
475{
476  my $t = shift;
477
478  my $ret = '';
479  if ($t >= 3600)
480  {
481    $ret .= sprintf("%d hr ", $t / 3600);
482    $t = $t % 3600;
483  }
484  $ret .= sprintf("%d min %d sec", $t / 60, $t % 60);
485  return $ret;
486}
487
488sub get_page
489{
490  my $ret = Shepherd::Common::get_url(@_);
491  if ($stats{successful_requests} and $stats{failed_requests} > ($stats{successful_requests} * 5))
492  {
493      printf "Exiting: %d failed network requests vs %d successes.\n",
494             $stats{failed_requests}, $stats{successful_requests};
495      print stats(1);
496      die "Too many network failures.";
497  }
498  $ret =~ s/ / /g if ($ret);
499  return $ret;
500}
501
502sub parse_guide
503{
504  my ($guidedata, $date, $phase) = @_;
505
506  print "Parsing guide page.\n" if ($debug);
507
508  $phase ||= 0;
509
510  $guidedata = Encode::decode_utf8($guidedata);
511
512  my $tree = HTML::TreeBuilder->new_from_content($guidedata);
513  my $curchan = '';
514  my @channels_seen;
515  my ($pid, $block, $line, $link, $title);
516  my $c = 0;
517  foreach my $tag ($tree->look_down('_tag' => 'td', 'class' => 'venue'))
518  {
519    # Some screens repeat channel name
520    next if ($tag->left);
521
522    $c++;
523    my $channame = $tag->as_text();
524    my $curchan = translate_channel_name($tag->as_text(), scalar(grep($_ eq $channame, @channels_seen)));
525    push @channels_seen, $channame;
526    if (!$channels->{$curchan})
527    {
528      print "Ignoring unsubscribed channel $curchan.\n" if ($debug);
529      next;
530    }
531    print "Channel: $curchan.\n" if ($debug);
532    $block = $tag->parent();
533    foreach $line ($block->look_down('_tag' => 'td', 'class' => undef))
534    {
535      my ($pid, $title, $start, $pextra);
536
537      my $linetext = $line->as_text();
538
539      foreach my $href ($line->look_down('_tag' => 'a'))
540      {
541          $title = strip_whitespace($href->as_text());
542          unless (HTML::Entities::decode($href->as_HTML()) =~ /event_popup\('(\d+)/)
543          {
544              &bad_parse("No pid found in guide data block");
545              next;
546          }
547          $pid = $1;
548          if ($linetext =~ /^(.*?)\Q$title\E(.*)/)
549          {
550              $start = strip_whitespace($1);
551              $linetext = $2;
552              if ($phase and $phase == 1 and $start =~ /pm/)
553              {
554                  $start = expand_date($start ." ". Ymd(DateCalc($date, "- 1 day")));
555              }
556              else
557              {
558                  $start = expand_date("$start $date");
559              }
560              if ($start)
561              {
562                  &addme($pid, $title, $start, undef, $curchan);
563                  next;
564              }
565          }
566          &bad_parse("Failed to match start time out of " . $href->as_HTML);
567      }
568    }
569  }
570  bad_parse("No shows found on guide page ($date phase $phase)") unless ($c);
571  $tree->delete();
572}
573
574sub addme
575{
576  my ($pid, $title, $start, $pextra, $curchan) = @_;
577 
578  if ($precache->{$pid})
579  {
580    print "Duplicate: $title ($pid) $start.\n" if ($debug);
581  }
582  else
583  {
584    print "New: $title " . ($pextra ? "($pextra) " : '') . 
585          "($pid) $start\n" if ($debug);
586    if ($pextra and $pextra =~ /\(\d\d\d\d\)/)
587    {
588      print "Ignoring detail.\n" if ($debug);
589      $pextra = undef;
590    }
591    $precache->{$pid} = { 'title' => $title, 
592                          'start' => $start,
593                          'sub-title' => $pextra,
594                          'channel' => $channels->{$curchan}
595                        };
596    my $lastpid = $lastpids->{$curchan};
597    if ($lastpid)
598    {
599        $precache->{$lastpid}->{stop} = $start;
600        if ($start < $precache->{$lastpid}->{start}) 
601        {
602            print "Oddness detected in \"" .
603                  $precache->{$lastpid}->{title} .
604                  "\" (pid $pid), starting at " .
605                  $precache->{$lastpid}->{start} .
606                  ".\n";
607            delete $precache->{$lastpid} if ($gaps);
608        }
609        elsif ($gaps)
610        {
611            if (is_outside_gaps($curchan, $precache->{$lastpid}->{start}, $precache->{$lastpid}->{stop}))
612            {
613                delete $precache->{$lastpid};
614            }
615        }
616    }
617    $lastpids->{$curchan} = $pid;
618  }
619}
620
621# The guide pages for some regions don't provide any way to distinguish
622# between (eg) "Prime (Regional)" and "Prime (Albury)" in the same region,
623# other than the order in which they're listed. (They're both called
624# simply "Prime".) This func tracks which is which.
625sub translate_channel_name
626{
627    my ($chan, $seen_before) = @_;
628
629    my $rchans;
630#     $rchans = {
631#       90 => { 'Prime' => [ 'Prime (Regional Victoria)', 'Prime (Albury)' ] },
632#       };
633
634    $rchans = {
635        95 => { 'TEN' => [ 'TEN (VIC: Mildura/Sunraysia)', 'TEN (Mildura Digital)' ] },
636        };
637
638    my $region = $opt->{region};
639    return $chan unless ($rchans->{$region} and $rchans->{$region}->{$chan});
640    return $rchans->{$region}->{$chan}->[$seen_before];
641}
642
643sub parse_details 
644{
645  my $detailsdata = shift;
646
647  my ($show, $str, @rows, $block, $start, $stop, $date, $time, @extra, @items);
648
649  $detailsdata = Encode::decode_utf8($detailsdata);
650
651  my $tree = HTML::TreeBuilder->new_from_content($detailsdata);
652
653  my $event = $tree->look_down('_tag' => 'div', 'class' => 'event');
654
655  return &bad_parse("Mising title") unless ($event);  # site is probably sending that block page
656
657  $show->{'title'} = [[ strip_whitespace($event->look_down('_tag' => 'h1')->as_text()), $lang ]];
658  $str = $tree->find('h2');
659  if ($str)
660  {
661    $show->{'sub-title'} = [[ strip_whitespace($str->as_text()), $lang ]];
662  }
663
664  $time = $tree->find('h3') or return &bad_parse("Missing section: Time");
665  $date = $tree->find('h4') or return &bad_parse("Missing section: Date");
666  if(HTML::Entities::decode($time->as_HTML()) =~ /(\b[\d\.]+[ap]m)\s+.*?\s+([\d\.]+[ap]m) (.*)</)
667  {
668    ($start, $stop) = ($1, $2);
669    $date = $date->as_text();
670    $show->{'start'} = &expand_date("$start $date");
671    $show->{'stop'} = &expand_date("$stop $date");
672    # This guide always has the stop time 1 minute early!
673    $show->{'stop'} = &expand_date(DateCalc($show->{'stop'}, "+ 1 minute"));
674    $show->{'channel'} = $3;
675  }
676  else
677  {
678    return &bad_parse("Missing times"); 
679  }
680  if (Date_Cmp($show->{'start'}, $show->{'stop'}) == 1)
681  {
682     $show->{'stop'} = &expand_date(DateCalc($show->{'stop'}, "+ 1 day"));
683     print "Adjusted STOP time.\n" if ($debug);
684  }
685
686  $block = $event->look_down('_tag' => 'p');
687  if ($block and $block->as_text())
688  {
689    $show->{'desc'} = [[ &strip_whitespace($block->as_text()), $lang ]];
690  }
691
692  my ($category, %type);
693  foreach my $tag ($event->look_down('_tag' => 'tr'))
694  {
695    $str = $tag->as_text();
696    if ($str =~ /Genre:(.+)/)
697    {
698      $category = &strip_whitespace($1);
699      $str = $tree->find('h5');
700      $type{movie} = 1 if ($str and $str->as_text() =~ /movie/i);
701    }
702    elsif ($str =~ /Rating:(.+)/)
703    {
704      $str = &strip_whitespace($1);
705      if ($str =~ /(.*?)\s*\[(.+)\]/)
706      {
707          $str = $1;
708          my $sr = $2;
709          $sr =~ s/or medical procedures//ig;
710          $sr = &Shepherd::Common::subrating($sr);
711          $str .= " $sr";
712      }
713      $show->{'rating'} = [[ $str, 'ABA', undef]];
714    }
715    elsif ($str =~ /Cast:(.+)/)
716    {
717      $show->{'credits'}{'actor'} = [ split(/, /, &strip_whitespace($1)) ];
718    }
719    elsif ($str =~ /Director:(.+)/)
720    {
721      $show->{'credits'}{'director'} = [ split(/, /, &strip_whitespace($1)) ];
722    }
723    elsif ($str =~ /Writer:(.+)/) # unseen
724    {
725      $show->{'credits'}{'writer'} = [ split(/, /, &strip_whitespace($1)) ];
726    }
727    elsif ($str =~ /Year:\s*(\d+)/)
728    {
729      $show->{'date'} = &strip_whitespace($1);
730    }
731    elsif ($str =~ /Country:(.+)/)
732    {
733      $show->{'country'} = [[ split(/, /, &strip_whitespace($1)) ]];
734    }
735    elsif ($str =~ /Language:(.+)/)
736    {
737      $show->{'language'} = [ &strip_whitespace($1) ];
738    }
739    elsif ($str =~ /Duration:(\d+)/)
740    {
741      $show->{'length'} = $1 * 60;
742    }
743    elsif ($str =~ /^ /)
744    {
745      my $td = $tag->look_down('_tag' => 'td');
746      $str = $td->as_HTML();
747      @extra = split(/<br ?\/?>/, $str);
748      my %video;
749      foreach my $bit (@extra)
750      {
751        $bit =~ s/<.*>//;
752        $bit = &strip_whitespace($bit);
753        next unless ($bit);
754        if ($bit eq 'Repeat')
755        {
756            $show->{'previously-shown'} = { };
757        }
758        elsif ($bit eq 'Widescreen')
759        {
760            $video{'aspect'} = '16:9';
761        }
762        elsif ($bit eq 'High Definition')
763        {
764            $video{'quality'} = 'HDTV';
765        }
766        elsif ($bit eq 'Closed Captions')
767        {
768            $show->{'subtitles'} = [ { 'type' => 'teletext' } ];
769        }
770        elsif ($bit eq 'New Episode')
771        {
772            $show->{'premiere'} = [ $bit ];
773        }
774        elsif ($bit eq 'Premiere')
775        {
776            $show->{'premiere'} = [ $bit ];
777            $type{premiere} = 1;
778        }
779        elsif ($bit eq 'LIVE' || $bit eq 'Live')
780        {
781            $type{live} = 1;
782        }
783        elsif ($bit eq 'Final' || $bit eq 'Finale')
784        {
785            $type{final} = 1;
786        }
787        elsif ($bit eq 'Return') # unseen
788        {
789            $type{return} = 1;
790        }
791        $show->{'video'} = \%video if (%video);
792      }
793    }
794  }
795  $show->{'category'} = [ &Shepherd::Common::generate_category(
796      $show->{'title'}[0][0], $category, %type) ];
797
798  $tree->delete();
799  print Dumper($show) if ($debug);
800  return $show;
801}
802
803sub bad_parse
804{
805  my $msg = shift;
806  print "Parsing error: $msg.\n";
807  $stats{failed_parses}++;
808  if ($stats{failed_parses} > 4 and !scalar(keys %$precache))
809  {
810      print "ERROR! Unable to parse any shows! Looks like a major problem. " .
811            "Exiting.\n";
812      print stats(1);
813      die "Major parsing error";
814  }
815  return 0;
816}
817
818sub is_outside_gaps
819{
820  my ($ch, $start, $stop) = @_;
821
822  $start = udate_to_epoch($start);
823  $stop = udate_to_epoch($stop);
824
825  foreach my $gap (@{$gaps->{$ch}})
826  {
827      if ($gap =~ /(\d+)-(\d+)/)
828      {
829          return 0 if ($stop > $1 and $start < $2);
830      }
831  }
832  return 1;
833}
834
835sub udate_to_epoch
836{
837    my $udate = shift;
838
839    if ($udate =~ /(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)/)
840    {
841        my ($year, $month, $day, $hour, $minute, $second) = ($1, $2, $3, $4, $5, $6);
842        $month--;
843        $year -= 1900;
844        return POSIX::strftime("%s", $second, $minute, $hour, $day, $month, $year);
845    }
846    die "Unable to parse udate $udate!";
847}
848
849sub read_channels_file 
850{
851    read_config_file('channels', 'channels_file');
852}
853
854sub read_gaps_file
855{
856    read_config_file('gaps', 'gaps_file');
857}
858
859sub read_config_file
860{
861    my ($name, $arg) = @_;
862
863    return unless ($opt->{$arg});
864    print "Reading $name file: $opt->{$arg}\n";
865    if (-r $opt->{$arg})
866    {
867        local (@ARGV, $/) = ($opt->{$arg});
868        no warnings 'all';
869        eval <>;
870        die "Can't parse $name file: $@" if ($@);
871    }
872    else
873    {
874        print "Unable to read $name file.\n";
875    }
876}
877
878sub get_command_line_options
879{
880  GetOptions($opt, qw(
881                        help
882                        debug
883                        output=s
884                        days=i
885                        offset=i
886                        region=i
887                        show-config
888                        show-channels
889                        rebuild-cache
890                        ignore-cache
891                        nowrite-cache
892                        dump-cache
893                        cache-file=s
894                        config-file=s
895                        channels_file=s
896                        gaps_file=s
897                        stats=i
898                        test
899                        desc
900                        version
901                        warper
902                    ));
903  $debug = $opt->{debug};   
904
905  if (@ARGV)
906  {
907    print "\nWARNING: Ignoring unknown option(s): @ARGV\n\n";
908  }
909}
910
911sub show_config
912{
913  my $short = shift;
914 
915  print "\nConfiguration\n".
916          "-------------\n";
917  unless ($short)
918  {
919    print "Debug mode : " . is_set($debug) . "\n" .
920          "Test mode  : " . is_set($opt->{test}) . "\n" .
921          "Webwarping : " . is_set($opt->{warper}) . "\n" .
922          "Cache file : " . $opt->{'cache-file'};
923    print ' (not reading)' if ($opt->{'ignore-cache'} or $opt->{'rebuild-cache'});
924    print ' (not updating)' if ($opt->{test} or $opt->{'ignore-cache'} or $opt->{'nowrite-cache'});
925    print "\n" .
926          "Output file: " . ($opt->{output} ? $opt->{output} : "None") . "\n" .
927          "Chann file : $opt->{channels_file}\n" .
928          ($opt->{gaps_file} ? "Gaps file  : " . $opt->{gaps_file} . "\n" : '') .
929          "Statistics : " . ($opt->{stats} ? "every " . $opt->{stats} . " seconds" : "off") . "\n";
930  }
931  print   "Region ID  : $opt->{region}\n" .
932          "Days wanted: $opt->{days} (offset: $opt->{offset})\n";
933  show_channels();
934  print "\n";
935}
936
937sub is_set
938{
939  my $arg = shift;
940  return $arg ? "Yes" : "No";
941}
942
943sub show_channels
944{
945  print "Subscribed channels:\n";
946  print "    $_ -> $channels->{$_}\n" for sort keys %$channels;
947}
948
949sub build_channel_map
950{
951  # --- extract sorted subscribed channel list from config-file hash;
952  # also compute canonicalised lowercased channel to xmltvid hash
953  %chanid = map { lc $_, $channels->{$_} } keys %$channels;
954}
955
956sub set_defaults
957{
958  my $defaults = {
959          'days' => 7,
960          'offset' => 0,
961          'region' => 94,
962          'stats' => 90,
963          'output' => getcwd() . '/output.xmltv',
964          'cache-file' => getcwd() . '/cache.dat',
965          'channels_file' => getcwd() . '/channels.conf'
966  };
967                                             
968  foreach (keys %$defaults)
969  {
970    unless (defined $opt->{$_})
971    {
972      $opt->{$_} = $defaults->{$_};
973    }
974  }
975
976#  $opt->{'days'} = 7 if ($opt->{'days'} > 7);
977
978  Shepherd::Common::set_defaults(
979        stats => \%stats, 
980        delay => "1-4",
981        debug => $debug,
982        webwarper => $opt->{warper}
983    );
984}
985
986sub expand_date
987{
988  my $t = shift;
989  $t =~ s/\./:/;
990  $t="$1:00$2 $3" if ($t =~ m:^(\d+)([ap]m)\s+(.*?)$:o);
991  my $ret = ParseDate($t);
992
993  # strip colons
994  $ret =~ tr/://d;
995
996  return $ret;
997}
998
999sub Ymd { UnixDate($_[0], "%Y-%m-%d") or die "Date problem in Ymd($_[0])" }
1000
1001sub strip_whitespace 
1002{ 
1003  my $str = shift; 
1004  $str =~ s/^\s*(.*?)\s*$/$1/; 
1005  return $str;
1006}
1007
1008sub sleep_for
1009{
1010    my $n = shift;
1011    return unless ($n);
1012    sleep $n;
1013    $stats{slept_for} += $n;
1014}
1015
1016sub help
1017{
1018  print q{
1019Command-line options:
1020    --help                 Print this message
1021
1022    --version              Show current version
1023    --show-channels        Show subscribed channels & exit
1024    --show-config          Show configuration details & exit
1025    --dump-cache           Show cache & exit
1026               
1027    --cache-file <file>    Use the specified cache file
1028    --output <file>        Write XML into the specified file
1029    --channels_file <file> Read channel subscriptions from file
1030    --gaps_file <file>     Read gaps from file
1031
1032    --region <n>           Grab data for region code <n>
1033    --days <n>             Grab <n> days of data (today being day 1)
1034    --offset <n>           Skip the first <n> days
1035
1036    --stats <n>            Print stats every n secs (0=off)
1037    --debug                Print lots of debugging output
1038
1039    --warper               Use webwarper.net anonymizer
1040
1041    --test                 Don't write any output or update cache
1042    --ignore-cache         Neither read nor update cache
1043    --nowrite-cache        Read but don't update cache
1044    --rebuild-cache        Destroy and rebuild cache
1045};
1046  exit 0;
1047}
Note: See TracBrowser for help on using the browser.