root/shepherd @ 151

Revision 151, 67.1 kB (checked in by max, 7 years ago)

Auto-enable components that were auto-disabled when they come back online.

Line 
1#!/usr/bin/perl -w
2
3# "Shepherd"
4
5my $version = '0.2.26';
6
7# A wrapper for various Aussie TV guide data grabbers
8#
9# Use --help for command-line options.
10# See shepherd.txt for details.
11#
12#  A current version of this script, plus a README file, might be here:
13#  http://www.whuffy.com/tv_grab_au/
14#
15# Changelog:
16# 0.1.0   : Basic self-updating and grabber management
17# 0.2.0   : --configure
18# 0.2.1   : Has a home in ~/.shepherd/
19# 0.2.2   : --check
20# 0.2.3   : Bugfix: archives correctly
21# 0.2.5   : Multi-grabber (potentially with partial data)
22# 0.2.6   : Postprocessor support
23# 0.2.7   : Changed online file structure
24# 0.2.8   : Integrated reconciler
25# 0.2.9   : Grabber config support
26# 0.2.10  : Bugfix: don't call postprocessors that aren't ready,
27#           rework accept-data-or-not postprocessor logic
28# 0.2.11  : Dedicated external reconciler support
29# 0.2.13  : revert 'alawys run' added in 0.2.12, --setorder bugfix
30# 0.2.14  : Changed online status file format
31# 0.2.15  : Intelli-random grabber ordering now kinda works
32# 0.2.16  : config logic for HD channels
33# 0.2.17  : bugfix timezone bogosities
34# 0.2.18  : care less about missing data in early-morning/overnight
35#           care more about missing data in evening/night
36# 0.2.22  : remove ->{order}, order is now set by quality
37#           explicitly tell reconciler the preferred _title_ source
38# 0.2.24  : logging and log files
39# 0.2.25  : use open-with-pipe rather than system() and look at
40#           return codes from called programmes
41
42BEGIN { *CORE::GLOBAL::die = \&my_die; }
43
44use strict;
45
46use LWP::Simple;
47use Sort::Versions;
48use Cwd;
49use Getopt::Long;
50use Data::Dumper;
51use XMLTV;
52use XMLTV::Ask;
53use POSIX qw(strftime mktime);
54use Date::Manip;
55use Algorithm::Diff;
56use List::Compare;
57use Compress::Zlib;
58
59# ---------------------------------------------------------------------------
60# --- Global Variables
61# ---------------------------------------------------------------------------
62
63my $progname = 'shepherd';
64
65my $HOME = 'http://www.whuffy.com/shepherd';
66
67my $invoked = Cwd::realpath($0);
68my @options = @ARGV;
69
70# By default, Shepherd runs from ~/.shepherd/. If it's not run as a user,
71# it will try /opt/shepherd/ instead.
72my $CWD = ($ENV{HOME} ? $ENV{HOME} . "/." : "/opt/") . $progname;
73-d $CWD or mkdir $CWD or die "Cannot create directory $CWD: $!";
74chdir($CWD);
75
76my $ARCHIVE_DIR = "$CWD/archive";
77
78
79#### analyzer settings ####
80# the following thresholds are used to control whether we keep calling grabbers or
81# not.
82
83my %policy;
84$policy{timeslot_size} = (5 * 60);      # 5 minute slots
85
86# PEAK timeslots -
87#  between 4.30pm and 11.30pm every day, only allow a maximum of
88#  15 minutes "programming data" missing
89#  if there is more than this, we will continue asking grabbers for more
90#  programming on this channel
91$policy{peak_max_missing} = 15*60;              # up to 15 mins max allowed missing
92$policy{peak_start} = (16*(60*60))+(30*60);     # 4.30pm
93$policy{peak_stop} = (23*(60*60))+(30*60);      # 11.30pm
94
95# NON-PEAK timeslots -
96#  between midnight and 6.15am every day, only allow up to 6 hours missing
97#  if there is more than this, we will continue asking grabbers for more
98#  programming on this channel
99$policy{nonpeak_max_missing} = 6*(60*60);       # up to 6 hours can be missing
100$policy{nonpeak_start} = 0;                     # midnight
101$policy{nonpeak_stop} = (6*(60*60))+(15*60);    # 6.15am
102
103# all other timeslots - (6.15am-4.30pm, 11.30pm-midnight)
104#  allow up to 60 minutes maximum missing programming
105$policy{other_max_missing} = 60*60;             # up to 60 mins max allowed missing
106
107# if a postprocessor failed 5 times in a row, automatically disable it
108$policy{postprocessor_disable_failure_threshold} = 5;
109
110#### end analyzer section ####
111
112my $opt;
113my $pref_title_source;
114my $mirror_site;
115my $made_changes = 0;
116my $debug = 0;
117my $components = { };
118my $gscore;
119my $region;
120my $channels;
121my $opt_channels;
122my $config_file =   "$CWD/$progname.conf";
123my $channels_file = "$CWD/channels.conf";
124my $log_file = "$CWD/$progname.log";
125my $days = 7;
126my $missing;
127my $timeslice;
128my $grabbed;
129my $gmt_offset;
130my $grabber_found_all_data;
131
132# postprocessing
133my $langs = [ 'en' ];
134my $plugin_data = { };
135my $channel_data = { };
136my $reconciler_found_all_data;
137my $input_postprocess_file = "";
138
139# OBSOLETE: will be removed
140my $grabbers;
141my $postprocessors;
142my $preferred;
143my $title_translation_table;
144my $pref_order;
145
146# ---------------------------------------------------------------------------
147# --- Setup
148# ---------------------------------------------------------------------------
149
150$| = 1;
151
152print ucfirst($progname) . " v$version\n\n";
153
154# Any options Shepherd doesn't understand, we'll pass to the grabber(s)
155Getopt::Long::Configure(qw/pass_through/);
156
157get_initial_command_line_options();
158
159help() if ($opt->{help});
160
161read_config_file();
162read_channels_file();
163
164get_remaining_command_line_options();
165
166if ($opt->{status})
167{
168    status();
169    exit;
170}
171
172if ($opt->{show_config})
173{
174    show_config();
175    exit;
176}
177
178if ($opt->{enable})
179{
180    enable($opt->{enable});
181}
182
183if ($opt->{disable})
184{
185    disable($opt->{disable});
186}
187
188if ($opt->{check})
189{
190    check();
191}
192
193if ($opt->{enable} or
194    $opt->{disable} or
195    $opt->{setorder} or
196    $opt->{check} or
197    $opt->{setpreftitlesource} or
198    $opt->{clearpreftitlesource} or
199    $opt->{mirror})
200{
201    write_config_file() if $made_changes;
202    status();
203    exit;
204}
205
206&open_logfile unless ($opt->{nolog});
207
208# ---------------------------------------------------------------------------
209# --- Update
210# ---------------------------------------------------------------------------
211
212unless ($opt->{noupdate})
213{
214    update($progname, $version);
215    write_config_file() if (($made_changes) && (! $opt->{configure}))
216}
217
218if ($opt->{configure})
219{
220    configure();
221}
222
223# ---------------------------------------------------------------------------
224# --- Go!
225# ---------------------------------------------------------------------------
226
227unless ($opt->{update})
228{
229    calc_date_range();
230    grab_data();
231    reconcile_data();
232    postprocess_data();
233    output_data();
234}
235
236&log("Done.\n");
237&close_logfile() unless $opt->{nolog};
238
239status();
240write_config_file();
241
242# ---------------------------------------------------------------------------
243# --- Subroutines
244# ---------------------------------------------------------------------------
245
246# -----------------------------------------
247# Subs: Grabbing
248# -----------------------------------------
249
250sub grab_data
251{
252    my $used_grabbers = 0;
253
254    &log("\nGrabber stage.\n");
255
256    &analyze_plugin_data("",1);   
257
258    while (my $grabber = choose_grabber())
259    {
260        $grabber_found_all_data = 0;
261        $used_grabbers++;
262
263        $components->{$grabber}->{lastdata} = time;
264        $components->{$grabber}->{laststatus} = "unknown";
265
266        &log((sprintf "\nSHEPHERD: Using grabber: (%d) %s\n", $used_grabbers, $grabber));
267
268        my $output = "$CWD/grabbers/$grabber/output.xmltv";
269
270        my $comm = "$CWD/grabbers/$grabber/$grabber " .
271                   "--region $region " .
272                   "--output $output";
273
274        # Category 1 grabbers (i.e. slow ones) are requested to only fetch the timeslice
275        # that we need. Category 2 grabbers are requested to get everything, since there's
276        # very little cost in grabbing that extra data, and we can use it in the reconciler
277        # to verify that everything looks OK.
278        if (query_config($grabber, 'category') == 1)
279        {
280            &log("$grabber is Category 1: grabbing timeslice.\n") if ($debug);
281
282            record_requested_chandays($grabber, $timeslice);
283
284            if ($timeslice->{start} != 0)
285            {
286                $comm .= " " . 
287                         query_config($grabber, 'option_days_offset') .
288                         " " .
289                         $timeslice->{start};
290            }
291
292            my $n = $timeslice->{stop} + 1;
293            if ($timeslice->{start} != 0 
294                    and 
295                !query_config($grabber, 'option_offset_eats_days'))
296            {
297                $n -= $timeslice->{start};
298            }
299            $comm .= " " .
300                     query_config($grabber, 'option_days') .
301                     " " . 
302                     $n;
303           
304            # Write a temporary channels file specifying only the channels we want
305            my $tmpchans;
306            foreach (@{$timeslice->{chans}})
307            {
308                $tmpchans->{$_} = $channels->{$_};
309            }
310            my $tmpcf = "$CWD/channels.conf.tmp";
311            write_file($tmpcf, 'temporary channels', [ $tmpchans ], [ 'channels' ]);
312            $comm .= " --channels_file $tmpcf";
313        }
314        else
315        {
316            &log("$grabber is category 2: grabbing everything.\n") if ($debug);
317            $comm .= " --days $days" if ($days);
318            $comm .= " --offset $opt->{offset}" if ($opt->{offset});
319            $comm .= " --channels_file $channels_file";
320        }
321        $comm .= " --debug" if ($debug);
322        $comm .= " @ARGV" if (@ARGV);
323
324        my $retval = 0;
325        if ((defined $opt->{dontcallgrabbers}) && ($opt->{dontcallgrabbers})) {
326            &log("SHEPHERD: not calling grabber because of --dontcallgrabbers option, but will instead use existing $output\n");
327            &log("SHEPHERD: would have called: $comm\n") if ($debug);
328        } else {
329            &log("SHEPHERD: Excuting command: $comm\n");
330            chdir "$CWD/grabbers/$grabber/";
331            $retval = call_prog($comm);
332            chdir $CWD;
333        }
334
335        if ($retval != 0) {
336            &log("grabber returned with non-zero return code $retval: assuming it failed.\n");
337            next;
338        }
339
340        # soak up the data we just collected
341        &soak_up_data($grabber, $output, "grabber");
342        $components->{$grabber}->{laststatus} = $plugin_data->{$grabber}->{laststatus};
343
344        # check to see if we have all the data we want
345        $grabber_found_all_data = &analyze_plugin_data("analysis of all grabbers so far");
346
347        # Record what we grabbed from cacheable C1 grabbers
348        if (query_config($grabber, 'category') == 1 and query_config($grabber, 'cache'))
349        {
350            my $missing_before = convert_dayhash_to_list($missing);
351            my $missing_after = convert_dayhash_to_list(detect_missing_data());
352            my $list = List::Compare->new($missing_before, $missing_after);
353            my @grabbed = $list->get_symmetric_difference();
354            &log("Grabbed: " . join (', ', @grabbed) . ".\n") if ($debug);
355            record_cached($grabber, @grabbed);
356            write_config_file();
357        }
358
359        last if ($grabber_found_all_data);
360    }
361
362
363    if ($used_grabbers == 0)
364    {
365        &log("No valid grabbers installed/enabled!\n");
366        return;
367    }
368
369    unless ($grabber_found_all_data)
370    {
371        &log("SHEPHERD: Ran through ALL grabbers but still missing data!!! :(\n");
372        return;
373    }
374}
375
376# -----------------------------------------
377# Subs: Intelli-random grabber selection
378# -----------------------------------------
379
380sub choose_grabber
381{
382    if (defined $gscore)        # Reset score hash
383    {
384        foreach (keys %$gscore)
385        {
386            $gscore->{$_} = 0;
387        }
388    }
389    else                        # Create score hash
390    {
391        foreach (query_grabbers())
392        {
393            unless ($components->{$_}->{disabled})
394            {
395                $gscore->{$_} = 0;
396                if (query_config($_, 'category') == 1 and query_config($_, 'cache'))
397                {
398                    $gscore->{$_ . ' [cache]'} = 0;
399                }
400            }
401        }
402    }
403
404    $missing = detect_missing_data();
405    $timeslice = find_best_timeslice();
406
407    if ($debug)
408    {
409        &log((sprintf "Best timeslice: day%s of channels %s (%d chandays).\n",
410                    ($timeslice->{start} == $timeslice->{stop} ?
411                        " $timeslice->{start}" :
412                        "s $timeslice->{start} - $timeslice->{stop}"),
413                    join(', ', @{$timeslice->{chans}}),
414                    $timeslice->{chandays}));
415    }
416
417    my $total = score_grabbers();
418 
419    if ($debug)
420    {
421        &log("Grabber selection:\n");
422        foreach (sort { $gscore->{$b} <=> $gscore->{$a} } keys %$gscore)
423        {
424            next if ($_ =~ /\[cache\]/);
425
426            my $score  = $gscore->{$_};
427            my $cscore = $gscore->{"$_ [cache]"};
428            my $cstr   = $cscore ? "(inc. $cscore cache pts)" : "";
429
430            if ($opt->{randomize})
431            {
432                &log((sprintf "%15s %6.1f%% %9s %s\n", 
433                        $_, 
434                        ($total ? 100* $score / $total : 0), 
435                        "$score pts",
436                        $cstr));
437            }
438            else
439            {
440                &log((sprintf   "%15s %4s pts %s\n", 
441                        $_, 
442                        $score,
443                        $cstr));
444            }
445        }
446    }
447
448    return undef unless ($total);
449
450    # Select a grabber
451    #
452    # Either do it randomly based on scores, or just return the
453    # highest-scoring grabber, depending on whether --randomize has
454    # been used.
455
456    my $r = int(rand($total));
457    my $c = 0;
458    my $best;
459
460    foreach my $grabber (keys %$gscore)
461    {
462        next if (!$gscore->{$grabber} or $grabber =~ /\[cache\]/);
463        if ($opt->{randomize})
464        {
465            if ($r >= $c and $r < ($c + $gscore->{$grabber}))
466            {
467                return select_grabber($grabber, $gscore);
468            }
469            $c += $gscore->{$grabber};
470        }
471        else
472        {
473            if (!$best or $gscore->{$grabber} > $gscore->{$best})
474            {
475                $best = $grabber;
476            }
477        }
478    }
479
480    if ($opt->{randomize} or !$best)
481    {
482        die "ERROR: failed to choose grabber.";
483    }
484    return select_grabber($best, $gscore);
485}
486
487sub select_grabber
488{
489    my ($grabber, $gscore) = @_;
490
491    &log("Selected $grabber.\n") if ($debug);
492    if (query_config($grabber, 'category') == 2)
493    {
494        # We might want to run C1 grabbers multiple times
495        # to grab various timeslices, but not C2 grabbers,
496        # which should get everything at once.
497        delete $gscore->{$grabber};
498    }
499    return $grabber;
500}
501
502# Grabbers earn 1 point for each slot or chanday they can fill.
503# This score is multiplied if the grabber:
504# * is a category 2 grabber (i.e. fast/cheap)
505# * is a category 1 grabber that has the data we want in a cache
506# * can supply high-quality data
507# Very low quality grabbers score 0 unless we need them; i.e. they're backups.
508sub score_grabbers
509{
510    my ($score, $total, $day, $catbonus, $dqbonus, $mult, $key);
511
512    my $bestdq = 0;
513
514    # Compare C2 grabbers against the raw missing file, because we'll get
515    # everything. But compare C1 grabbers against the timeslice, because we'll
516    # only ask them for a slice. This goes for the [cache] and regular C1s.
517    foreach my $grabber (keys %$gscore)
518    {
519        # for each slot, say whether we can fill it or not -- that is,
520        # whether we support this channel and this day #.
521
522        my $hits = 0;
523        my $cat = query_config($grabber, 'category');
524        my $dq = query_config($grabber, 'quality');
525
526        if ($cat == 1)
527        {
528            $key = cut_down_missing($grabber);
529            # &log("Grabber $grabber is Category 1: comparing capability to best timeslice.\n") if ($debug);
530        }
531        else
532        {
533            $key = $missing;
534            # &log("Grabber $grabber is Category 2: comparing capability to all wanted channels and days.\n") if ($debug);
535        }
536
537        if ($grabber =~ /\[cache\]/)
538        {
539            $hits = find_cache_hits($grabber, $key);
540        }
541        else
542        {
543            foreach my $day (sort keys %$key)
544            {
545                my $val = supports_day($grabber, $day);
546                next unless ($val);
547                # &log("Day $day:") if ($debug);
548                foreach my $ch (@{$key->{$day}})
549                {
550                    if (supports_channel($grabber, $ch, $day))
551                    {
552                        # &log(" $ch") if ($debug);
553                        $hits += $val;
554                    }
555                }
556                # &log("\n") if $debug;
557                $hits = 1 if ($hits > 0 and $hits < 1);
558            }
559        }
560
561        my $catbonus = 1;
562        $catbonus = 3 if ($cat == 2);
563        if ($grabber =~ /\[cache\]/)
564        {
565            # Bonus is on a sliding scale between 1 and 2 depending on
566            # % of required data in cache
567            $catbonus += $hits / $timeslice->{chandays};
568        }
569
570        my $dqbonus = 2 ** ($dq-1);
571
572        my $mult = $dq ** $catbonus;
573
574        my $score = int($hits * $mult);
575
576        if ($debug)
577        {
578            my $str = sprintf "Grabber %s can supply %d chandays",
579                                $grabber, $hits;
580            if ($hits)
581            {
582                $str .= sprintf " at x%.1f (cat: %d, DQ: %d): %d pts",
583                            $mult,
584                            $cat,
585                            $dq,
586                            $score;
587            }
588            &log("$str.\n");
589        }
590
591        $gscore->{$grabber} += $score;
592        $total += $score;
593        if ($grabber =~ /\[cache\]/)
594        {
595            $gscore->{query_name($grabber)} += $score;
596        }
597
598        if ($score and $dq > $bestdq)
599        {
600            $bestdq = $dq;
601        }
602    }
603
604    # Eliminate grabbers of data quality 1 if there are any better-quality
605    # alternatives when using randomize.
606    if ($opt->{randomize})
607    {
608        foreach (keys %$gscore)
609        {
610            if ($gscore->{$_}
611                    and
612                query_config($_, 'quality') == 1
613                    and
614                $bestdq > 1)
615            {
616                $total -= $gscore->{$_};
617                $gscore->{$_} = 0;
618                &log("Zeroing grabber $_ due to low data quality.\n") if ($debug);
619            }
620        }
621    }
622
623    return $total;
624}
625
626# Return 1 if the grabber can provide data for this channel, else 0.
627sub supports_channel
628{
629    my ($grabber, $ch, $day) = @_;
630
631    my $mdpc = query_config($grabber, 'max_days_per_chan');
632    if ($mdpc)
633    {
634        if ($mdpc->{$ch})
635        {
636            return ($mdpc->{$ch} > $day);
637        }
638    }
639
640    my $channels_supported = query_config($grabber, 'channels');
641    unless (defined $channels_supported)
642    {
643        &log("WARNING: Grabber $grabber has no channel support " .
644              "specified in config.\n");
645        $channels_supported = '';
646    }
647
648    return 1 unless ($channels_supported); # Empty string means we support all
649   
650    $ch =~ s/ /_/g;
651    my $match = ($channels_supported =~ /\b$ch\b/);
652    my $exceptions = ($channels_supported =~/^-/);
653    return ($match != $exceptions);
654}
655
656# Return 0 if the grabber can't provide data for this day,
657# 1 if it can reliably, and 0.5 if it can unreliably.
658#
659# Note that a max_days of 7 means the grabber can retrieve data for
660# today plus 6 days.
661sub supports_day
662{
663    my ($grabber, $day) = @_;
664
665    return 0 unless ($day < query_config($grabber, 'max_days'));
666    return 0.5 if ($day >= query_config($grabber, 'max_reliable_days'));
667    return 1;
668}
669
670sub find_cache_hits
671{
672    my ($grabber, $key) = @_;
673
674    $grabber = query_name($grabber);
675
676    return 0 unless ($components->{$grabber}->{cached});
677
678    my $hits = 0;
679
680    foreach my $day (keys %$key)
681    {
682        my $date = substr(DateCalc("today", "+ $day days"), 0, 8);
683        foreach my $ch (@{$key->{$day}})
684        {
685            $hits++ if (grep(/^$date:$ch$/, @{$components->{$grabber}->{cached}}));
686        }
687    }
688    return $hits;
689}
690
691# Build a dayhash of what channel/day data we're currently missing.
692# I think granularity of one day is good for now; could possibly be
693# made more fine-grained if we think grabbers will support that.
694sub detect_missing_data
695{
696    my $m = { };
697
698    my $chandays = 0;
699    foreach my $ch (keys %$channels)
700    {
701        # is this channel missing too much data?
702        unless ($channel_data->{$ch}->{analysis}->{data_ok}) {
703            # not ok - record which days are bad
704            foreach my $day (@{($channel_data->{$ch}->{analysis}->{day})}) {
705                push(@{($m->{($day->{num})})}, $ch) unless ($day->{day_ok});
706            }
707        }
708    }
709
710    foreach my $day (keys %$m)
711    {
712        $m->{$day} = [ sort @{$m->{$day}} ];
713        $chandays += scalar(@{$m->{$day}}) if ($debug);
714    }
715
716    if ($debug)
717    {
718        &log("Need data for days " . join(", ", sort keys %$m) . 
719             " ($chandays chandays).\n");
720    }
721    return $m;
722}
723
724# Find the largest timeslice in the current $missing dayhash; i.e.
725# something like "Days 4 - 6 of ABC and SBS." This works by iterating
726# through the days and looking for overlaps where consecutive days
727# want the same channels.
728sub find_best_timeslice
729{
730    my ($overlap, $a);
731    my $slice = { 'chandays' => 0 };
732
733    foreach my $day (0 .. $days-1)
734    {
735        consider_slice($slice, $day, $day, @{$missing->{$day}});
736        $overlap = $missing->{$day};
737        foreach my $nextday (($day + 1) .. $days-1)
738        {
739            last unless ($missing->{$nextday});
740            $a = Algorithm::Diff::LCS($overlap, $missing->{$nextday});
741            last unless ($a and @{$a});
742            consider_slice($slice, $day, $nextday, @{$a});
743            $overlap = $a;
744        }
745    }
746    return $slice;
747}
748
749sub consider_slice
750{
751    my ($slice, $startday, $stopday, @chans) = @_;
752
753    my $challenger = ($stopday - $startday + 1) * scalar(@chans);
754    return unless ($challenger > $slice->{chandays});
755
756    # We have a winner!
757    $slice->{start} = $startday;
758    $slice->{stop} = $stopday;
759    $slice->{chans} = [ @chans ];
760    $slice->{chandays} = $challenger;
761}
762
763# Record what a cacheable C1 grabber has just retrieved for us,
764# so we know next time that this data can be grabbed quickly.
765sub record_cached
766{
767    my ($grabber, @grabbed) = @_;
768
769    &log("Recording cache for grabber $grabber.\n") if ($debug);
770
771    my $gcache = $components->{$grabber}->{cached};
772    $gcache = [ ] unless ($gcache);
773    my @newcache;
774    my $today = strftime("%Y%m%d", localtime);
775
776    # remove old chandays
777    foreach my $chanday (@$gcache)
778    {
779        $chanday =~ /(\d+):(.*)/;
780        if ($1 >= $today)
781        {
782            push (@newcache, $chanday);
783        }
784    }
785
786    # record new chandays
787    foreach my $chanday (@grabbed)
788    {
789        push (@newcache, $chanday) unless (grep(/^$chanday$/, @newcache));
790    }
791    $components->{$grabber}->{cached} = [ @newcache ];
792}
793
794# Takes a dayhash and returns it as a list like this:
795# ( "20061018:ABC", "20061018:Seven", ... )
796sub convert_dayhash_to_list
797{
798    my $h = shift;
799
800    my @ret;
801    foreach my $day (keys %$h)
802    {
803        my $date = substr(DateCalc("today", "+ $day days"), 0, 8);
804        foreach my $ch (@{$h->{$day}})
805        {
806            push (@ret, "$date:$ch");
807        }
808    }
809    @ret = sort @ret;
810    return \@ret;
811}
812
813# If we're about to re-try a grabber, make sure that we're not asking
814# it for the same data. That is, prevent a broken C1 grabber causing
815# an infinite loop.
816sub record_requested_chandays
817{
818    my ($grabber, $slice) = @_;
819
820    &log("Recording timeslice request; will not request these chandays " .
821         "from $grabber again.\n") if ($debug);
822
823    my @requested;
824    for my $day ($slice->{start} .. $slice->{stop})
825    {
826        foreach my $ch (@{$slice->{chans}})
827        {
828            push @requested, "$day:$ch";
829        }
830    }
831    if ($grabbed->{$grabber})
832    {
833        push @{$grabbed->{$grabber}}, @requested;
834    }
835    else
836    {
837        $grabbed->{$grabber} = [ @requested ];
838    }
839}
840
841# If this grabber has been called previously, remove those chandays
842# from the current request -- we don't want to ask it over and over
843# for a timeslice that it has already failed to provide.
844sub cut_down_missing
845{
846    my $grabber = shift;
847
848    $grabber = query_name($grabber);
849    my $dayhash = {};
850
851    # Take the timeslice and expand it to a dayhash, while pruning
852    # any chandays that have previously been requested from this
853    # grabber.
854    foreach my $day ($timeslice->{start} .. $timeslice->{stop})
855    {
856        my @chans;
857        foreach my $ch (@{$timeslice->{chans}})
858        {
859            unless ($grabbed->{$grabber} and grep(/$day:$ch/, @{$grabbed->{$grabber}}))
860            {
861                push (@chans, $ch)
862            }
863        }
864        $dayhash->{$day} = [ @chans ] if (@chans);
865    }
866
867    return $dayhash;
868}
869
870# -----------------------------------------
871# Subs: Analyzing data
872# -----------------------------------------
873
874# interpret xmltv data from this grabber/postprocessor
875sub soak_up_data
876{
877    my ($plugin, $output, $plugintype) = @_;
878
879    if (! -r $output) {
880        &log((sprintf "SHEPHERD: Warning: plugin '%s' output file '%s' does not exist\n",$plugin,$output));
881        return;
882    }
883
884    my $this_plugin = $plugin_data->{$plugin};
885    &log((sprintf "SHEPHERD: Started parsing XMLTV from '%s' in '%s' .. any errors below are from parser:\n",$plugin,$output));
886    eval { $this_plugin->{xmltv} = XMLTV::parsefiles($output); };
887    &log((sprintf "SHEPHERD: Completed XMLTV parsing from '%s'\n",$plugin));
888
889    if (!($this_plugin->{xmltv})) {
890        &log("WARNING: Plugin $plugin didn't seem to return any valid XMLTV!\n");
891        return;
892    }
893
894    $this_plugin->{valid} = 1;
895    $this_plugin->{output_filename} = $output;
896
897    my $xmltv = $this_plugin->{xmltv};
898    my ($encoding, $credits, $chan, $progs) = @$xmltv;
899    $this_plugin->{total_duration} = 0;
900    $this_plugin->{programmes} = 0;
901    $this_plugin->{progs_with_invalid_date} = 0;        # explicitly track unparsable dates
902    $this_plugin->{progs_with_unknown_channel} = 0;     # explicitly track unknown channels
903
904    my $seen_channels_with_data = 0;
905
906    #
907    # first iterate through all programmes and see if there are any channels we don't know about
908    #
909    my %chan_xml_list;
910    foreach my $ch (sort keys %{$channels}) {
911        $chan_xml_list{($channels->{$ch})} = 1;
912    }
913    foreach my $prog (@$progs) {
914        if (!defined $chan_xml_list{($prog->{channel})}) {
915            $this_plugin->{progs_with_unknown_channel}++;
916            &log((sprintf " - WARNING: plugin '%s' returned data for unknown channel '%s': ignored.\n",$plugin,$prog->{channel}));
917            $chan_xml_list{($prog->{channel})} = 1;     # so we warn only once
918        }
919    }
920       
921    # iterate thru channels
922    foreach my $ch (sort keys %{$channels}) {
923        my $seen_progs_on_this_channel = 0;
924
925        # iterate thru programmes per channel
926        foreach my $prog (@$progs) {
927            next if ($prog->{channel} ne $channels->{$ch});
928
929            my $t1 = &parse_xmltv_date($prog->{start});
930            my $t2 = &parse_xmltv_date($prog->{stop});
931
932            if (!$t1 || !$t2) {
933                &log((sprintf " - WARNING: plugin '%s' returned programme data with invalid timestamp format: \"%s\": can't parse.\n",
934                    $plugin,(!$t1 ? $prog->{start} : $prog->{stop}))) if (!$this_plugin->{progs_with_invalid_date});
935                $this_plugin->{progs_with_invalid_date}++;
936                next;
937            }
938
939            # store plugin-specific stats
940            $this_plugin->{programmes}++;
941            $this_plugin->{total_duration} += ($t2 - $t1);
942            $seen_progs_on_this_channel++;
943            $this_plugin->{earliest_data_seen} = $t1 if (!defined $this_plugin->{earliest_data_seen});
944            $this_plugin->{earliest_data_seen} = $t1 if ($t1 < $this_plugin->{earliest_data_seen});
945            $this_plugin->{latest_data_seen} = $t2 if (!defined $this_plugin->{latest_data_seen});
946            $this_plugin->{latest_data_seen} = $t2 if ($t2 > $this_plugin->{latest_data_seen});
947
948            # store channel-specific stats
949            $channel_data->{$ch}->{programmes}++;
950            $channel_data->{$ch}->{total_duration} += ($t2 - $t1);
951
952            # programme is outside the timeslots we are interested in.
953            next if ($t1 > $policy{endtime});
954            next if ($t2 < $policy{starttime});
955
956            # store timeslot info
957            my $start_slotnum = 0;
958            $start_slotnum = int(($t1 - $policy{starttime}) / $policy{timeslot_size})
959                if ($t1 >= $policy{starttime});
960
961            my $end_slotnum = ($policy{num_timeslots}-1);
962            $end_slotnum = int(($t2 - $policy{starttime}) / $policy{timeslot_size})
963                if ($t2 < $policy{endtime});
964
965            # add this programme into the global timeslots table for this channel
966            foreach my $slotnum ($start_slotnum..$end_slotnum) {
967                $channel_data->{$ch}->{timeslots}[$slotnum]++;
968            }
969        }
970
971        $seen_channels_with_data++ if ($seen_progs_on_this_channel > 0);
972    }
973
974    # print some stats about what we saw!
975    &log((sprintf "SHEPHERD: %s '%s' returned data for %d channels, %d programmes, %dd%02dh%02dm%02ds duration, %s%s\n",
976        ucfirst($plugintype), $plugin, $seen_channels_with_data, $this_plugin->{programmes},
977        int($this_plugin->{total_duration} / 86400),            # days
978        int(($this_plugin->{total_duration} % 86400) / 3600),   # hours
979        int(($this_plugin->{total_duration} % 3600) / 60),      # mins
980        int($this_plugin->{total_duration} % 60),               # sec
981        (defined $this_plugin->{earliest_data_seen} ? (strftime "%a %e %b %H:%M - ", localtime($this_plugin->{earliest_data_seen})) : 'no data'),
982        (defined $this_plugin->{latest_data_seen} ? (strftime "%a %e %b %H:%M", localtime($this_plugin->{latest_data_seen})) : '')));
983
984    $this_plugin->{laststatus} = sprintf "%dch/%dpr/%dhrs %s-%s",
985        $seen_channels_with_data, $this_plugin->{programmes},
986        int($this_plugin->{total_duration} / 3600),
987        (defined $this_plugin->{earliest_data_seen} ? (strftime "%a%d%b", localtime($this_plugin->{earliest_data_seen})) : 'no'),
988        (defined $this_plugin->{latest_data_seen} ? (strftime "%a%d%b", localtime($this_plugin->{latest_data_seen})) : 'data');
989
990    $plugin_data->{$plugin} = $this_plugin;
991}
992
993
994# analyze grabber data - do we have all the data we want?
995sub analyze_plugin_data
996{
997    my ($analysistype,$quiet) = @_;
998    &log("SHEPHERD: $analysistype:\n") unless $quiet;
999
1000    my $total_channels = 0;
1001
1002    my $overall_data_ok = 1; # until proven otherwise
1003
1004    # iterate across each channel
1005    foreach my $ch (sort keys %{$channels}) {
1006        $total_channels++;
1007
1008        my $data;
1009        my $lastpol = "";
1010        $data->{data_ok} = 1; # unless proven otherwise
1011        $data->{have} = 0;
1012        $data->{missing} = 0;
1013
1014        for my $slotnum (0..($policy{num_timeslots}-1)) {
1015            my $bucket_start_offset = ($slotnum * $policy{timeslot_size});
1016
1017            # work out day number of when this bucket is.
1018            # number from 0 onwards.  (i.e. today=0).
1019            # for a typical 7 day grabber this will actually mean 8 days of data (0-7)
1020            # with days 0 and 7 truncated to half-days
1021            my $day = int(($bucket_start_offset + $policy{first_bucket_offset}) / 86400);
1022
1023            if (!defined $data->{day}->[$day]) {
1024                $data->{day}->[$day]->{num} = $day;
1025                $data->{day}->[$day]->{have} = 0;
1026                $data->{day}->[$day]->{missing} = 0;
1027                $data->{day}->[$day]->{missing_peak} = 0;
1028                $data->{day}->[$day]->{missing_nonpeak} = 0;
1029                $data->{day}->[$day]->{missing_other} = 0;
1030
1031                $data->{day}->[$day]->{day_ok} = 1; # until proven otherwise
1032
1033                # day changed, dump any 'already_missing' data
1034                &dump_already_missing($data);
1035            }
1036
1037            # we have programming data for this bucket.  great!  process next bucket
1038            if ((defined $channel_data->{$ch}->{timeslots}[$slotnum]) &&
1039                ($channel_data->{$ch}->{timeslots}[$slotnum] > 0)) {
1040
1041                # if we have missing data queued up, push it now
1042                &dump_already_missing($data);
1043
1044                &dump_already_missing_period($data->{day}->[$day],$lastpol)
1045                  if ($lastpol ne "");
1046
1047                $data->{day}->[$day]->{have} += $policy{timeslot_size};
1048                $data->{have} += $policy{timeslot_size};
1049                next;
1050            }
1051
1052            # we don't have programming for this channel for this bucket
1053
1054            # some grabbers take HOURS to run. if this bucket (missing data) is for
1055            # a time period now in the past, then don't include it
1056            next if (($bucket_start_offset + $policy{starttime}) < time);
1057
1058            # work out the localtime of when this bucket is
1059            my $bucket_seconds_offset = ($bucket_start_offset + $policy{first_bucket_offset}) % 86400;
1060
1061            # store details of where we are missing data
1062            if (!defined $data->{already_missing}) {
1063                $data->{already_missing} = sprintf "#%d/%02d:%02d",
1064                  $day,
1065                  int($bucket_seconds_offset / 3600),
1066                  int(($bucket_seconds_offset % 3600) / 60);
1067            }
1068            $data->{already_missing_last} = $bucket_seconds_offset + $policy{timeslot_size} - 1;
1069
1070            $data->{day}->[$day]->{missing} += $policy{timeslot_size};
1071            $data->{missing} += $policy{timeslot_size};
1072
1073            # work out what policy missing data for this bucket fits into
1074            my $pol;
1075            if (($bucket_seconds_offset >= $policy{peak_start}) &&
1076                (($bucket_seconds_offset+$policy{timeslot_size}) <= $policy{peak_stop})) {
1077                $pol = "peak";
1078            } elsif (($bucket_seconds_offset >= $policy{nonpeak_start}) &&
1079                     (($bucket_seconds_offset+$policy{timeslot_size}) <= $policy{nonpeak_stop})) {
1080                $pol = "nonpeak";
1081            } else {
1082                $pol = "other";
1083            }
1084
1085            &dump_already_missing_period($data->{day}->[$day],$lastpol)
1086              if (($lastpol ne $pol) && ($lastpol ne ""));
1087
1088            $lastpol = $pol;
1089
1090            $data->{day}->[$day]->{"missing_".$pol} += $policy{timeslot_size};
1091
1092            $data->{day}->[$day]->{"already_missing_".$pol."_start"} = $bucket_seconds_offset
1093              if (!defined $data->{day}->[$day]->{"already_missing_".$pol."_start"});
1094            $data->{day}->[$day]->{"already_missing_".$pol."_stop"} = $bucket_seconds_offset + $policy{timeslot_size} - 1;
1095
1096            $data->{day}->[$day]->{day_ok} = 0 if ($data->{day}->[$day]->{missing_peak} > $policy{peak_max_missing});
1097            $data->{day}->[$day]->{day_ok} = 0 if ($data->{day}->[$day]->{missing_nonpeak} > $policy{nonpeak_max_missing});
1098            $data->{day}->[$day]->{day_ok} = 0 if ($data->{day}->[$day]->{missing_other} > $policy{other_max_missing});
1099            $data->{data_ok} = 0 if ($data->{day}->[$day]->{day_ok} == 0);
1100            $overall_data_ok = 0 if ($data->{data_ok} == 0);
1101        }
1102
1103        # finished all timeslots in this channel.
1104        # if we have missing data queued up, push it now
1105        &dump_already_missing($data);
1106
1107        # fill in any last missing period data
1108        foreach my $day (@{($data->{day})}) {
1109            &dump_already_missing_period($day,"peak");
1110            &dump_already_missing_period($day,"nonpeak");
1111            &dump_already_missing_period($day,"other");
1112        }
1113
1114        my $statusstring = sprintf " > ch %s: %s programming: %s\n", 
1115          $ch, pretty_duration($data->{have}),
1116          $data->{data_ok} ? "PASS (within thresholds)" : "FAIL, missing data over policy threshold:";
1117
1118        # display per-day missing data statistics
1119        foreach my $day (@{($data->{day})}) {
1120            unless ($day->{day_ok}) {
1121                $statusstring .= sprintf "\t".(strftime("%a %e %b",localtime($policy{starttime}+($day->{num}*86400)))).": ";
1122
1123                # do we have any data for this day?
1124                $statusstring .= "peak ".join(", ",(@{($day->{missing_peak_table})}))
1125                  if (($day->{missing_peak}) && ($day->{missing_peak} > $policy{peak_max_missing}));
1126
1127                $statusstring .= sprintf "%snon-peak %s",
1128                  ($day->{missing_peak} ? " / " : ""),
1129                  join(", ",(@{($day->{missing_nonpeak_table})}))
1130                  if (($day->{missing_nonpeak}) && ($day->{missing_nonpeak} > $policy{nonpeak_max_missing}));
1131
1132                $statusstring .= sprintf "%sother %s",
1133                  (($day->{missing_peak} + $day->{missing_nonpeak}) > 0 ? " / " : ""),
1134                  join(", ",(@{($day->{missing_other_table})}))
1135                  if (($day->{missing_other}) && ($day->{missing_other} > $policy{other_max_missing}));
1136
1137                $statusstring .= "\n";
1138            }
1139        }
1140        &log($statusstring) unless $quiet;
1141
1142        delete $channel_data->{$ch}->{analysis} if (defined $channel_data->{$ch}->{analysis});
1143        $channel_data->{$ch}->{analysis} = $data;
1144    }
1145
1146    &log((sprintf " > OVERALL: %s\n", ($overall_data_ok ? "PASS" : "FAIL"))) unless $quiet;
1147
1148    return $overall_data_ok; # return 1 for good, 0 for need more
1149}
1150
1151# helper routine for filling in 'missing_all' array
1152sub dump_already_missing
1153{
1154    my $d = shift;
1155    if (defined $d->{already_missing}) {
1156        $d->{already_missing} .= sprintf "-%02d:%02d",
1157          int($d->{already_missing_last} / 3600),
1158          int(($d->{already_missing_last} % 3600) / 60)
1159          if (defined $d->{already_missing_last});
1160        push(@{($d->{missing_all})}, $d->{already_missing});
1161        delete $d->{already_missing};
1162        delete $d->{already_missing_last};
1163    }
1164}
1165
1166# helper routine for filling in per-day missing data
1167# specific to peak/nonpeak/other
1168sub dump_already_missing_period
1169{
1170    my ($d,$p) = @_;
1171    my $startvar = "already_missing_".$p."_start";
1172    my $stopvar = "already_missing_".$p."_stop";
1173
1174    if (defined $d->{$startvar}) {
1175        push(@{($d->{"missing_".$p."_table"})},
1176          sprintf "%02d:%02d-%02d:%02d",
1177            int($d->{$startvar} / 3600),
1178            int(($d->{$startvar} % 3600) / 60),
1179            int($d->{$stopvar} / 3600),
1180            int(($d->{$stopvar} % 3600) / 60));
1181        delete $d->{$startvar};
1182        delete $d->{$stopvar};
1183    }
1184}
1185
1186# given a duration (seconds), return it in a pretty "{days}d{hr}h{min}m" string
1187# and indication of whether the duration is over its threshold or not
1188sub pretty_duration
1189{
1190    my ($d,$crit) = @_;
1191    my $s = "";
1192    $s .= sprintf "%dd",int($d / (60*60*24)) if ($d >= (60*60*24));
1193    $s .= sprintf "%dh",int(($d % (60*60*24)) / (60*60)) if (($d % (60*60*24)) >= (60*60));
1194    $s .= sprintf "%dm",int(($d % (60*60)) / 60) if (($d % (60*60)) >= 60);
1195    $s .= "no" if ($s eq "");
1196
1197    if (defined $crit) {
1198        $s .= "[!]" if ($d > $crit);
1199    }
1200    return $s;
1201}
1202
1203# work out date range we are expecting data to be in
1204sub calc_date_range
1205{
1206    # work out GMT offset - we only do this once
1207    if (!$gmt_offset) {
1208        # work out our gmt offset
1209        my @l = localtime(43200), my @g = gmtime(43200);
1210        $gmt_offset = (($l[2] - $g[2])*(60*60)) + (($l[1] - $g[1])*60);
1211    }
1212
1213    $policy{starttime} = time;
1214
1215    # set endtime as per $days less 1 day + hours left today
1216    $policy{endtime} = $policy{starttime} + ((60*60*24)*($days-1)) + (86400 - (($policy{starttime} + $gmt_offset) % 86400));
1217
1218    # normalize starttime to beginning of next bucket
1219    $policy{starttime} += ($policy{timeslot_size} - ($policy{starttime} % $policy{timeslot_size}));
1220
1221    # work out how many seconds into a day our first bucket starts
1222    $policy{first_bucket_offset} = ($policy{starttime} + $gmt_offset) % 86400;
1223
1224    # normalize endtime to end of previous bucket
1225    $policy{endtime} -= ($policy{endtime} % $policy{timeslot_size});
1226
1227    # if we are working with an --offset, apply it now.
1228    $policy{starttime} += (86400 * $opt->{offset}) if ($opt->{offset});
1229
1230    # work out number of buckets
1231    $policy{num_timeslots} = ($policy{endtime} - $policy{starttime}) / $policy{timeslot_size};
1232}
1233
1234
1235# strptime type date parsing - BUT - if no timezone is present, treat time as being in localtime
1236# rather than the various other perl implementation which treat it as being in UTC/GMT
1237sub parse_xmltv_date
1238{
1239    my $datestring = shift;
1240    my @t; # 0=sec,1=min,2=hour,3=day,4=month,5=year,6=wday,7=yday,8=isdst
1241    my $tz_offset = 0;
1242
1243    if ($datestring =~ /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})/) {
1244        ($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);
1245        ($t[6],$t[7],$t[8]) = (-1,-1,-1);
1246
1247        # if input data has a timezone offset, then offset by that
1248        if ($datestring =~ /\+(\d{2})(\d{2})/) {
1249            $tz_offset = $gmt_offset - (($1*(60*60)) + ($2*60));
1250        } elsif ($datestring =~ /\-(\d{2})(\d{2})/) {
1251            $tz_offset = $gmt_offset + (($1*(60*60)) + ($2*60));
1252        }
1253
1254        my $e = mktime(@t);
1255        return ($e+$tz_offset) if ($e > 1);
1256    }
1257    return undef;
1258}
1259
1260# -----------------------------------------
1261# Subs: Reconciling data
1262# -----------------------------------------
1263
1264# for all the data we have, try to pick the best bits!
1265sub reconcile_data
1266{
1267    &log("\nReconciling data:\n\n");
1268
1269    my $num_grabbers = 0;
1270    my $input_files = "";
1271    my @input_file_list;
1272
1273    &log("Preferred title preferences from '$pref_title_source'\n")
1274        if ((defined $pref_title_source) &&
1275            ($plugin_data->{$pref_title_source}) &&
1276            ($plugin_data->{$pref_title_source}->{valid}));
1277
1278    &log("Preference for whose data we prefer as follows:\n");
1279    foreach my $proggy (sort { $components->{$b}->{config}->{quality} <=> $components->{$a}->{config}->{quality} } query_grabbers()) {
1280        if ((!$components->{$proggy}->{disabled}) && ($plugin_data->{$proggy}) && ($plugin_data->{$proggy}->{valid})) {
1281            $num_grabbers++;
1282            &log((sprintf "  %d. %s (%s)\n", $num_grabbers, $proggy, $plugin_data->{$proggy}->{output_filename}));
1283
1284            $input_files .= $plugin_data->{$proggy}->{output_filename}." ";
1285            push(@input_file_list,$plugin_data->{$proggy}->{output_filename});
1286        }
1287    }
1288
1289    if ($num_grabbers == 0) {
1290        die "Nothing to reconcile!  There is no valid grabber data!\n";
1291    }
1292
1293    foreach my $reconciler (sort { $components->{$a} <=> $components->{$b} } query_reconcilers()) {
1294        next if ($components->{$reconciler}->{disabled});
1295        next if (!$components->{$reconciler}->{ready});
1296
1297        $reconciler_found_all_data = &call_data_processor("reconciler",$reconciler,$input_files);
1298
1299        if ((!$reconciler_found_all_data) && ($grabber_found_all_data)) {
1300            # urgh.  this reconciler did a bad bad thing ...
1301            &log("SHEPHERD: XML data from reconciler $reconciler appears bogus, will try to use another reconciler\n");
1302        } else {
1303            &log("SHEPHERD: Data from reconciler $reconciler looks good\n");
1304            $input_postprocess_file = $plugin_data->{$reconciler}->{output_filename};
1305        }
1306
1307        last if ($input_postprocess_file ne "");
1308    }
1309
1310    if ($input_postprocess_file eq "") {
1311        # no reconcilers worked!!
1312        &log("SHEPHERD: WARNING: No reconcilers seemed to work!  Falling back to concatenating the data together!\n");
1313
1314        my %w_args = ();
1315        $input_postprocess_file = "$CWD/input_preprocess.xmltv";
1316        my $fh = new IO::File ">$input_postprocess_file" || die "could not open $input_postprocess_file for writing: $!\n";
1317        %w_args = (OUTPUT => $fh);
1318        XMLTV::catfiles(\%w_args, @input_file_list);
1319    }
1320}
1321
1322
1323# -----------------------------------------
1324# Subs: Postprocessing
1325# -----------------------------------------
1326
1327sub postprocess_data
1328{
1329    # for our first postprocessor, we feed it ALL of the XMLTV files we have
1330    # as each postprocessor runs, we feed in the output from the previous one
1331    # Shepherd checks the "completeness" of the data that comes out of a postprocessor & automatically
1332    # reverts back to the previous postprocessor if it was shown to be bad
1333
1334    # first time around: feed in reconciled data ($input_postprocess_file)
1335
1336    &log("\nPostprocessing stage:\n");
1337
1338    foreach my $postprocessor (sort { $components->{$a} <=> $components->{$b} } query_postprocessors()) {
1339        next if ($components->{$postprocessor}->{disabled});
1340        next if (!$components->{$postprocessor}->{ready});
1341
1342        my $found_all_data = call_data_processor("postprocessor",$postprocessor,$input_postprocess_file);
1343
1344        if ($found_all_data) {
1345            # accept what this postprocessor did to our output ...
1346            &log("SHEPHERD: accepting output from postprocessor $postprocessor, feeding it into next stage\n");
1347            $input_postprocess_file = $plugin_data->{$postprocessor}->{output_filename};
1348            delete $components->{$postprocessor}->{conescutive_failures} if (defined $components->{$postprocessor}->{conescutive_failures});
1349            next;
1350        }
1351
1352        # urgh.  this postprocessor did a bad bad thing ...
1353        &log("SHEPHERD: XML data from postprocessor $postprocessor rejected, using XML from previous stage\n");
1354
1355        if (defined $components->{$postprocessor}->{conescutive_failures}) {
1356            $components->{$postprocessor}->{conescutive_failures}++;
1357        } else {
1358            $components->{$postprocessor}->{conescutive_failures} = 1;
1359        }
1360        &log((sprintf "SHEPHERD: Postprocessor \"%s\" has now failed %d times in a row.  %d more and it will be automatically disabled.\n",
1361            $postprocessor,
1362            $components->{$postprocessor}->{conescutive_failures},
1363            ($policy{postprocessor_disable_failure_threshold} - $components->{$postprocessor}->{conescutive_failures})));
1364
1365        if ($components->{$postprocessor}->{conescutive_failures} >= $policy{postprocessor_disable_failure_threshold}) {
1366            &log("SHEPHERD: Disabling Postprocessor \"$postprocessor\".\n");
1367            $components->{$postprocessor}->{disabled} = 1;
1368        }
1369    }
1370}
1371
1372
1373# -----------------------------------------
1374# Subs: Postprocessing/Reconciler helpers
1375# -----------------------------------------
1376
1377sub call_data_processor
1378{
1379    my ($data_processor_type, $data_processor_name, $input_files) = @_;
1380
1381    $components->{$data_processor_name}->{lastdata} = time;
1382    $components->{$data_processor_name}->{laststatus} = "unknown";
1383
1384    &log((sprintf "\nSHEPHERD: Using %s: %s\n",$data_processor_type,$data_processor_name));
1385
1386    my $output = sprintf "%s/%ss/%s/output.xmltv",$CWD,$data_processor_type,$data_processor_name;
1387    my $comm = sprintf "%s/%ss/%s/%s",$CWD,$data_processor_type,$data_processor_name,$data_processor_name;
1388    $comm .= " --region $region" .
1389             " --channels_file $channels_file" .
1390             " --output $output";
1391    $comm .= " --days $days" if ($days);
1392    $comm .= " --offset $opt->{offset}" if ($opt->{offset});
1393    $comm .= " --debug" if ($debug);
1394    $comm .= " @ARGV" if (@ARGV);
1395
1396    $comm .= " --preftitle ".$plugin_data->{$pref_title_source}->{output_filename}
1397      if (($data_processor_type eq "reconciler") &&
1398          (defined $pref_title_source) &&
1399          ($plugin_data->{$pref_title_source}) &&
1400          ($plugin_data->{$pref_title_source}->{valid}));
1401
1402    $comm .= " $input_files";
1403    &log("SHEPHERD: Excuting command: $comm\n");
1404
1405    my $dir = sprintf "%s/%ss/%s/",$CWD,$data_processor_type,$data_processor_name;
1406    chdir $dir;
1407    my $retval = call_prog($comm);
1408    chdir $CWD;
1409
1410    if ($retval != 0) {
1411        &log("$data_processor_type returned with non-zero return code $retval: assuming it failed.\n");
1412        return 0;
1413    }
1414
1415    #
1416    # soak up the data we just collected and check it
1417    # YES - these are the SAME routines we used in the previous 'grabber' phase
1418    # but the difference here is that we clear out our 'channel_data' beforehand
1419    # so we can independently analyze the impact of this postprocessor.
1420    # if it clearly returns bad data, don't use that data (go back one step) and
1421    # flag the postprocessor as having failed.  after 3 consecutive failures, disable it
1422    #
1423
1424    # clear out channel_data
1425    foreach my $ch (keys %{$channels}) {
1426        delete $channel_data->{$ch};
1427    }
1428
1429    # process and analyze it!
1430    &soak_up_data($data_processor_name, $output, $data_processor_type);
1431    my $have_all_data = &analyze_plugin_data("$data_processor_type $data_processor_name");
1432
1433    $components->{$data_processor_name}->{laststatus} = $plugin_data->{$data_processor_name}->{laststatus};
1434
1435    return $have_all_data;
1436}
1437
1438
1439sub output_data
1440{
1441    # $input_postprocess_file contains our final output
1442    # send it to whereever --output told us to!
1443
1444    my $output_filename = "$CWD/output.xmltv";
1445    $output_filename = $opt->{output} if ($opt->{output});
1446
1447    open(OUTFILE,">$output_filename") || die "could not open output file $output_filename for writing: $!\n";
1448
1449    if (!(open(INFILE,"<$input_postprocess_file"))) {
1450        &log((sprintf "WARNING: could not open input file \"%s\": %s\n", $input_postprocess_file, $!));
1451        &log("Output XMLTV data may be damanged as a result!\n");
1452    } else {
1453        while (<INFILE>) {
1454            print OUTFILE $_;
1455        }
1456        close(INFILE);
1457        close(OUTFILE);
1458    }
1459
1460    &log("Final output stored in $output_filename.\n");
1461}
1462
1463# -----------------------------------------
1464# Subs: Updates & Installations
1465# -----------------------------------------
1466
1467sub update
1468{
1469    &log("\nChecking for updates:\n\n");
1470
1471    my $data = fetch_file("status");
1472
1473    return unless ($data);
1474
1475    my %clist = %$components;
1476    while ($data =~ /(.*):(.*):(.*)/g)
1477    {
1478        my ($progtype, $proggy, $latestversion) = ($1,$2,$3);
1479        # TEMP HACK FOR TRANSITION: REMOVE LATER
1480        if ($latestversion eq 'shepherd')
1481        {
1482            $latestversion = $proggy;
1483            $proggy = 'shepherd';
1484        }
1485        update_component($proggy, $latestversion, $progtype);
1486        delete $clist{$proggy};
1487    }
1488
1489    # work out what components disappeared (if any)
1490    foreach (keys %clist) {
1491        unless ($components->{$_}->{disabled}) {
1492            &log("\nDeleted component: $_.\n");
1493            disable($_, 2);
1494            $made_changes = 1;
1495        }
1496    }
1497}
1498
1499sub update_component
1500{
1501    my ($proggy, $latestversion, $progtype) = @_;
1502
1503    # handle new installs..
1504    if ($progtype eq "shepherd") {
1505        if(! -e "$CWD/$progname") {
1506            &log("Missing: $CWD/$progname\n");
1507            install($progname, $latestversion, $progtype);
1508            return;
1509        }
1510    } else {
1511        if (!defined $components->{$proggy} or ! -e ($progtype . "s/$proggy/$proggy")) {
1512            &log("NEW: $progtype: $proggy.\n");
1513            install($proggy, $latestversion, $progtype);
1514            return;
1515        }
1516        if ($components->{$proggy}->{disabled}) {
1517            &log("Warning: grabber $proggy disabled by config file.\n");
1518        }
1519    }
1520
1521    # upgrade/downgrades
1522    my $ver;
1523    if ($progtype eq "shepherd") {
1524        $ver = $version;
1525    }
1526    else {
1527        $ver = $components->{$proggy}->{ver};
1528    } 
1529
1530    my $result = versioncmp($ver, $latestversion);
1531    if ($result == -1) {
1532        &log("UPGRADING $proggy from v$ver to v$latestversion.\n");
1533    } elsif ($result == 1) {
1534        &log("DOWNGRADING $proggy from v$ver to v$latestversion.\n");
1535    } else {
1536        &log("Already have latest version of $proggy: v$ver.\n");
1537        return;
1538    }
1539    install($proggy, $latestversion, $progtype);
1540}
1541
1542sub install
1543{
1544    my ($proggy, $latestversion, $progtype) = @_;
1545    my $config;
1546
1547    &log("Downloading $proggy v$latestversion.\n");
1548
1549    my $rdir = "";
1550    my $ldir = $CWD;
1551    my $ver = "unknown";
1552
1553    if ($progtype eq "shepherd") {
1554        $ver = $version;
1555    } else {
1556        $rdir = $progtype . "s";
1557        $ldir = "$CWD/$progtype" . "s/$proggy";
1558        $ver = $components->{$proggy}->{ver} if ((defined $components->{$proggy}) && $components->{$proggy}->{ver});
1559        -d ("$CWD/$progtype" . "s") or mkdir ("$CWD/$progtype" . "s") or die "Cannot create directory $CWD/$progtype" . "s: $!";
1560    }
1561    -d $ldir or mkdir $ldir or die "Cannot create directory $ldir: $!";
1562
1563    my $newfile = "$ldir/$proggy-$latestversion";
1564    my $rfile = "$rdir/$proggy";
1565
1566    return unless (fetch_file($rfile, $newfile));
1567
1568    # Fetch grabber config file
1569    $rfile .= ".conf";
1570    $config = fetch_file($rfile);
1571
1572    if (!$config) {
1573        return if ($progtype eq "grabber"); # grabbers MUST have config files
1574    } else {
1575        eval $config;
1576    }
1577
1578    # Make component executable
1579    chmod 0755,$newfile;
1580
1581    -d $ARCHIVE_DIR or mkdir $ARCHIVE_DIR or die "Cannot create directory $ARCHIVE_DIR: $!";
1582
1583    if (-e "$ldir/$proggy")
1584    {
1585        rename("$ldir/$proggy", "$ARCHIVE_DIR/$proggy-$ver");
1586    }
1587    rename($newfile, "$ldir/$proggy");
1588   
1589    &log("Installed $proggy v$latestversion.\n") if ($debug);
1590
1591    # if the update was for shepherd itself, restart it
1592    if ($progtype eq "shepherd") {
1593        &log("\n*** Restarting ***\n\n");
1594        &close_logfile unless $opt->{nolog};
1595        exec("$ldir/$proggy @options");
1596        # This exits.
1597    }
1598
1599    &log("Testing $proggy...\n") if ($debug);
1600    my $result = test_proggy($ldir,"$ldir/$proggy");
1601
1602    $components->{$proggy}->{type} = $progtype;
1603    $components->{$proggy}->{ver} = $latestversion;
1604    $components->{$proggy}->{ready} = $result;
1605    $components->{$proggy}->{config} = $config;
1606
1607    # If this component was disabled automatically, re-enable it.
1608    # But if it was disabled manually, leave it off.
1609    my $d = $components->{$proggy}->{disabled};
1610    if ($d and $d == 2)
1611    {
1612        enable($proggy);
1613    }
1614
1615    $components->{$proggy}->{laststatus} = sprintf "updated to %s on %s", $latestversion, (strftime "%a%d%b%y",localtime(time));
1616
1617    $made_changes = 1;
1618}
1619
1620sub fetch_file
1621{
1622    my ($fn, $store) = @_;
1623
1624    my $sites = "";
1625    $sites = "$mirror_site," if ($mirror_site);
1626    $sites .= $HOME;
1627
1628    my $ret;
1629    foreach my $site (split(/,/,$sites)) 
1630    {
1631        &log("Fetching $site/$fn.\n");
1632        if ($store)
1633        {
1634            $ret = LWP::Simple::getstore("$site/$fn", $store);
1635            return 1 if (is_success($ret));
1636        }
1637        else
1638        {
1639            $ret = LWP::Simple::get("$site/$fn");
1640            return $ret if ($ret);
1641        }
1642        &log("Failed to retrieve $site/$fn.\n");
1643    }
1644    return undef;
1645}
1646
1647sub test_proggy
1648{
1649    my ($testdir,$proggyexec) = @_;
1650
1651    chdir($testdir);
1652    my $result = call_prog("$proggyexec --ready");
1653    chdir ($CWD);
1654
1655    print "Return value: $result\n" if ($debug);
1656
1657    print "\nComponent $proggyexec did not exit cleanly!\n" .
1658         "It may require configuration.\n\n" if ($result);
1659    return !$result;
1660}
1661
1662sub enable
1663{
1664    my $proggy = shift;
1665
1666    # confirm it exists first
1667    if (!$components->{$proggy}) {
1668        printf "No such component: \"%s\".\n",$proggy;
1669        return;
1670    }
1671    print "Enabling $proggy.\n";
1672
1673    delete $components->{$proggy}->{disabled};
1674    $components->{$proggy}->{laststatus} = sprintf "enabled on %s, not run yet",(strftime "%a%d%b%y", localtime(time));
1675    $made_changes = 1;
1676}
1677
1678sub disable
1679{
1680    my ($proggy, $n) = @_;
1681
1682    # confirm it exists first
1683    if (!$components->{$proggy}) {
1684        printf "No such component: \"%s\".\n",$proggy;
1685        return;
1686    }
1687    print "Disabling $proggy.\n";
1688   
1689    $n ||= 1;
1690    $components->{$proggy}->{disabled} = $n;
1691    $components->{$proggy}->{laststatus} = sprintf "manually disabled on %s",(strftime "%a%d%b%y", localtime(time));
1692    $made_changes = 1;
1693}
1694
1695sub check
1696{
1697    my $result;
1698    foreach my $proggy (keys %$components) {
1699        my $progtype = $components->{$proggy}->{type};
1700        $result = test_proggy("$CWD/$progtype" . "s/$proggy", "$CWD/$progtype" . "s/$proggy/$proggy");
1701        printf "%s %s: %s\n",ucfirst($progtype), $proggy,($result ? "OK" : "Failed");
1702        if (!$result ne !$components->{$proggy}->{ready}) {
1703            $components->{$proggy}->{ready} = $result;
1704            $made_changes = 1;
1705        }
1706    }
1707}
1708# -----------------------------------------
1709# Subs: Utilities
1710# -----------------------------------------
1711#
1712
1713sub query_grabbers
1714{
1715    my ($conf, $val) = @_;
1716    return query_component_type('grabber',$conf,$val);
1717}
1718
1719sub query_reconcilers
1720{
1721    return query_component_type('reconciler');
1722}
1723
1724sub query_postprocessors
1725{
1726    return query_component_type('postprocessor');
1727}
1728
1729sub query_component_type
1730{
1731    my ($progtype,$conf,$val) = @_;
1732
1733    my @ret = ();
1734    foreach (keys %$components)
1735    {
1736        if ($components->{$_}->{type} eq $progtype) {
1737            if (defined $conf) {
1738                push (@ret, $_) if (query_config($_,$conf) eq $val);
1739            } else {
1740                push (@ret, $_);
1741            }
1742        }
1743    }
1744    return @ret;
1745}
1746
1747sub query_name
1748{
1749    my $str = shift;
1750    if ($str =~ /(.*) \[cache\]/)
1751    {
1752        return $1;
1753    }
1754    return $str;
1755}
1756
1757sub query_config
1758{
1759    my ($grabber, $key) = @_;
1760
1761    $grabber = query_name($grabber);
1762    return undef unless ($components->{$grabber});
1763    return $components->{$grabber}->{config}->{$key};
1764}
1765
1766sub rotate_logfiles
1767{
1768    # keep last 4 log files
1769    my $num;
1770    for ($num = 4; $num > 0; $num--) {
1771        my $f1 = sprintf "%s.%d.gz",$log_file,$num;
1772        my $f2 = sprintf "%s.%d.gz",$log_file,$num+1;
1773        unlink($f2);
1774        rename($f1,$f2);
1775    }
1776
1777    my $f2 = sprintf "%s.1",$log_file;
1778    rename($log_file,$f2);
1779}
1780
1781sub compress_file
1782{
1783    my $infile = shift;
1784    my $outfile = sprintf "%s.gz",$infile;
1785    my $gz;
1786
1787    if (!(open(INFILE,"<$infile"))) {
1788        warn "could not open file $infile for reading: $!\n";
1789        return;
1790    }
1791
1792    if (!($gz = gzopen($outfile,"wb"))) {
1793        warn "could not open file $outfile for writing: $!\n";
1794        return;
1795    }
1796
1797    while (<INFILE>) {
1798        my $byteswritten = $gz->gzwrite($_);
1799        warn "error writing to compressed file: error $gz->gzerror"
1800          if ($byteswritten == 0);
1801    }
1802    close(INFILE);
1803    $gz->gzclose();
1804    unlink($infile);
1805}
1806
1807sub open_logfile
1808{
1809    &rotate_logfiles;
1810    printf "Logging to $log_file.\n";
1811    open(LOG_FILE,">$log_file") || die "can't open log file $log_file for writing: $!\n";
1812
1813    my $now = localtime(time);
1814    printf LOG_FILE "$progname version $version started at $now\n\n";
1815}
1816
1817sub close_logfile
1818{
1819    close(LOG_FILE);
1820    compress_file($log_file.".1");
1821}
1822
1823sub log
1824{
1825    my $entry = shift;
1826    print $entry;
1827    printf LOG_FILE "%s",$entry unless $opt->{nolog};
1828}
1829
1830sub call_prog
1831{
1832    my $prog = shift;
1833    if (!(open(PROG,"$prog|"))) {
1834        &log("warning: couldn't exec \"$prog\": $!\n");
1835        return -1;
1836    }
1837    while(<PROG>) {
1838        &log($_);
1839    }
1840    close(PROG);
1841
1842    if ($? == -1) {
1843        &log("Failed to execute prog: $!\n");
1844        return -1;
1845    } elsif ($? & 127) {
1846        &log((sprintf "prog died with signal %d, %s coredump\n",
1847          ($? & 127),  ($? & 128) ? "with" : "without"));
1848        return $?;
1849    } else {
1850        &log((sprintf "prog exited with value %d\n", $? >> 8));
1851        return ($? >> 8);
1852    }
1853}
1854
1855# -----------------------------------------
1856# Subs: Setup
1857# -----------------------------------------
1858
1859sub read_config_file
1860{
1861    read_file($config_file, 'configuration');
1862
1863    # TEMPORARY! Convert old $grabbers/$postprocessors config file to
1864    # new $components format.
1865    if (defined $grabbers or defined $postprocessors)
1866    {
1867        foreach (keys %$grabbers) {
1868            $grabbers->{$_}->{type} = 'grabber';
1869        }
1870        foreach (keys %$postprocessors) {
1871            $postprocessors->{$_}->{type} = 'postprocessor';
1872        }
1873        $components = { %$grabbers, %$postprocessors };
1874        $grabbers = undef;
1875        $postprocessors = undef;
1876    }
1877
1878
1879    # if a mirror has been specified, add it into our config
1880    if ($opt->{mirror}) {
1881        $mirror_site = $opt->{mirror};
1882        $made_changes = 1;
1883        print "Adding mirror: $mirror_site\n";
1884    }
1885
1886    # if a preferred title source has been specified, add it to our config
1887    if ($opt->{setpreftitlesource}) {
1888        $pref_title_source = $opt->{setpreftitlesource};
1889        $made_changes = 1;
1890        print "Added preferred title source: $pref_title_source\n";
1891    }
1892
1893    # if requesting to clear preferred title and we have one, remove it
1894    if (($opt->{clearpreftitlesource}) && (defined $pref_title_source)) {
1895        print "Removed preferred title source $pref_title_source\n";
1896        $pref_title_source = undef;
1897        $made_changes = 1;
1898    }
1899
1900}
1901
1902sub read_channels_file
1903{
1904    read_file($channels_file, 'channels');
1905}
1906
1907sub read_file
1908{
1909    my $fn = shift;
1910    my $name = shift;
1911
1912    print "Reading $name file: $fn\n";
1913    unless (-r $fn)
1914    {
1915        unless ($opt->{configure})
1916        {
1917            print "\nNo $name file found.\n" .
1918                  ucfirst($progname) . " must be configured: " .
1919                  "configuring now.\n\n";
1920            $opt->{'configure'} = 1;
1921        }
1922        return;
1923    }
1924    local (@ARGV, $/) = ($fn);
1925    no warnings 'all';
1926    eval <>;
1927    if ($@ and !$opt->{configure})
1928    {
1929        warn "\nERROR in $name file! Details:\n$@";
1930        print "You may wish to CTRL-C and fix this.\n\nContinuing anyway in:";
1931        foreach (1 .. 5)
1932        {
1933            print " " . (6 - $_);
1934            sleep 1;
1935        }
1936        print "\n";
1937    }
1938}
1939
1940sub write_config_file
1941{
1942    write_file($config_file, 'configuration', 
1943        [$region,  $pref_title_source,  $mirror_site,  $components ],
1944        ["region", "pref_title_source", "mirror_site", "components" ]);
1945}
1946
1947sub write_channels_file
1948{
1949    write_file($channels_file, 'channels',
1950        [ $channels,  $opt_channels ],
1951        [ 'channels', 'opt_channels' ]);
1952}
1953
1954sub write_file
1955{
1956    my ($fn, $name, $vars, $varnames) = @_;
1957    open (FN, ">$fn") or die "Can't write to $name file $fn: $!";
1958    print FN Data::Dumper->Dump($vars, $varnames);
1959    close FN;
1960    print "Wrote $name file $fn.\n" if ($debug);
1961}
1962
1963sub get_initial_command_line_options
1964{
1965  GetOptions( 'config-file=s'   => \$opt->{configfile},
1966              'help'            => \$opt->{help},
1967              'configure'       => \$opt->{configure},
1968              'mirror=s'        => \$opt->{mirror},
1969              'setpreftitle=s'  => \$opt->{setpreftitlesource},
1970              'clearpreftitle'  => \$opt->{clearpreftitlesource},
1971              'dontcallgrabbers' => \$opt->{dontcallgrabbers},
1972              'debug'           => \$debug);
1973}
1974
1975sub get_remaining_command_line_options
1976{
1977    GetOptions(
1978              'version'         => \$opt->{status},
1979              'status'          => \$opt->{status},
1980              'list'            => \$opt->{list},
1981              'show-config'     => \$opt->{show_config},
1982
1983              'update'          => \$opt->{update},
1984              'noupdate'        => \$opt->{noupdate},
1985
1986              'disable=s'       => \$opt->{disable},
1987              'enable=s'        => \$opt->{enable},
1988
1989              'nolog'           => \$opt->{nolog},
1990
1991              'days=i'          => \$days,
1992              'offset=i'        => \$opt->{offset},
1993              'show-channels'   => \$opt->{show_channels},
1994              'output=s'        => \$opt->{output},
1995              'randomize'       => \$opt->{randomize}, # experimental
1996              'check'           => \$opt->{check}
1997            );
1998}
1999
2000
2001# -----------------------------------------
2002# Subs: Configuration
2003# -----------------------------------------
2004
2005sub configure
2006{
2007    my $REGIONS = {
2008        "ACT" => 126,
2009        "NSW: Sydney" => 73,
2010        "NSW: Newcastle" => 184,
2011        "NSW: Central Coast" => 66,
2012        "NSW: Griffith" => 67,
2013        "NSW: Broken Hill" => 63,
2014        "NSW: Northern NSW" => 69,
2015        "NSW: Southern NSW" => 71,
2016        "NSW: Remote and Central" => 106,
2017        "NT: Darwin" => 74,
2018        "NT: Remote & Central" => 108,
2019        "QLD: Brisbane" => 75,
2020        "QLD: Gold Coast" => 78,
2021        "QLD: Regional" => 79,
2022        "QLD: Remote & Central" => 114,
2023        "SA: Adelaide" => 81,
2024        "SA: Renmark" => 82,
2025        "SA: Riverland" => 83,
2026        "SA: South East SA" => 85,
2027        "SA: Spencer Gulf" => 86,
2028        "SA: Remote & Central" => 107,
2029        "Tasmania" => 88,
2030        "VIC: Melbourne" => 94,
2031        "VIC: Geelong" => 93,
2032        "VIC: Eastern Victoria" => 90,
2033        "VIC: Mildura/Sunraysia" => 95,
2034        "VIC: Western Victoria" => 98,
2035        "WA: Perth" => 101,
2036        "WA: Regional" => 102
2037    };
2038
2039    print "\nConfiguring.\n\n" .
2040          "Select your region:\n";
2041    foreach (sort keys %$REGIONS)
2042    {
2043        printf(" (%3d) %s\n", $REGIONS->{$_}, $_);
2044    }
2045    $region = ask_choice("Enter region code:", "94", values %$REGIONS);
2046
2047    print "\nFetching channel information... ";
2048
2049    my @channellist = get_channels();
2050
2051    print "done.\n\n" .
2052          "For each channel you want guide data for, enter an XMLTV id\n" .
2053          "of your choice (e.g. \"seven.free.au\"). If you don't need\n" .
2054          "guide data for this channel, just press Enter.\n\n" .
2055          "Please don't subscribe to unneeded channels.\n\nChannels:\n";
2056    $channels = {};
2057    my $line;
2058    foreach (@channellist)
2059    {
2060        $line = ask(" \"$_\"? ");
2061        $channels->{$_} = $line if ($line);
2062    }
2063
2064    print "\nHigh Definition TV (HDTV)\n".
2065          "Most Australian TV networks broadcast at least some\n".
2066          "programmes in HDTV each week, but for the most part\n".
2067          "either upsample SD to HD or play a rolling demonstration\n".
2068          "HD clip when they don't have the programme in HD format.\n\n".
2069          "If you have a HDTV capable system and are interested in\n".
2070          "having Shepherd's postprocessors populate HDTV content\n".
2071          "then Shepherd will need to know the XMLTV IDs for the HD\n".
2072          "channels also.\n";
2073    if (ask_boolean("\nDo you wish to include HDTV channels?")) {
2074        print "\nFor each channel you want guide data for, enter an XMLTV id\n" .
2075              "of your choice (e.g. \"sevenhd.free.au\"). If you don't need\n" .
2076              "guide data for this channel, just press Enter.\n\n";
2077
2078        $opt_channels = {};
2079        foreach (@channellist)
2080        {
2081            next if (($_ =~ /ABC2/i) || ($_ =~ /SBS News/i) || ($_ =~ /31/));
2082            $_ .= "HD";
2083            $line = ask(" \"$_\"? ");
2084            $opt_channels->{$_} = $line if ($line);
2085        }
2086    }
2087
2088
2089    print "\nHave you been running a grabber previously?\n\n".
2090          "Some data sources show the programme \"Spicks & Specks\"\n".
2091          "with an '&', some show it as \"Spicks and Specks\" with 'and'.\n".
2092          "It doesn't matter which way it is - other than if you have an existing\n".
2093          "recording policy set one way or the other, it would be preferable\n".
2094          "to not have to go and re-enter recording schedules.\n\n".
2095          "Shepherd's \"reconciler\" is smart enough to work out many subtle\n".
2096          "differences in title names and will automatically rename programmes\n".
2097          "to match the same format as previously seen.\n\n".
2098          "If you have been using a grabber in the past, if you let Shepherd\n".
2099          "know what it is, it can use that to ask the reconciler to 'learn'\n".
2100          "the subtle programme name changes between different grabbers,\n".
2101          "hopefully resulting in consistent programme names, regardless of\n".
2102          "the data source.\n\n".
2103          "Were you using one of the following data sources previously?\n".
2104          "If so, type the name of it here or leave blank if you haven't\n".
2105          "run a grabber previously or don't care about having to fix existing\n".
2106          "recording schedules.\n\n".
2107          "Choose from: ".join(", ",query_grabbers())."\nChoice: ";
2108
2109    my $pref = ask("Order? ");
2110    $pref_title_source = $pref if ($pref);
2111
2112    print "\n";
2113    show_channels();
2114    unless(ask_boolean("\nCreate configuration file?"))
2115    {
2116        print "Aborting configuration.\n";
2117        exit 0;
2118    }
2119
2120    write_config_file();
2121    write_channels_file();
2122
2123    print "Finished configuring.\n\n" .
2124          "Shepherd is installed into $CWD.\n\n";
2125   
2126    if ($invoked ne "$CWD/$progname" and $invoked =~ /$progname/)
2127    {
2128        print "Warning: you invoked this program as $invoked.\n" .
2129            "In the future, it should be run as $CWD/$progname,\n" .
2130            "to avoid constantly re-downloading the latest version.\n\n" .
2131            "MythTV users may wish to create the following symlink, by " .
2132            "doing this (as root):\n" .
2133            "\"ln -s $CWD/$progname /usr/bin/tv_grab_au\".\n\n" .
2134            "You may safely delete $invoked.\n\n";
2135    }
2136
2137    status();
2138
2139    unless (ask_boolean("\nGrab data now?"))
2140    {
2141        exit 0;
2142    }
2143}
2144
2145sub get_channels
2146{
2147    my @date = localtime;
2148    my $page = LWP::Simple::get(
2149        "http://au.tv.yahoo.com/results.html?rg=$region&dt=" .
2150        ($date[5] + 1900) . "-$date[4]-$date[3]");
2151    my @channellist;
2152    while ($page =~ /<tr class=rtb><td class=rth><a .*?>(.*?)<\/a>/g)
2153    {
2154        push @channellist, $1;
2155    }
2156    return @channellist;
2157}
2158
2159# -----------------------------------------
2160# Subs: Status & Help
2161# -----------------------------------------
2162
2163sub show_config
2164{
2165    print "\nConfiguration\n".
2166          "-------------\n" .
2167          "Config file: $config_file\n" .
2168          "Debug mode : " . is_set($debug) . "\n" .
2169          "Output file: " . ($opt->{output} ? $opt->{output} : "None") . "\n" .
2170          "Region ID  : $region\n";
2171  show_channels();
2172  print "\n";
2173  status();
2174  print "\n";
2175}
2176
2177sub show_channels
2178{
2179  print "Subscribed channels:\n";
2180  print "    $_ -> $channels->{$_}\n" for sort keys %$channels;
2181  print "Optional (HDTV) channels:\n";
2182  print "    $_ -> $opt_channels->{$_}\n" for sort keys %$opt_channels;
2183}
2184
2185sub is_set
2186{
2187    my $arg = shift;
2188    return $arg ? "Yes" : "No";
2189}
2190
2191sub pretty_print
2192{
2193    my ($p, $len) = @_;
2194    my $spaces = ' ' x (79-$len);
2195    my $ret = "";
2196
2197    while (length($p) > 0) {
2198        if (length($p) <= $len) {
2199            $ret .= $p;
2200            $p = "";
2201        } else {
2202            # find a space to the left of cutoff
2203            my $len2 = $len;
2204            while ((substr($p,$len2,1) ne ' ') && ($len2 > 0)) {
2205                $len2--;
2206            }
2207            if ($len2 == 0) {
2208                # no space - just print it with cutoff
2209                $ret .= substr($p,0,$len);
2210                $p = substr($p,$len,(length($p)-$len));
2211            } else {
2212                # print up to space
2213                $ret .= substr($p,0,$len2);
2214                $p = substr($p,($len2+1),(length($p)-$len2+1));
2215            }
2216            # print whitespace
2217            $ret .= "\n".$spaces;
2218        }
2219    }
2220    return $ret;
2221}
2222
2223sub status
2224{
2225    print "\nThe following plugins are known:\n",
2226          " Type     Name           Version Description\n".
2227          " -------- -------------- ------- ----------------------------------------------\n";
2228
2229    foreach (sort { $components->{$a}->{type} cmp $components->{$b}->{type} } keys %{$components}) {
2230        printf " %-8s %-15s%7s %46s\n",
2231          substr($components->{$_}->{type},0,8), 
2232          length($_) > 15 ? substr($_,0,13).".." : $_,
2233          ($components->{$_}->{ver} ? substr($components->{$_}->{ver},0,7) : "unknown"),
2234          (defined $components->{$_}->{config}->{desc} ?
2235            pretty_print($components->{$_}->{config}->{desc},46) : "");
2236    }
2237    printf "\n";
2238
2239    print "Grabbers, listed in order of quality:\n".
2240          "                   Enabled/\n".
2241          " Grabber        Qual Ready Last Run   Status\n" .
2242          " -------------- ---- ----- ---------- -----------------------------------------\n";
2243    my %qual_table = ( 3 => "Best", 2 => "Good", 1 => "Avg" );
2244    foreach (sort { $components->{$b}->{config}->{quality} <=> $components->{$a}->{config}->{quality} } query_grabbers()) {
2245        my $h = $components->{$_};
2246        printf  " %-15s%-4s  %1s/%1s %11s %s\n",
2247          length($_) > 15 ? substr($_,0,13).".." : $_,
2248          $qual_table{($h->{config}->{quality})},
2249          $h->{disabled} ? 'N' : 'Y',
2250          $h->{ready} ? 'Y' : 'N',
2251          $h->{lastdata} ? (strftime "%a%d%b%y", localtime($h->{lastdata})) : 'never',
2252          $h->{laststatus} ? pretty_print($h->{laststatus},41) : '';
2253    }
2254
2255    print "\n".
2256          "              Enabled/\n".
2257          " Reconciler     Ready Last Run   Status\n" .
2258          " -------------- ----- ---------- ----------------------------------------------\n";
2259    foreach (sort { $components->{$a} <=> $components->{$b} } query_reconcilers()) {
2260        my $h = $components->{$_};
2261        printf  " %-15s %1s/%1s %11s %s\n",
2262          length($_) > 15 ? substr($_,0,13).".." : $_,
2263          $h->{disabled} ? 'N' : 'Y',
2264          $h->{ready} ? 'Y' : 'N',
2265          $h->{lastdata} ? (strftime "%a%d%b%y", localtime($h->{lastdata})) : 'never',
2266          $h->{laststatus} ? pretty_print($h->{laststatus},46) : '';
2267    }
2268
2269    print "\n".
2270          "              Enabled/\n".
2271          " Postprocessor  Ready Last Run   Status\n" .
2272          " -------------- ----- ---------- ----------------------------------------------\n";
2273    foreach (sort { $components->{$a} <=> $components->{$b} } query_postprocessors()) {
2274        my $h = $components->{$_};
2275        printf  " %-15s %1s/%1s %11s %s\n",
2276          length($_) > 15 ? substr($_,0,13).".." : $_,
2277          $h->{disabled} ? 'N' : 'Y',
2278          $h->{ready} ? 'Y' : 'N',
2279          $h->{lastdata} ? (strftime "%a%d%b%y", localtime($h->{lastdata})) : 'never',
2280          $h->{laststatus} ? pretty_print($h->{laststatus},46) : '';
2281    }
2282    printf "\nPreferred titles from grabber '%s'\n",$pref_title_source if ($pref_title_source);
2283    printf "\n";
2284}
2285
2286sub help
2287{
2288    print q{
2289Command-line options:
2290    --help                Print this message
2291
2292    --status              Print a list of grabbers maintained
2293    --list                Print a detailed list of grabbers
2294    --mirror <s>          Set URL <s> as primary location to check for updates
2295
2296    --configure           Setup
2297    --show-config         Print setup details
2298
2299    --setpreftitle <s>    Set preferred 'title' source as grabber <s>
2300    --clearpreftitle      clear preferred 'title' source
2301
2302    --disable <s>         Don't ever use grabber/postprocessor <s>
2303    --enable <s>          Okay, maybe use it again then
2304    --uninstall <s>       Remove a disabled grabber/postprocessor
2305
2306    --noupdate            Do not attempt to update before running
2307    --update              Update only; do not grab data
2308
2309    --check               Check status of all grabbers and postprocessors
2310
2311    --nolog               Don't write a logfile
2312};
2313    exit 0;
2314}
2315
2316# -----------------------------------------
2317# Subs: override handlers for standard perl.
2318# -----------------------------------------
2319
2320# ugly hack. please don't try this at home kids!
2321sub my_die {
2322    my ($arg,@rest) = @_;
2323    my ($pack,$file,$line,$sub) = caller(0);
2324
2325    # check if we are in an eval()
2326    if ($^S) {
2327        printf STDERR "  shepherd caught a die() within eval{} from file $file line $line\n";
2328    } else {
2329            printf STDERR "\nDIE: line %d in file %s\n",$line,$file;
2330            if ($arg) {
2331                CORE::die($arg,@rest);
2332            } else {
2333                CORE::die(join("",@rest));
2334            }
2335    }
2336}
Note: See TracBrowser for help on using the browser.