root/shepherd @ 163

Revision 163, 67.3 kB (checked in by max, 7 years ago)

Removed a couple of obsolete global vars.

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