root/shepherd @ 130

Revision 130, 60.9 kB (checked in by lincoln, 7 years ago)

--status bling, more concise descriptions of all plugins, re-add .conf file back (optional) for postprocessors/reconcilers

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