root/shepherd @ 140

Revision 140, 62.2 kB (checked in by max, 7 years ago)

Support for max_days_per_chan.

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