root/engines/roo @ 209

Revision 209, 46.7 kB (checked in by lincoln, 7 years ago)

simplify component logic into apps and engines

  • Property svn:executable set to *
Line 
1#!/usr/bin/perl -w
2
3my $myprogname = 'dog';
4my $progname = 'shepherd';
5my $version = '0.3.1';
6
7# 'dog'
8# "Shepherd"
9# A wrapper for various Aussie TV guide data grabbers
10#
11# Use --help for command-line options.
12#
13# Shepherd is an attempt to reconcile many different tv_grab_au scripts and
14# make one cohesive reliable data set. It works by calling a series of
15# scripts that grab data from a large variety of sources, and then
16# analysing the resulting XML data sets and determining which of the many
17# is the most reliable.
18
19# Shepherd runs in 4 passes:
20#  pass 1: (app/shepherd)  Checks that all components are up-to-date, auto-
21#                          updates if not.
22#                          Passes control onto shepherd
23#  pass 2: (engine/dog)    calls grabbers to fill in missing data
24#  pass 3: (engine/dog)    calls reconciler to reconcile overlapping data
25#                          and normalize programme titles to our preferred title
26#  pass 4: (engine/dog)    calls postprocessors to postprocess data
27#                          (e.g. flag HDTV programmes, augment with IMDb etc.)
28
29# Changelog:
30# 0.3.1  : split 'dog' into its own engine
31
32BEGIN { *CORE::GLOBAL::die = \&my_die; }
33
34use strict;
35no strict 'refs';
36
37use LWP::UserAgent;
38use Getopt::Long;
39use Data::Dumper;
40use XMLTV;
41use POSIX qw(strftime mktime);
42use Date::Manip;
43use Algorithm::Diff;
44use List::Compare;
45
46# ---------------------------------------------------------------------------
47# --- Global Variables
48# ---------------------------------------------------------------------------
49
50my @options = @ARGV;
51
52# By default, Shepherd runs from ~/.shepherd/. If it's not run as a user,
53# it will try /opt/shepherd/ instead.
54my $CWD = ($ENV{HOME} ? $ENV{HOME} . "/." : "/opt/") . $progname;
55-d $CWD or mkdir $CWD or die "Cannot create directory $CWD: $!";
56chdir($CWD);
57
58#### analyzer settings ####
59# the following thresholds are used to control whether we keep calling grabbers or
60# not.
61
62my %policy;
63$policy{timeslot_size} = (5 * 60);      # 5 minute slots
64
65# PEAK timeslots -
66#  between 4.30pm and 11.30pm every day, only allow a maximum of
67#  15 minutes "programming data" missing
68#  if there is more than this, we will continue asking grabbers for more
69#  programming on this channel
70$policy{peak_max_missing} = 15*60;              # up to 15 mins max allowed missing
71$policy{peak_start} = (16*(60*60))+(30*60);     # 4.30pm
72$policy{peak_stop} = (23*(60*60))+(30*60);      # 11.30pm
73
74# NON-PEAK timeslots -
75#  between midnight and 6.15am every day, only allow up to 6 hours missing
76#  if there is more than this, we will continue asking grabbers for more
77#  programming on this channel
78$policy{nonpeak_max_missing} = 6*(60*60);       # up to 6 hours can be missing
79$policy{nonpeak_start} = 0;                     # midnight
80$policy{nonpeak_stop} = (6*(60*60))+(15*60);    # 6.15am
81
82# all other timeslots - (6.15am-4.30pm, 11.30pm-midnight)
83#  allow up to 60 minutes maximum missing programming
84$policy{other_max_missing} = 60*60;             # up to 60 mins max allowed missing
85
86# if a postprocessor failed 5 times in a row, automatically disable it
87$policy{postprocessor_disable_failure_threshold} = 5;
88
89#### end analyzer section ####
90
91my $opt = { };
92my $pref_title_source;
93my $mirror_site;
94my $debug = 0;
95my $components = { };
96my $gscore;
97my $region;
98my $channels;
99my $opt_channels;
100my $config_file =   "$CWD/$progname.conf";
101my $channels_file = "$CWD/channels.conf";
102my $days = 7;
103my $missing;
104my $timeslice;
105my $grabbed;
106my $gmt_offset;
107my $grabber_found_all_data;
108my $writer;
109my $components_used = "";
110
111# postprocessing
112my $langs = [ 'en' ];
113my $plugin_data = { };
114my $channel_data = { };
115my $reconciler_found_all_data;
116my $input_postprocess_file = "";
117
118# OBSOLETE: will be removed
119my $preferred;
120my $title_translation_table;
121my $pref_order;
122
123# ---------------------------------------------------------------------------
124# --- Setup
125# ---------------------------------------------------------------------------
126
127# Any options Shepherd doesn't understand, we'll pass to the grabber(s)
128Getopt::Long::Configure(qw/pass_through/);
129&get_command_line_options;
130
131if ($opt->{version}) { print "$version\n"; exit 0; }  # This is horrible
132$| = 1; 
133print ucfirst($myprogname) . " v$version\n\n";
134
135exit 0 if ($opt->{version});
136die "run tv_grab_au --help for details.\n" if ($opt->{help});
137
138&read_config_file;
139&read_channels_file;
140
141# ---------------------------------------------------------------------------
142# --- Go!
143# ---------------------------------------------------------------------------
144
145&calc_date_range;
146&grab_data;
147&reconcile_data;
148&postprocess_data;
149&output_data;
150&write_config_file;
151
152# ---------------------------------------------------------------------------
153# --- Subroutines
154# ---------------------------------------------------------------------------
155
156# -----------------------------------------
157# Subs: Grabbing
158# -----------------------------------------
159
160sub grab_data
161{
162    my $used_grabbers = 0;
163
164    printf "\nGrabber stage.\n";
165
166    &analyze_plugin_data("",1);   
167
168    while (my $grabber = choose_grabber())
169    {
170        $grabber_found_all_data = 0;
171        $used_grabbers++;
172
173        $components->{$grabber}->{laststatus} = "unknown";
174
175        printf "\nSHEPHERD: Using grabber: (%d) %s\n", $used_grabbers, $grabber;
176
177        my $output = "$CWD/grabbers/$grabber/output.xmltv";
178
179        my $comm = "$CWD/grabbers/$grabber/$grabber " .
180                   "--region $region " .
181                   "--output $output";
182
183        # Category 1 grabbers (i.e. slow ones) are requested to only fetch the timeslice
184        # that we need. Category 2 grabbers are requested to get everything, since there's
185        # very little cost in grabbing that extra data, and we can use it in the reconciler
186        # to verify that everything looks OK.
187        if (query_config($grabber, 'category') == 1)
188        {
189            printf "$grabber is Category 1: grabbing timeslice.\n" if ($debug);
190
191            record_requested_chandays($grabber, $timeslice);
192
193            if ($timeslice->{start} != 0)
194            {
195                $comm .= " " . 
196                         query_config($grabber, 'option_days_offset') .
197                         " " .
198                         $timeslice->{start};
199            }
200
201            my $n = $timeslice->{stop} + 1;
202            if ($timeslice->{start} != 0 
203                    and 
204                !query_config($grabber, 'option_offset_eats_days'))
205            {
206                $n -= $timeslice->{start};
207            }
208            $comm .= " " .
209                     query_config($grabber, 'option_days') .
210                     " " . 
211                     $n;
212           
213            # Write a temporary channels file specifying only the channels we want
214            my $tmpchans;
215            foreach (@{$timeslice->{chans}})
216            {
217                $tmpchans->{$_} = $channels->{$_};
218            }
219            my $tmpcf = "$CWD/channels.conf.tmp";
220            write_file($tmpcf, 'temporary channels', [ $tmpchans ], [ 'channels' ]);
221            $comm .= " --channels_file $tmpcf";
222        }
223        else
224        {
225            printf "$grabber is category 2: grabbing everything.\n" if ($debug);
226            $comm .= " --days $days" if ($days);
227            $comm .= " --offset $opt->{offset}" if ($opt->{offset});
228            $comm .= " --channels_file $channels_file";
229        }
230        $comm .= " --debug" if ($debug);
231        $comm .= " @ARGV" if (@ARGV);
232
233        my $retval = 0;
234        if ((defined $opt->{dontcallgrabbers}) && ($opt->{dontcallgrabbers})) {
235            printf "SHEPHERD: not calling grabber because of --dontcallgrabbers option, but will instead use existing $output\n";
236            printf "SHEPHERD: would have called: $comm\n" if ($debug);
237        } else {
238            printf "SHEPHERD: Excuting command: $comm\n";
239            chdir "$CWD/grabbers/$grabber/";
240            $retval = call_prog($comm);
241            chdir $CWD;
242        }
243
244        if ($retval != 0) {
245            printf "grabber returned with non-zero return code $retval: assuming it failed.\n";
246            next;
247        }
248
249        # soak up the data we just collected
250        &soak_up_data($grabber, $output, "grabber");
251        $components->{$grabber}->{laststatus} = $plugin_data->{$grabber}->{laststatus};
252        $components->{$grabber}->{lastdata} = time if ($plugin_data->{$grabber}->{valid});
253
254        # check to see if we have all the data we want
255        $grabber_found_all_data = &analyze_plugin_data("analysis of all grabbers so far");
256
257        # Record what we grabbed from cacheable C1 grabbers
258        if (query_config($grabber, 'category') == 1 and query_config($grabber, 'cache'))
259        {
260            my $missing_before = convert_dayhash_to_list($missing);
261            my $missing_after = convert_dayhash_to_list(detect_missing_data());
262            my $list = List::Compare->new($missing_before, $missing_after);
263            my @grabbed = $list->get_symmetric_difference();
264            printf "Grabbed: " . join (', ', @grabbed) . ".\n" if ($debug);
265            record_cached($grabber, @grabbed);
266            write_config_file();
267        }
268
269        last if ($grabber_found_all_data);
270    }
271
272
273    if ($used_grabbers == 0)
274    {
275        printf "No valid grabbers installed/enabled!\n";
276        return;
277    }
278
279    unless ($grabber_found_all_data)
280    {
281        printf "SHEPHERD: Ran through ALL grabbers but still missing data!!! :(\n";
282        return;
283    }
284}
285
286# -----------------------------------------
287# Subs: Intelli-random grabber selection
288# -----------------------------------------
289
290sub choose_grabber
291{
292    if (defined $gscore)        # Reset score hash
293    {
294        foreach (keys %$gscore)
295        {
296            $gscore->{$_} = 0;
297        }
298    }
299    else                        # Create score hash
300    {
301        foreach (query_grabbers())
302        {
303            unless ($components->{$_}->{disabled})
304            {
305                $gscore->{$_} = 0;
306                if (query_config($_, 'category') == 1 and query_config($_, 'cache'))
307                {
308                    $gscore->{$_ . ' [cache]'} = 0;
309                }
310            }
311        }
312    }
313
314    $missing = detect_missing_data();
315    $timeslice = find_best_timeslice();
316
317    if ($debug)
318    {
319        printf "Best timeslice: day%s of channels %s (%d chandays).\n",
320                    ($timeslice->{start} == $timeslice->{stop} ?
321                        " $timeslice->{start}" :
322                        "s $timeslice->{start} - $timeslice->{stop}"),
323                    join(', ', @{$timeslice->{chans}}),
324                    $timeslice->{chandays};
325    }
326
327    my $total = score_grabbers();
328 
329    if ($debug)
330    {
331        printf "Grabber selection:\n";
332        foreach (sort { $gscore->{$b} <=> $gscore->{$a} } keys %$gscore)
333        {
334            next if ($_ =~ /\[cache\]/);
335
336            my $score  = $gscore->{$_};
337            my $cscore = $gscore->{"$_ [cache]"};
338            my $cstr   = $cscore ? "(inc. $cscore cache pts)" : "";
339
340            if ($opt->{randomize})
341            {
342                printf "%15s %6.1f%% %9s %s\n", 
343                        $_, 
344                        ($total ? 100* $score / $total : 0), 
345                        "$score pts",
346                        $cstr;
347            }
348            else
349            {
350                printf "%15s %4s pts %s\n", 
351                        $_, 
352                        $score,
353                        $cstr;
354            }
355        }
356    }
357
358    return undef unless ($total);
359
360    # Select a grabber
361
362    # If the user has specified a pref_title_source -- i.e. he is
363    # transitioning from a known grabber -- then we make sure it
364    # has run at least once, to build the list of title translations.
365    if ($pref_title_source)
366    {
367        my @prefs = split(/,/, $pref_title_source);
368        foreach my $grabber (@prefs)
369        {
370            unless ($components->{$grabber}->{lastdata})
371            {
372                printf "Need to build title translation list for transitional grabber $grabber.\n";
373                return select_grabber($grabber, $gscore) if ($gscore->{$grabber});
374                printf "WARNING: Can't run $grabber to build title translation list!\n";
375            }
376        }
377    }
378
379    # Either do it randomly based on scores, or just return the
380    # highest-scoring grabber, depending on whether --randomize has
381    # been used.
382
383    my $r = int(rand($total));
384    my $c = 0;
385    my $best;
386
387    foreach my $grabber (keys %$gscore)
388    {
389        next if (!$gscore->{$grabber} or $grabber =~ /\[cache\]/);
390        if ($opt->{randomize})
391        {
392            if ($r >= $c and $r < ($c + $gscore->{$grabber}))
393            {
394                return select_grabber($grabber, $gscore);
395            }
396            $c += $gscore->{$grabber};
397        }
398        else
399        {
400            if (!$best or $gscore->{$grabber} > $gscore->{$best})
401            {
402                $best = $grabber;
403            }
404        }
405    }
406
407    if ($opt->{randomize} or !$best)
408    {
409        die "ERROR: failed to choose grabber.";
410    }
411    return select_grabber($best, $gscore);
412}
413
414sub select_grabber
415{
416    my ($grabber, $gscore) = @_;
417
418    printf "Selected $grabber.\n" if ($debug);
419    if (query_config($grabber, 'category') == 2)
420    {
421        # We might want to run C1 grabbers multiple times
422        # to grab various timeslices, but not C2 grabbers,
423        # which should get everything at once.
424        delete $gscore->{$grabber};
425    }
426    return $grabber;
427}
428
429# Grabbers earn 1 point for each slot or chanday they can fill.
430# This score is multiplied if the grabber:
431# * is a category 2 grabber (i.e. fast/cheap)
432# * is a category 1 grabber that has the data we want in a cache
433# * can supply high-quality data
434# Very low quality grabbers score 0 unless we need them; i.e. they're backups.
435sub score_grabbers
436{
437    my ($score, $total, $day, $catbonus, $dqbonus, $mult, $key);
438
439    my $bestdq = 0;
440
441    # Compare C2 grabbers against the raw missing file, because we'll get
442    # everything. But compare C1 grabbers against the timeslice, because we'll
443    # only ask them for a slice. This goes for the [cache] and regular C1s.
444    foreach my $grabber (keys %$gscore)
445    {
446        # for each slot, say whether we can fill it or not -- that is,
447        # whether we support this channel and this day #.
448
449        my $hits = 0;
450        my $cat = query_config($grabber, 'category');
451        my $dq = query_config($grabber, 'quality');
452
453        if ($cat == 1)
454        {
455            $key = cut_down_missing($grabber);
456            # printf "Grabber $grabber is Category 1: comparing capability to best timeslice.\n" if ($debug);
457        }
458        else
459        {
460            $key = $missing;
461            # printf "Grabber $grabber is Category 2: comparing capability to all wanted channels and days.\n" if ($debug);
462        }
463
464        if ($grabber =~ /\[cache\]/)
465        {
466            $hits = find_cache_hits($grabber, $key);
467        }
468        else
469        {
470            foreach my $day (sort keys %$key)
471            {
472                my $val = supports_day($grabber, $day);
473                next unless ($val);
474                # printf "Day $day:" if ($debug);
475                foreach my $ch (@{$key->{$day}})
476                {
477                    if (supports_channel($grabber, $ch, $day))
478                    {
479                        # printf " $ch" if ($debug);
480                        $hits += $val;
481                    }
482                }
483                # printf "\n" if $debug;
484                $hits = 1 if ($hits > 0 and $hits < 1);
485            }
486        }
487
488        my $catbonus = 1;
489        $catbonus = 3 if ($cat == 2);
490        if ($grabber =~ /\[cache\]/)
491        {
492            # Bonus is on a sliding scale between 1 and 2 depending on
493            # % of required data in cache
494            $catbonus += $hits / $timeslice->{chandays};
495        }
496
497        my $dqbonus = 2 ** ($dq-1);
498
499        my $mult = $dq ** $catbonus;
500
501        my $score = int($hits * $mult);
502
503        if ($debug)
504        {
505            my $str = sprintf "Grabber %s can supply %d chandays",
506                                $grabber, $hits;
507            if ($hits)
508            {
509                $str .= sprintf " at x%.1f (cat: %d, DQ: %d): %d pts",
510                            $mult,
511                            $cat,
512                            $dq,
513                            $score;
514            }
515            printf "$str.\n";
516        }
517
518        $gscore->{$grabber} += $score;
519        $total += $score;
520        if ($grabber =~ /\[cache\]/)
521        {
522            $gscore->{query_name($grabber)} += $score;
523        }
524
525        if ($score and $dq > $bestdq)
526        {
527            $bestdq = $dq;
528        }
529    }
530
531    # Eliminate grabbers of data quality 1 if there are any better-quality
532    # alternatives when using randomize.
533    if ($opt->{randomize})
534    {
535        foreach (keys %$gscore)
536        {
537            if ($gscore->{$_}
538                    and
539                query_config($_, 'quality') == 1
540                    and
541                $bestdq > 1)
542            {
543                $total -= $gscore->{$_};
544                $gscore->{$_} = 0;
545                printf "Zeroing grabber $_ due to low data quality.\n" if ($debug);
546            }
547        }
548    }
549
550    return $total;
551}
552
553# Return 1 if the grabber can provide data for this channel, else 0.
554sub supports_channel
555{
556    my ($grabber, $ch, $day) = @_;
557
558    my $mdpc = query_config($grabber, 'max_days_per_chan');
559    if ($mdpc)
560    {
561        if ($mdpc->{$ch})
562        {
563            return ($mdpc->{$ch} > $day);
564        }
565    }
566
567    my $channels_supported = query_config($grabber, 'channels');
568    unless (defined $channels_supported)
569    {
570        printf "WARNING: Grabber $grabber has no channel support " .
571              "specified in config.\n";
572        $channels_supported = '';
573    }
574
575    return 1 unless ($channels_supported); # Empty string means we support all
576   
577    $ch =~ s/ /_/g;
578    my $match = ($channels_supported =~ /\b$ch\b/);
579    my $exceptions = ($channels_supported =~/^-/);
580    return ($match != $exceptions);
581}
582
583# Return 0 if the grabber can't provide data for this day,
584# 1 if it can reliably, and 0.5 if it can unreliably.
585#
586# Note that a max_days of 7 means the grabber can retrieve data for
587# today plus 6 days.
588sub supports_day
589{
590    my ($grabber, $day) = @_;
591
592    return 0 unless ($day < query_config($grabber, 'max_days'));
593    return 0.5 if ($day >= query_config($grabber, 'max_reliable_days'));
594    return 1;
595}
596
597sub find_cache_hits
598{
599    my ($grabber, $key) = @_;
600
601    $grabber = query_name($grabber);
602
603    return 0 unless ($components->{$grabber}->{cached});
604
605    my $hits = 0;
606
607    foreach my $day (keys %$key)
608    {
609        my $date = substr(DateCalc("today", "+ $day days"), 0, 8);
610        foreach my $ch (@{$key->{$day}})
611        {
612            $hits++ if (grep(/^$date:$ch$/, @{$components->{$grabber}->{cached}}));
613        }
614    }
615    return $hits;
616}
617
618# Build a dayhash of what channel/day data we're currently missing.
619# I think granularity of one day is good for now; could possibly be
620# made more fine-grained if we think grabbers will support that.
621sub detect_missing_data
622{
623    my $m = { };
624
625    my $chandays = 0;
626    foreach my $ch (keys %$channels)
627    {
628        # is this channel missing too much data?
629        unless ($channel_data->{$ch}->{analysis}->{data_ok}) {
630            # not ok - record which days are bad
631            foreach my $day (@{($channel_data->{$ch}->{analysis}->{day})}) {
632                push(@{($m->{($day->{num})})}, $ch) unless ($day->{day_ok});
633            }
634        }
635    }
636
637    foreach my $day (keys %$m)
638    {
639        $m->{$day} = [ sort @{$m->{$day}} ];
640        $chandays += scalar(@{$m->{$day}}) if ($debug);
641    }
642
643    if ($debug)
644    {
645        printf "Need data for days " . join(", ", sort keys %$m) . 
646             " ($chandays chandays).\n";
647    }
648    return $m;
649}
650
651# Find the largest timeslice in the current $missing dayhash; i.e.
652# something like "Days 4 - 6 of ABC and SBS." This works by iterating
653# through the days and looking for overlaps where consecutive days
654# want the same channels.
655sub find_best_timeslice
656{
657    my ($overlap, $a);
658    my $slice = { 'chandays' => 0 };
659
660    foreach my $day (0 .. $days-1)
661    {
662        consider_slice($slice, $day, $day, @{$missing->{$day}});
663        $overlap = $missing->{$day};
664        foreach my $nextday (($day + 1) .. $days-1)
665        {
666            last unless ($missing->{$nextday});
667            $a = Algorithm::Diff::LCS($overlap, $missing->{$nextday});
668            last unless ($a and @{$a});
669            consider_slice($slice, $day, $nextday, @{$a});
670            $overlap = $a;
671        }
672    }
673    return $slice;
674}
675
676sub consider_slice
677{
678    my ($slice, $startday, $stopday, @chans) = @_;
679
680    my $challenger = ($stopday - $startday + 1) * scalar(@chans);
681    return unless ($challenger > $slice->{chandays});
682
683    # We have a winner!
684    $slice->{start} = $startday;
685    $slice->{stop} = $stopday;
686    $slice->{chans} = [ @chans ];
687    $slice->{chandays} = $challenger;
688}
689
690# Record what a cacheable C1 grabber has just retrieved for us,
691# so we know next time that this data can be grabbed quickly.
692sub record_cached
693{
694    my ($grabber, @grabbed) = @_;
695
696    printf "Recording cache for grabber $grabber.\n" if ($debug);
697
698    my $gcache = $components->{$grabber}->{cached};
699    $gcache = [ ] unless ($gcache);
700    my @newcache;
701    my $today = strftime("%Y%m%d", localtime);
702
703    # remove old chandays
704    foreach my $chanday (@$gcache)
705    {
706        $chanday =~ /(\d+):(.*)/;
707        if ($1 >= $today)
708        {
709            push (@newcache, $chanday);
710        }
711    }
712
713    # record new chandays
714    foreach my $chanday (@grabbed)
715    {
716        push (@newcache, $chanday) unless (grep(/^$chanday$/, @newcache));
717    }
718    $components->{$grabber}->{cached} = [ @newcache ];
719}
720
721# Takes a dayhash and returns it as a list like this:
722# ( "20061018:ABC", "20061018:Seven", ... )
723sub convert_dayhash_to_list
724{
725    my $h = shift;
726
727    my @ret;
728    foreach my $day (keys %$h)
729    {
730        my $date = substr(DateCalc("today", "+ $day days"), 0, 8);
731        foreach my $ch (@{$h->{$day}})
732        {
733            push (@ret, "$date:$ch");
734        }
735    }
736    @ret = sort @ret;
737    return \@ret;
738}
739
740# If we're about to re-try a grabber, make sure that we're not asking
741# it for the same data. That is, prevent a broken C1 grabber causing
742# an infinite loop.
743sub record_requested_chandays
744{
745    my ($grabber, $slice) = @_;
746
747    printf "Recording timeslice request; will not request these chandays " .
748         "from $grabber again.\n" if ($debug);
749
750    my @requested;
751    for my $day ($slice->{start} .. $slice->{stop})
752    {
753        foreach my $ch (@{$slice->{chans}})
754        {
755            push @requested, "$day:$ch";
756        }
757    }
758    if ($grabbed->{$grabber})
759    {
760        push @{$grabbed->{$grabber}}, @requested;
761    }
762    else
763    {
764        $grabbed->{$grabber} = [ @requested ];
765    }
766}
767
768# If this grabber has been called previously, remove those chandays
769# from the current request -- we don't want to ask it over and over
770# for a timeslice that it has already failed to provide.
771sub cut_down_missing
772{
773    my $grabber = shift;
774
775    $grabber = query_name($grabber);
776    my $dayhash = {};
777
778    # Take the timeslice and expand it to a dayhash, while pruning
779    # any chandays that have previously been requested from this
780    # grabber.
781    foreach my $day ($timeslice->{start} .. $timeslice->{stop})
782    {
783        my @chans;
784        foreach my $ch (@{$timeslice->{chans}})
785        {
786            unless ($grabbed->{$grabber} and grep(/$day:$ch/, @{$grabbed->{$grabber}}))
787            {
788                push (@chans, $ch)
789            }
790        }
791        $dayhash->{$day} = [ @chans ] if (@chans);
792    }
793
794    return $dayhash;
795}
796
797# -----------------------------------------
798# Subs: Analyzing data
799# -----------------------------------------
800
801# interpret xmltv data from this grabber/postprocessor
802sub soak_up_data
803{
804    my ($plugin, $output, $plugintype) = @_;
805
806    if (! -r $output) {
807        printf "SHEPHERD: Warning: plugin '%s' output file '%s' does not exist\n",$plugin,$output;
808        return;
809    }
810
811    my $this_plugin = $plugin_data->{$plugin};
812    printf "SHEPHERD: Started parsing XMLTV from '%s' in '%s' .. any errors below are from parser:\n",$plugin,$output;
813    eval { $this_plugin->{xmltv} = XMLTV::parsefiles($output); };
814    printf "SHEPHERD: Completed XMLTV parsing from '%s'\n",$plugin;
815
816    if (!($this_plugin->{xmltv})) {
817        printf "WARNING: Plugin $plugin didn't seem to return any valid XMLTV!\n";
818        return;
819    }
820
821    $this_plugin->{valid} = 1;
822    $this_plugin->{output_filename} = $output;
823    $components_used .= " + ".$plugin."(v".$components->{$plugin}->{ver}.")";
824
825    my $xmltv = $this_plugin->{xmltv};
826    my ($encoding, $credits, $chan, $progs) = @$xmltv;
827    $this_plugin->{total_duration} = 0;
828    $this_plugin->{programmes} = 0;
829    $this_plugin->{progs_with_invalid_date} = 0;        # explicitly track unparsable dates
830    $this_plugin->{progs_with_unknown_channel} = 0;     # explicitly track unknown channels
831
832    my $seen_channels_with_data = 0;
833
834    #
835    # first iterate through all programmes and see if there are any channels we don't know about
836    #
837    my %chan_xml_list;
838    foreach my $ch (sort keys %{$channels}) {
839        $chan_xml_list{($channels->{$ch})} = 1;
840    }
841    foreach my $prog (@$progs) {
842        if (!defined $chan_xml_list{($prog->{channel})}) {
843            $this_plugin->{progs_with_unknown_channel}++;
844            printf " - WARNING: plugin '%s' returned data for unknown channel '%s': ignored.\n",$plugin,$prog->{channel};
845            $chan_xml_list{($prog->{channel})} = 1;     # so we warn only once
846        }
847    }
848       
849    # iterate thru channels
850    foreach my $ch (sort keys %{$channels}) {
851        my $seen_progs_on_this_channel = 0;
852
853        # iterate thru programmes per channel
854        foreach my $prog (@$progs) {
855            next if ($prog->{channel} ne $channels->{$ch});
856
857            my $t1 = &parse_xmltv_date($prog->{start});
858            my $t2 = &parse_xmltv_date($prog->{stop});
859
860            if (!$t1 || !$t2) {
861                printf " - WARNING: plugin '%s' returned programme data with invalid timestamp format: \"%s\": can't parse.\n",
862                    $plugin,(!$t1 ? $prog->{start} : $prog->{stop}) if (!$this_plugin->{progs_with_invalid_date});
863                $this_plugin->{progs_with_invalid_date}++;
864                next;
865            }
866
867            # store plugin-specific stats
868            $this_plugin->{programmes}++;
869            $this_plugin->{total_duration} += ($t2 - $t1);
870            $seen_progs_on_this_channel++;
871            $this_plugin->{earliest_data_seen} = $t1 if (!defined $this_plugin->{earliest_data_seen});
872            $this_plugin->{earliest_data_seen} = $t1 if ($t1 < $this_plugin->{earliest_data_seen});
873            $this_plugin->{latest_data_seen} = $t2 if (!defined $this_plugin->{latest_data_seen});
874            $this_plugin->{latest_data_seen} = $t2 if ($t2 > $this_plugin->{latest_data_seen});
875
876            # store channel-specific stats
877            $channel_data->{$ch}->{programmes}++;
878            $channel_data->{$ch}->{total_duration} += ($t2 - $t1);
879
880            # programme is outside the timeslots we are interested in.
881            next if ($t1 > $policy{endtime});
882            next if ($t2 < $policy{starttime});
883
884            # store timeslot info
885            my $start_slotnum = 0;
886            $start_slotnum = int(($t1 - $policy{starttime}) / $policy{timeslot_size})
887                if ($t1 >= $policy{starttime});
888
889            my $end_slotnum = ($policy{num_timeslots}-1);
890            $end_slotnum = int(($t2 - $policy{starttime}) / $policy{timeslot_size})
891                if ($t2 < $policy{endtime});
892
893            # add this programme into the global timeslots table for this channel
894            foreach my $slotnum ($start_slotnum..$end_slotnum) {
895                $channel_data->{$ch}->{timeslots}[$slotnum]++;
896            }
897        }
898
899        $seen_channels_with_data++ if ($seen_progs_on_this_channel > 0);
900    }
901
902    # print some stats about what we saw!
903    printf "SHEPHERD: %s '%s' returned data for %d channels, %d programmes, %dd%02dh%02dm%02ds duration, %s%s\n",
904        ucfirst($plugintype), $plugin, $seen_channels_with_data, $this_plugin->{programmes},
905        int($this_plugin->{total_duration} / 86400),            # days
906        int(($this_plugin->{total_duration} % 86400) / 3600),   # hours
907        int(($this_plugin->{total_duration} % 3600) / 60),      # mins
908        int($this_plugin->{total_duration} % 60),               # sec
909        (defined $this_plugin->{earliest_data_seen} ? (strftime "%a %e %b %H:%M - ", localtime($this_plugin->{earliest_data_seen})) : 'no data'),
910        (defined $this_plugin->{latest_data_seen} ? (strftime "%a %e %b %H:%M", localtime($this_plugin->{latest_data_seen})) : '');
911
912    $this_plugin->{laststatus} = sprintf "%dch/%dpr/%dhrs %s-%s",
913        $seen_channels_with_data, $this_plugin->{programmes},
914        int($this_plugin->{total_duration} / 3600),
915        (defined $this_plugin->{earliest_data_seen} ? (strftime "%a%d%b", localtime($this_plugin->{earliest_data_seen})) : 'no'),
916        (defined $this_plugin->{latest_data_seen} ? (strftime "%a%d%b", localtime($this_plugin->{latest_data_seen})) : 'data');
917
918    $plugin_data->{$plugin} = $this_plugin;
919}
920
921
922# analyze grabber data - do we have all the data we want?
923sub analyze_plugin_data
924{
925    my ($analysistype,$quiet) = @_;
926    printf "SHEPHERD: $analysistype:\n" unless $quiet;
927
928    my $total_channels = 0;
929
930    my $overall_data_ok = 1; # until proven otherwise
931
932    # iterate across each channel
933    foreach my $ch (sort keys %{$channels}) {
934        $total_channels++;
935
936        my $data;
937        my $lastpol = "";
938        $data->{data_ok} = 1; # unless proven otherwise
939        $data->{have} = 0;
940        $data->{missing} = 0;
941
942        for my $slotnum (0..($policy{num_timeslots}-1)) {
943            my $bucket_start_offset = ($slotnum * $policy{timeslot_size});
944
945            # work out day number of when this bucket is.
946            # number from 0 onwards.  (i.e. today=0).
947            # for a typical 7 day grabber this will actually mean 8 days of data (0-7)
948            # with days 0 and 7 truncated to half-days
949            my $day = int(($bucket_start_offset + $policy{first_bucket_offset}) / 86400);
950
951            if (!defined $data->{day}->[$day]) {
952                $data->{day}->[$day]->{num} = $day;
953                $data->{day}->[$day]->{have} = 0;
954                $data->{day}->[$day]->{missing} = 0;
955                $data->{day}->[$day]->{missing_peak} = 0;
956                $data->{day}->[$day]->{missing_nonpeak} = 0;
957                $data->{day}->[$day]->{missing_other} = 0;
958
959                $data->{day}->[$day]->{day_ok} = 1; # until proven otherwise
960
961                # day changed, dump any 'already_missing' data
962                &dump_already_missing($data);
963            }
964
965            # we have programming data for this bucket.  great!  process next bucket
966            if ((defined $channel_data->{$ch}->{timeslots}[$slotnum]) &&
967                ($channel_data->{$ch}->{timeslots}[$slotnum] > 0)) {
968
969                # if we have missing data queued up, push it now
970                &dump_already_missing($data);
971
972                &dump_already_missing_period($data->{day}->[$day],$lastpol)
973                  if ($lastpol ne "");
974
975                $data->{day}->[$day]->{have} += $policy{timeslot_size};
976                $data->{have} += $policy{timeslot_size};
977                next;
978            }
979
980            # we don't have programming for this channel for this bucket
981
982            # some grabbers take HOURS to run. if this bucket (missing data) is for
983            # a time period now in the past, then don't include it
984            next if (($bucket_start_offset + $policy{starttime}) < time);
985
986            # work out the localtime of when this bucket is
987            my $bucket_seconds_offset = ($bucket_start_offset + $policy{first_bucket_offset}) % 86400;
988
989            # store details of where we are missing data
990            if (!defined $data->{already_missing}) {
991                $data->{already_missing} = sprintf "#%d/%02d:%02d",
992                  $day,
993                  int($bucket_seconds_offset / 3600),
994                  int(($bucket_seconds_offset % 3600) / 60);
995            }
996            $data->{already_missing_last} = $bucket_seconds_offset + $policy{timeslot_size} - 1;
997
998            $data->{day}->[$day]->{missing} += $policy{timeslot_size};
999            $data->{missing} += $policy{timeslot_size};
1000
1001            # work out what policy missing data for this bucket fits into
1002            my $pol;
1003            if (($bucket_seconds_offset >= $policy{peak_start}) &&
1004                (($bucket_seconds_offset+$policy{timeslot_size}) <= $policy{peak_stop})) {
1005                $pol = "peak";
1006            } elsif (($bucket_seconds_offset >= $policy{nonpeak_start}) &&
1007                     (($bucket_seconds_offset+$policy{timeslot_size}) <= $policy{nonpeak_stop})) {
1008                $pol = "nonpeak";
1009            } else {
1010                $pol = "other";
1011            }
1012
1013            &dump_already_missing_period($data->{day}->[$day],$lastpol)
1014              if (($lastpol ne $pol) && ($lastpol ne ""));
1015
1016            $lastpol = $pol;
1017
1018            $data->{day}->[$day]->{"missing_".$pol} += $policy{timeslot_size};
1019
1020            $data->{day}->[$day]->{"already_missing_".$pol."_start"} = $bucket_seconds_offset
1021              if (!defined $data->{day}->[$day]->{"already_missing_".$pol."_start"});
1022            $data->{day}->[$day]->{"already_missing_".$pol."_stop"} = $bucket_seconds_offset + $policy{timeslot_size} - 1;
1023
1024            $data->{day}->[$day]->{day_ok} = 0 if ($data->{day}->[$day]->{missing_peak} > $policy{peak_max_missing});
1025            $data->{day}->[$day]->{day_ok} = 0 if ($data->{day}->[$day]->{missing_nonpeak} > $policy{nonpeak_max_missing});
1026            $data->{day}->[$day]->{day_ok} = 0 if ($data->{day}->[$day]->{missing_other} > $policy{other_max_missing});
1027            $data->{data_ok} = 0 if ($data->{day}->[$day]->{day_ok} == 0);
1028            $overall_data_ok = 0 if ($data->{data_ok} == 0);
1029        }
1030
1031        # finished all timeslots in this channel.
1032        # if we have missing data queued up, push it now
1033        &dump_already_missing($data);
1034
1035        # fill in any last missing period data
1036        foreach my $day (@{($data->{day})}) {
1037            &dump_already_missing_period($day,"peak");
1038            &dump_already_missing_period($day,"nonpeak");
1039            &dump_already_missing_period($day,"other");
1040        }
1041
1042        my $statusstring = sprintf " > ch %s: %s programming: %s\n", 
1043          $ch, pretty_duration($data->{have}),
1044          $data->{data_ok} ? "PASS (within thresholds)" : "FAIL, missing data over policy threshold:";
1045
1046        # display per-day missing data statistics
1047        foreach my $day (@{($data->{day})}) {
1048            unless ($day->{day_ok}) {
1049                $statusstring .= sprintf "\t".(strftime("%a %e %b",localtime($policy{starttime}+($day->{num}*86400)))).": ";
1050
1051                # do we have any data for this day?
1052                $statusstring .= "peak ".join(", ",(@{($day->{missing_peak_table})}))
1053                  if (($day->{missing_peak}) && ($day->{missing_peak} > $policy{peak_max_missing}));
1054
1055                $statusstring .= sprintf "%snon-peak %s",
1056                  ($day->{missing_peak} ? " / " : ""),
1057                  join(", ",(@{($day->{missing_nonpeak_table})}))
1058                  if (($day->{missing_nonpeak}) && ($day->{missing_nonpeak} > $policy{nonpeak_max_missing}));
1059
1060                $statusstring .= sprintf "%sother %s",
1061                  (($day->{missing_peak} + $day->{missing_nonpeak}) > 0 ? " / " : ""),
1062                  join(", ",(@{($day->{missing_other_table})}))
1063                  if (($day->{missing_other}) && ($day->{missing_other} > $policy{other_max_missing}));
1064
1065                $statusstring .= "\n";
1066            }
1067        }
1068        printf $statusstring unless $quiet;
1069
1070        delete $channel_data->{$ch}->{analysis} if (defined $channel_data->{$ch}->{analysis});
1071        $channel_data->{$ch}->{analysis} = $data;
1072    }
1073
1074    printf " > OVERALL: %s\n", ($overall_data_ok ? "PASS" : "FAIL") unless $quiet;
1075
1076    return $overall_data_ok; # return 1 for good, 0 for need more
1077}
1078
1079# helper routine for filling in 'missing_all' array
1080sub dump_already_missing
1081{
1082    my $d = shift;
1083    if (defined $d->{already_missing}) {
1084        $d->{already_missing} .= sprintf "-%02d:%02d",
1085          int($d->{already_missing_last} / 3600),
1086          int(($d->{already_missing_last} % 3600) / 60)
1087          if (defined $d->{already_missing_last});
1088        push(@{($d->{missing_all})}, $d->{already_missing});
1089        delete $d->{already_missing};
1090        delete $d->{already_missing_last};
1091    }
1092}
1093
1094# helper routine for filling in per-day missing data
1095# specific to peak/nonpeak/other
1096sub dump_already_missing_period
1097{
1098    my ($d,$p) = @_;
1099    my $startvar = "already_missing_".$p."_start";
1100    my $stopvar = "already_missing_".$p."_stop";
1101
1102    if (defined $d->{$startvar}) {
1103        push(@{($d->{"missing_".$p."_table"})},
1104          sprintf "%02d:%02d-%02d:%02d",
1105            int($d->{$startvar} / 3600),
1106            int(($d->{$startvar} % 3600) / 60),
1107            int($d->{$stopvar} / 3600),
1108            int(($d->{$stopvar} % 3600) / 60));
1109        delete $d->{$startvar};
1110        delete $d->{$stopvar};
1111    }
1112}
1113
1114# given a duration (seconds), return it in a pretty "{days}d{hr}h{min}m" string
1115# and indication of whether the duration is over its threshold or not
1116sub pretty_duration
1117{
1118    my ($d,$crit) = @_;
1119    my $s = "";
1120    $s .= sprintf "%dd",int($d / (60*60*24)) if ($d >= (60*60*24));
1121    $s .= sprintf "%dh",int(($d % (60*60*24)) / (60*60)) if (($d % (60*60*24)) >= (60*60));
1122    $s .= sprintf "%dm",int(($d % (60*60)) / 60) if (($d % (60*60)) >= 60);
1123    $s .= "no" if ($s eq "");
1124
1125    if (defined $crit) {
1126        $s .= "[!]" if ($d > $crit);
1127    }
1128    return $s;
1129}
1130
1131# work out date range we are expecting data to be in
1132sub calc_date_range
1133{
1134    # work out GMT offset - we only do this once
1135    if (!$gmt_offset) {
1136        # work out our gmt offset
1137        my @l = localtime(43200), my @g = gmtime(43200);
1138        $gmt_offset = (($l[2] - $g[2])*(60*60)) + (($l[1] - $g[1])*60);
1139    }
1140
1141    $policy{starttime} = time;
1142
1143    # set endtime as per $days less 1 day + hours left today
1144    $policy{endtime} = $policy{starttime} + ((60*60*24)*($days-1)) + (86400 - (($policy{starttime} + $gmt_offset) % 86400));
1145
1146    # normalize starttime to beginning of next bucket
1147    $policy{starttime} += ($policy{timeslot_size} - ($policy{starttime} % $policy{timeslot_size}));
1148
1149    # work out how many seconds into a day our first bucket starts
1150    $policy{first_bucket_offset} = ($policy{starttime} + $gmt_offset) % 86400;
1151
1152    # normalize endtime to end of previous bucket
1153    $policy{endtime} -= ($policy{endtime} % $policy{timeslot_size});
1154
1155    # if we are working with an --offset, apply it now.
1156    $policy{starttime} += (86400 * $opt->{offset}) if ($opt->{offset});
1157
1158    # work out number of buckets
1159    $policy{num_timeslots} = ($policy{endtime} - $policy{starttime}) / $policy{timeslot_size};
1160}
1161
1162
1163# strptime type date parsing - BUT - if no timezone is present, treat time as being in localtime
1164# rather than the various other perl implementation which treat it as being in UTC/GMT
1165sub parse_xmltv_date
1166{
1167    my $datestring = shift;
1168    my @t; # 0=sec,1=min,2=hour,3=day,4=month,5=year,6=wday,7=yday,8=isdst
1169    my $tz_offset = 0;
1170
1171    if ($datestring =~ /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})/) {
1172        ($t[5],$t[4],$t[3],$t[2],$t[1],$t[0]) = (int($1)-1900,int($2)-1,int($3),int($4),int($5),0);
1173        ($t[6],$t[7],$t[8]) = (-1,-1,-1);
1174
1175        # if input data has a timezone offset, then offset by that
1176        if ($datestring =~ /\+(\d{2})(\d{2})/) {
1177            $tz_offset = $gmt_offset - (($1*(60*60)) + ($2*60));
1178        } elsif ($datestring =~ /\-(\d{2})(\d{2})/) {
1179            $tz_offset = $gmt_offset + (($1*(60*60)) + ($2*60));
1180        }
1181
1182        my $e = mktime(@t);
1183        return ($e+$tz_offset) if ($e > 1);
1184    }
1185    return undef;
1186}
1187
1188# -----------------------------------------
1189# Subs: Reconciling data
1190# -----------------------------------------
1191
1192# for all the data we have, try to pick the best bits!
1193sub reconcile_data
1194{
1195    printf "\nReconciling data:\n\n";
1196
1197    my $num_grabbers = 0;
1198    my $input_files = "";
1199    my @input_file_list;
1200
1201    printf "Preferred title preferences from '$pref_title_source'\n"
1202        if ((defined $pref_title_source) &&
1203            ($plugin_data->{$pref_title_source}) &&
1204            ($plugin_data->{$pref_title_source}->{valid}));
1205
1206    printf "Preference for whose data we prefer as follows:\n";
1207    foreach my $proggy (sort { $components->{$b}->{config}->{quality} <=> $components->{$a}->{config}->{quality} } query_grabbers()) {
1208        if ((!$components->{$proggy}->{disabled}) && ($plugin_data->{$proggy}) && ($plugin_data->{$proggy}->{valid})) {
1209            $num_grabbers++;
1210            printf "  %d. %s (%s)\n", $num_grabbers, $proggy, $plugin_data->{$proggy}->{output_filename};
1211
1212            $input_files .= $plugin_data->{$proggy}->{output_filename}." ";
1213            push(@input_file_list,$plugin_data->{$proggy}->{output_filename});
1214        }
1215    }
1216
1217    if ($num_grabbers == 0) {
1218        die "Nothing to reconcile!  There is no valid grabber data!\n";
1219    }
1220
1221    foreach my $reconciler (sort { $components->{$a} <=> $components->{$b} } query_reconcilers()) {
1222        next if ($components->{$reconciler}->{disabled});
1223        next if (!$components->{$reconciler}->{ready});
1224
1225        $reconciler_found_all_data = &call_data_processor("reconciler",$reconciler,$input_files);
1226
1227        if ((!$reconciler_found_all_data) && ($grabber_found_all_data)) {
1228            # urgh.  this reconciler did a bad bad thing ...
1229            printf "SHEPHERD: XML data from reconciler $reconciler appears bogus, will try to use another reconciler\n";
1230        } else {
1231            printf "SHEPHERD: Data from reconciler $reconciler looks good\n";
1232            $input_postprocess_file = $plugin_data->{$reconciler}->{output_filename};
1233        }
1234
1235        last if ($input_postprocess_file ne "");
1236    }
1237
1238    if ($input_postprocess_file eq "") {
1239        # no reconcilers worked!!
1240        printf "SHEPHERD: WARNING: No reconcilers seemed to work!  Falling back to concatenating the data together!\n";
1241
1242        my %w_args = ();
1243        $input_postprocess_file = "$CWD/input_preprocess.xmltv";
1244        my $fh = new IO::File ">$input_postprocess_file" || die "could not open $input_postprocess_file for writing: $!\n";
1245        %w_args = (OUTPUT => $fh);
1246        XMLTV::catfiles(\%w_args, @input_file_list);
1247    }
1248}
1249
1250
1251# -----------------------------------------
1252# Subs: Postprocessing
1253# -----------------------------------------
1254
1255sub postprocess_data
1256{
1257    # for our first postprocessor, we feed it ALL of the XMLTV files we have
1258    # as each postprocessor runs, we feed in the output from the previous one
1259    # Shepherd checks the "completeness" of the data that comes out of a postprocessor & automatically
1260    # reverts back to the previous postprocessor if it was shown to be bad
1261
1262    # first time around: feed in reconciled data ($input_postprocess_file)
1263
1264    printf "\nPostprocessing stage:\n";
1265
1266    foreach my $postprocessor (sort { $components->{$a} <=> $components->{$b} } query_postprocessors()) {
1267        next if ($components->{$postprocessor}->{disabled});
1268        next if (!$components->{$postprocessor}->{ready});
1269
1270        my $found_all_data = call_data_processor("postprocessor",$postprocessor,$input_postprocess_file);
1271
1272        if ($found_all_data) {
1273            # accept what this postprocessor did to our output ...
1274            printf "SHEPHERD: accepting output from postprocessor $postprocessor, feeding it into next stage\n";
1275            $input_postprocess_file = $plugin_data->{$postprocessor}->{output_filename};
1276            delete $components->{$postprocessor}->{conescutive_failures} if (defined $components->{$postprocessor}->{conescutive_failures});
1277            next;
1278        }
1279
1280        # urgh.  this postprocessor did a bad bad thing ...
1281        printf "SHEPHERD: XML data from postprocessor $postprocessor rejected, using XML from previous stage\n";
1282
1283        if (defined $components->{$postprocessor}->{conescutive_failures}) {
1284            $components->{$postprocessor}->{conescutive_failures}++;
1285        } else {
1286            $components->{$postprocessor}->{conescutive_failures} = 1;
1287        }
1288        printf "SHEPHERD: Postprocessor \"%s\" has now failed %d times in a row.  %d more and it will be automatically disabled.\n",
1289            $postprocessor,
1290            $components->{$postprocessor}->{conescutive_failures},
1291            ($policy{postprocessor_disable_failure_threshold} - $components->{$postprocessor}->{conescutive_failures});
1292
1293        if ($components->{$postprocessor}->{conescutive_failures} >= $policy{postprocessor_disable_failure_threshold}) {
1294            printf "SHEPHERD: Disabling Postprocessor \"$postprocessor\".\n";
1295            $components->{$postprocessor}->{disabled} = 1;
1296        }
1297    }
1298}
1299
1300
1301# -----------------------------------------
1302# Subs: Postprocessing/Reconciler helpers
1303# -----------------------------------------
1304
1305sub call_data_processor
1306{
1307    my ($data_processor_type, $data_processor_name, $input_files) = @_;
1308
1309    $components->{$data_processor_name}->{lastdata} = time;
1310    $components->{$data_processor_name}->{laststatus} = "unknown";
1311
1312    printf "\nSHEPHERD: Using %s: %s\n",$data_processor_type,$data_processor_name;
1313
1314    my $output = sprintf "%s/%ss/%s/output.xmltv",$CWD,$data_processor_type,$data_processor_name;
1315    my $comm = sprintf "%s/%ss/%s/%s",$CWD,$data_processor_type,$data_processor_name,$data_processor_name;
1316    $comm .= " --region $region" .
1317             " --channels_file $channels_file" .
1318             " --output $output";
1319    $comm .= " --days $days" if ($days);
1320    $comm .= " --offset $opt->{offset}" if ($opt->{offset});
1321    $comm .= " --debug" if ($debug);
1322    $comm .= " @ARGV" if (@ARGV);
1323
1324    $comm .= " --preftitle ".$plugin_data->{$pref_title_source}->{output_filename}
1325      if (($data_processor_type eq "reconciler") &&
1326          (defined $pref_title_source) &&
1327          ($plugin_data->{$pref_title_source}) &&
1328          ($plugin_data->{$pref_title_source}->{valid}));
1329
1330    $comm .= " $input_files";
1331    printf "SHEPHERD: Excuting command: $comm\n";
1332
1333    my $dir = sprintf "%s/%ss/%s/",$CWD,$data_processor_type,$data_processor_name;
1334    chdir $dir;
1335    my $retval = call_prog($comm);
1336    chdir $CWD;
1337
1338    if ($retval != 0) {
1339        printf "$data_processor_type returned with non-zero return code $retval: assuming it failed.\n";
1340        return 0;
1341    }
1342
1343    #
1344    # soak up the data we just collected and check it
1345    # YES - these are the SAME routines we used in the previous 'grabber' phase
1346    # but the difference here is that we clear out our 'channel_data' beforehand
1347    # so we can independently analyze the impact of this postprocessor.
1348    # if it clearly returns bad data, don't use that data (go back one step) and
1349    # flag the postprocessor as having failed.  after 3 consecutive failures, disable it
1350    #
1351
1352    # clear out channel_data
1353    foreach my $ch (keys %{$channels}) {
1354        delete $channel_data->{$ch};
1355    }
1356
1357    # process and analyze it!
1358    &soak_up_data($data_processor_name, $output, $data_processor_type);
1359    my $have_all_data = &analyze_plugin_data("$data_processor_type $data_processor_name");
1360
1361    $components->{$data_processor_name}->{laststatus} = $plugin_data->{$data_processor_name}->{laststatus};
1362
1363    return $have_all_data;
1364}
1365
1366
1367sub output_data
1368{
1369    # $input_postprocess_file contains our final output
1370    # send it to whereever --output told us to!
1371
1372    my $output_filename = "$CWD/output.xmltv";
1373    $output_filename = $opt->{output} if ($opt->{output});
1374
1375    my %writer_args = ( encoding => 'ISO-8859-1' );
1376    my $fh = new IO::File(">$output_filename") ||
1377      die "can't open $output_filename for writing: $!";
1378    $writer_args{OUTPUT} = $fh;
1379
1380    $writer = new XMLTV::Writer(%writer_args);
1381    $writer->start( {
1382        'source-info-name' => "$progname v".$components->{$progname}->{ver},
1383        'generator-info-name' =>
1384          $progname."(v".$components->{$progname}->{ver}.") + ".
1385          $myprogname."(v".$version.")".$components_used} );
1386
1387    XMLTV::parsefiles_callback(undef, undef, \&output_data_channel_cb, 
1388        \&output_data_programme_cb, $input_postprocess_file);
1389    $writer->end();
1390
1391    printf "Final output stored in $output_filename.\n";
1392}
1393
1394sub output_data_channel_cb
1395{
1396    my $c = shift;
1397    $writer->write_channel($c);
1398}
1399
1400sub output_data_programme_cb
1401{
1402    my $prog=shift;
1403    $writer->write_programme($prog);
1404}
1405
1406# -----------------------------------------
1407# Subs: Utilities
1408# -----------------------------------------
1409#
1410
1411sub query_grabbers
1412{
1413    my ($conf, $val) = @_;
1414    return query_component_type('grabber',$conf,$val);
1415}
1416
1417sub query_reconcilers
1418{
1419    return query_component_type('reconciler');
1420}
1421
1422sub query_postprocessors
1423{
1424    return query_component_type('postprocessor');
1425}
1426
1427sub query_component_type
1428{
1429    my ($progtype,$conf,$val) = @_;
1430
1431    my @ret = ();
1432    foreach (keys %$components)
1433    {
1434        if ($components->{$_}->{type} eq $progtype) {
1435            if (defined $conf) {
1436                push (@ret, $_) if (query_config($_,$conf) eq $val);
1437            } else {
1438                push (@ret, $_);
1439            }
1440        }
1441    }
1442    return @ret;
1443}
1444
1445sub query_name
1446{
1447    my $str = shift;
1448    if ($str =~ /(.*) \[cache\]/)
1449    {
1450        return $1;
1451    }
1452    return $str;
1453}
1454
1455sub query_config
1456{
1457    my ($grabber, $key) = @_;
1458
1459    $grabber = query_name($grabber);
1460    return undef unless ($components->{$grabber});
1461    return $components->{$grabber}->{config}->{$key};
1462}
1463
1464sub call_prog
1465{
1466    my $prog = shift;
1467    if (!(open(PROG,"$prog|"))) {
1468        printf "warning: couldn't exec \"$prog\": $!\n";
1469        return -1;
1470    }
1471    while(<PROG>) {
1472        print $_;
1473    }
1474    close(PROG);
1475
1476    if ($? == -1) {
1477        printf "Failed to execute prog: $!\n";
1478        return -1;
1479    } elsif ($? & 127) {
1480        printf "prog died with signal %d, %s coredump\n",
1481          ($? & 127),  (($? & 128) ? "with" : "without");
1482        return $?;
1483    } else {
1484        printf "prog exited with value %d\n", ($? >> 8) if ($debug or $?);
1485        return ($? >> 8);
1486    }
1487}
1488
1489# -----------------------------------------
1490# Subs: Setup
1491# -----------------------------------------
1492
1493sub read_config_file
1494{
1495    read_file($config_file, 'configuration');
1496}
1497
1498sub read_channels_file
1499{
1500    read_file($channels_file, 'channels');
1501}
1502
1503sub read_file
1504{
1505    my $fn = shift;
1506    my $name = shift;
1507
1508    print "Reading $name file: $fn\n";
1509    unless (-r $fn)
1510    {
1511        unless ($opt->{configure})
1512        {
1513            print "\nNo $name file found.\n" .
1514                  ucfirst($progname) . " must be configured: " .
1515                  "configuring now.\n\n";
1516            $opt->{'configure'} = 1;
1517        }
1518        return;
1519    }
1520    local (@ARGV, $/) = ($fn);
1521    no warnings 'all';
1522    eval <>;
1523    if ($@ and !$opt->{configure})
1524    {
1525        warn "\nERROR in $name file! Details:\n$@";
1526        print "You may wish to CTRL-C and fix this.\n\nContinuing anyway in:";
1527        foreach (1 .. 5)
1528        {
1529            print " " . (6 - $_);
1530            sleep 1;
1531        }
1532        print "\n";
1533    }
1534}
1535
1536sub write_config_file
1537{
1538    write_file($config_file, 'configuration', 
1539        [$region,  $pref_title_source,  $mirror_site,  $components ],
1540        ["region", "pref_title_source", "mirror_site", "components" ]);
1541}
1542
1543sub write_channels_file
1544{
1545    write_file($channels_file, 'channels',
1546        [ $channels,  $opt_channels ],
1547        [ 'channels', 'opt_channels' ]);
1548}
1549
1550sub write_file
1551{
1552    my ($fn, $name, $vars, $varnames) = @_;
1553    open (FN, ">$fn") or die "Can't write to $name file $fn: $!";
1554    print FN Data::Dumper->Dump($vars, $varnames);
1555    close FN;
1556    print "Wrote $name file $fn.\n" if ($debug);
1557}
1558
1559sub get_command_line_options
1560{
1561  GetOptions($opt, qw(  help
1562                        dontcallgrabbers
1563                        version
1564                        debug
1565                        days=i
1566                        offset=i
1567                        output=s
1568                        randomize
1569                    ));
1570    $debug = $opt->{debug};
1571    $days = $opt->{days} if ($opt->{days});
1572}
1573
1574# -----------------------------------------
1575# Subs: override handlers for standard perl.
1576# -----------------------------------------
1577
1578# ugly hack. please don't try this at home kids!
1579sub my_die {
1580    my ($arg,@rest) = @_;
1581    my ($pack,$file,$line,$sub) = caller(0);
1582
1583    # check if we are in an eval()
1584    if ($^S) {
1585        printf "  shepherd caught a die() within eval{} from file $file line $line\n";
1586    } else {
1587            printf "\nDIE: line %d in file %s\n",$line,$file;
1588            if ($arg) {
1589                CORE::die($arg,@rest);
1590            } else {
1591                CORE::die(join("",@rest));
1592            }
1593    }
1594}
Note: See TracBrowser for help on using the browser.