root/trunk/grabbers/rex @ 1415

Revision 1415, 24.8 kB (checked in by max, 12 months ago)

rex: Prevent returning bad day 8+ data

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