root/shepherd @ 158

Revision 158, 67.7 kB (checked in by max, 7 years ago)

Identify self in useragent string when fetching shepherd files.

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