root/shepherd @ 191

Revision 191, 49.4 kB (checked in by lincoln, 7 years ago)

CPAN dependency diet

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