root/shepherd @ 36

Revision 36, 64.5 kB (checked in by max, 7 years ago)

Some more intelli-random grabber selection order.

Line 
1#!/usr/bin/perl -w
2
3# "Shepherd"
4
5my $version = '0.2.8';
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
26BEGIN { *CORE::GLOBAL::die = \&my_die; }
27
28use strict;
29
30use LWP::Simple;
31use Sort::Versions;
32use Cwd;
33use Getopt::Long;
34use Data::Dumper;
35use XMLTV;
36use XMLTV::Ask;
37use DateTime::Format::Strptime;
38use POSIX qw(strftime);
39use Time::HiRes qw(gettimeofday tv_interval);
40
41# ---------------------------------------------------------------------------
42# --- Global Variables
43# ---------------------------------------------------------------------------
44
45my $progname = 'shepherd';
46
47my $HOME = 'http://www.whuffy.com/shepherd';
48
49my $invoked = Cwd::realpath($0);
50
51# By default, Shepherd runs from ~/.shepherd/. If it's not run as a user,
52# it will try /opt/shepherd/ instead.
53my $CWD = ($ENV{HOME} ? $ENV{HOME} . "/." : "/opt/") . $progname;
54-d $CWD or mkdir $CWD or die "Cannot create directory $CWD: $!";
55chdir($CWD);
56
57my $GRABBER_DIR = "$CWD/grabbers";
58my $POSTPROCESSOR_DIR = "$CWD/postprocessors";
59my $ARCHIVE_DIR = "$CWD/archive";
60
61
62#### analyzer settings ####
63
64my $timeslot_size = (15 * 60);                  # 15 minute slots
65my $channel_ok_threshold_percent = 90;          # 90% these may need to be
66                                                # tweaked but look ok for now
67my $postprocessor_ok_threshold_percent = 80;    # 80% these may need to be
68                                                # tweaked but look ok for now
69my $postprocessor_disable_failure_threshold = 5;# number of times a
70                                                # postprocessor has to fail
71                                                # in a row before it is
72                                                # automatically disabled
73
74#### reconciler settings ####
75my %reclogic;
76
77# when setting the upper-bounds of a timeslot to look for overlapping
78# programmes between grabbers, cap the upper time window that we are prepared
79# to tolerate programmes ending within.
80# disabled by default (max overrides percent for upper bounds)
81
82$reclogic{compare_overlapping_programmes_extra_overtime_duration_percent} = 5;
83$reclogic{compare_overlapping_programmes_extra_overtime_max} = 0;
84
85# if there is a conflict between two grabbers, choose the grabber
86# with the MOST programmes in the timeslot WITHOUT COUNTING
87# any programmes of <= $reclogic{min_time_override_for_duplicate}
88
89$reclogic{min_time_override_for_duplicate} = (5 * 60);  # default = 5 minutes
90
91# when we've selected one grabber to insert data from, we look at all the other
92# grabbers for overlapping fields we can use to augment our primary grabber
93# set some thresholds for when to consider programming to be the 'same'
94# (with fuzz -/+ 5 mins max), -/+ 2.5mins for programmes <= 15 mins
95
96$reclogic{duplicate_programme_augment_data_short_cutoff} = (15*60); # <=15mins = short, >15mins = long
97$reclogic{duplicate_programme_augment_data_long_duration_threshold} = (5*60); # +/-5 mins
98$reclogic{duplicate_programme_augment_data_short_duration_threshold} = (2.5*60); # +/- 2min30
99
100# for any programmes we didn't choose, reset the starttime up to a maximum
101# window of 5 minutes to the same value as our "chosen" endtime.
102# if can't readjust, then ignore (remove) that programme
103
104$reclogic{readjust_starttime_for_nonmatched_programmes} = (5*60);
105
106#### end reconciler settings ####
107
108my $opt;
109my $pref_order;
110my $mirror_site;
111my $made_changes = 0;
112my $debug = 1;
113my $recdebug = 0;
114my $grabbers = { };
115my $gscore;
116my $preferred; # obsolete but may still exist in shepherd.conf
117my $region;
118my $channels;
119my $config_file =   "$CWD/$progname.conf";
120my $channels_file = "$CWD/channels.conf";
121my $days = 7;
122
123# postprocessing
124my $postprocessors = { };
125my $title_translation_table = { };
126my $langs = [ 'en' ];
127my $num_timeslots;
128my $plugin_data = { };
129my $channel_data = { };
130my $starttime, my $endtime;
131my $input_postprocess_file = "$CWD/reconciled_output_pre_postprocessors.xml";
132my $insufficient_grabber_data = 0;
133
134# ---------------------------------------------------------------------------
135# --- Setup
136# ---------------------------------------------------------------------------
137
138print ucfirst($progname) . " v$version\n\n";
139#print "Cwd: $CWD.\n";
140
141# Any options Shepherd doesn't understand, we'll pass to the grabber(s)
142Getopt::Long::Configure(qw/pass_through/);
143
144get_initial_command_line_options();
145
146help() if ($opt->{help});
147
148unless ($opt->{configure})
149{
150    read_config_file();
151    read_channels_file();
152}
153
154get_remaining_command_line_options();
155
156if ($opt->{status})
157{
158    status();
159    exit;
160}
161
162if ($opt->{show_config})
163{
164    show_config();
165    exit;
166}
167
168if ($opt->{enable})
169{
170    enable($opt->{enable});
171}
172
173if ($opt->{disable})
174{
175    disable($opt->{disable});
176}
177
178if ($opt->{setorder})
179{
180    set_order(0, $opt->{setorder}); 
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->{mirror})
193{
194    set_order(1) if $made_changes;
195    write_config_file() if $made_changes;
196    status();
197    exit;
198}
199
200# ---------------------------------------------------------------------------
201# --- Update
202# ---------------------------------------------------------------------------
203
204unless ($opt->{noupdate})
205{
206    update($progname, $version);
207    set_order(1) if $made_changes;
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    my $need_more_data = 1;
246
247    print "\nGrabber stage:\n";
248
249    my $grabber;
250   
251    while ($grabber = choose_grabber())
252    {
253        $used_grabbers++;
254
255        $grabbers->{$grabber}->{lastdata} = time;
256        $grabbers->{$grabber}->{laststatus} = "unknown";
257
258        printf "\nSHEPHERD: Using grabber: (%d) %s\n", $used_grabbers, $grabber;
259
260        my $output = "$GRABBER_DIR/$grabber/output.xmltv";
261
262        my $comm = "$GRABBER_DIR/$grabber/$grabber " .
263                   "--region $region " .
264                   "--channels_file $channels_file " .
265                   "--output $output";
266
267        # NOTE: ideally a grabber could be instructed to fetch partial data through --channel, --starttime & --endtime
268        # we don't have that for now so instead whenever there is missing data, ALL 7 days for all channels will be collected
269        # FIXME FUTURE: call grabbers just with what we want...
270        $comm .= " --days $days" if ($days);
271        $comm .= " --offset $opt->{offset}" if ($opt->{offset});
272        $comm .= " --debug" if ($debug);
273        $comm .= " @ARGV" if (@ARGV);
274
275        if ((defined $opt->{dontcallgrabbers}) && ($opt->{dontcallgrabbers})) {
276            printf "SHEPHERD: not calling grabber because of --dontcallgrabbers option, but will instead use existing $output\n";
277        } else {
278            print "SHEPHERD: Excuting command: $comm\n";
279            chdir "$GRABBER_DIR/$grabber/";
280            system($comm);
281            chdir $CWD;
282        }
283
284        # soak up the data we just collected
285        &soak_up_data($grabber, $output, "grabber");
286        $grabbers->{$grabber}->{laststatus} = $plugin_data->{$grabber}->{laststatus};
287
288        # check to see if we have all the data we want
289        $need_more_data = &analyze_plugin_data($channel_ok_threshold_percent, "AGGREGATE GRABBER");
290
291        last if (!$need_more_data);
292    }
293
294
295    if ($used_grabbers == 0)
296    {
297        print "No valid grabbers installed/enabled!\n";
298        return;
299    }
300
301    if ($need_more_data)
302    {
303        print "SHEPHERD: Ran through ALL grabbers but still missing data!!! :(\n";
304        $insufficient_grabber_data = 1;
305        return;
306    }
307}
308
309sub choose_grabber
310{
311    unless (defined $gscore)
312    {
313        foreach (keys %$grabbers)
314        {
315            unless ($grabbers->{$_}->{disabled})
316            {
317                $gscore->{$_} = 0;
318            }
319        }
320    }
321
322    my $total = score_grabbers();
323
324    return undef unless ($total);
325
326    if ($debug)
327    {
328        print "Grabber selection probabilities:\n";
329        foreach (keys %$gscore)
330        {
331            printf "%15s %6.1f%% %12s\n", 
332                   $_, 100 * $gscore->{$_} / $total, "($gscore->{$_} pts)";
333        }
334    }
335
336    return undef unless (scalar keys %$gscore);
337
338    my $r = int(rand($total));
339#    print "Total score: $total.\nRand: $r.\n";
340
341    my $c = 0;
342    foreach (keys %$gscore)
343    {
344        next if (!$gscore->{$_});
345        if ($r >= $c and $r < ($c + $gscore->{$_}))
346        {
347            delete $gscore->{$_};
348            print "Selected $_.\n" if ($debug);
349            return $_;
350        }
351        $c += $gscore->{$_};
352    }
353    die "ERROR: failed to choose grabber.";
354}
355
356sub score_grabbers
357{
358    my ($score, $total, $niceness, $granularity, $m);
359
360    my $missing = detect_missing_data();
361
362    foreach my $grabber (keys %$gscore)
363    {
364        # for each slot, say whether we can fill it or not -- that is,
365        # whether we support this channel and this day #.
366       
367        my $hits = 0;
368        foreach (@$missing)
369        {
370            my ($ch, $day) = split(/:/);
371            my $result = (can_support_channel($grabber, $ch)
372                          and
373                          can_support_day($grabber, $day));
374            $hits += $result;
375#           print "$grabber vs $ch:$day: " . ($result ? "OK" : "no") . "\n";
376        }
377        $niceness = $grabbers->{$grabber}->{config}->{niceness};
378        unless ($niceness)
379        {
380            print "WARNING: Grabber $grabber has no niceness support " .
381                  "specified in config.\n";
382            $niceness = 5;
383        }
384        $granularity = $grabbers->{$grabber}->{config}->{granularity};
385        unless (defined $granularity)
386        {
387            print "WARNING: Grabber $grabber has no granularity support " .
388                "specified in config.\n";
389            $granularity = '';
390        }
391        # TODO: use granularity more intelligently ('c' vs 'd') -- Max.
392        $granularity = length ($granularity);
393        my $total_channeldays = $days * scalar (keys %$channels);
394        $granularity *= int((($total_channeldays - 1 ) / scalar(@$missing))/2);
395
396        $score = $hits * ($niceness + $granularity);
397        print "Grabber $grabber can fill $hits empty slots with $niceness niceness and $granularity granularity: scoring $score pts.\n";
398        $gscore->{$grabber} = $score;
399        $total += $score;
400    }
401    return $total;
402}
403
404sub can_support_channel
405{
406    my ($grabber, $ch) = @_;
407
408    my $channels_supported = $grabbers->{$grabber}->{config}->{channels};
409    unless (defined $channels_supported)
410    {
411        print "WARNING: Grabber $grabber has no channel support " .
412              "specified in config.\n";
413        $channels_supported = '';
414    }
415
416    return 1 unless ($channels_supported); # Empty string means we support all
417   
418    my $match = ($channels_supported =~ /\b$ch\b/);
419#    $match ||= 0;
420    my $exceptions = ($channels_supported =~/^-/);
421#    $exceptions ||= 0;
422#       print "M: " . Dumper ($match) . "E:" . Dumper ($exceptions) . "\n";
423#    print "Can $grabber support channel $ch: " . ($match != $exceptions ? 1 : 0) .
424        #      " (match $match, exceptions: $exceptions).\n";
425    return ($match != $exceptions);
426}
427
428sub can_support_day
429{
430    my ($grabber, $day) = @_;
431
432    my $days_supported = $grabbers->{$grabber}->{config}->{max_days};
433    unless ($days_supported)
434    {
435        print "WARNING: Grabber $grabber has no max_days support " .
436              "specified in config.\n";
437        $days_supported = 2;
438    }
439#    print "Can $grabber support day $day: " . ($day <= $days_supported) . ".\n";
440    return $day <= $days_supported;
441}
442
443#
444# Build a little hash of what channel/day data we're currently missing.
445# I think granularity of one day is good for now; could possibly be
446# made more fine-grained if we think grabbers will support that.
447#
448sub detect_missing_data
449{
450    my @missing;
451
452    my $timeslots_per_day = (24 * 60 * 60) / $timeslot_size;
453
454    foreach my $ch (keys %$channels)
455    {
456        if (defined $channel_data->{$ch})
457        {
458            my $slotnum;
459            for ($slotnum = 0; $slotnum < $num_timeslots-1; $slotnum++) 
460            {
461                if (!@{$channel_data->{$ch}->{timeslots}}[$slotnum])
462                {
463                    my $day = int($slotnum / $timeslots_per_day) + 1;
464                    push @missing, "$ch:$day";
465                    $slotnum += $timeslots_per_day -
466                                ($slotnum % $timeslots_per_day);
467
468                }
469            }
470        }
471        else
472        {
473            for my $i (1 .. $days)
474            {
475                push (@missing, "$ch:$i");
476            }
477        }
478
479    }
480
481    print "Need data for @missing.\n" if ($debug);
482    return \@missing;
483}
484
485
486# interpret xmltv data from this grabber/postprocessor
487sub soak_up_data
488{
489    my ($plugin, $output, $plugintype) = @_;
490
491    if (! -r $output) {
492        printf "SHEPHERD: Warning: plugin '%s' output file '%s' does not exist\n",$plugin,$output;
493        return;
494    }
495
496    my $parse_start_time = [gettimeofday];
497    printf STDERR "SHEPHERD: Started parsing XMLTV from '%s' in '%s' .. any errors below are from parser:\n",$plugin,$output;
498    eval { $plugin_data->{$plugin}->{xmltv} = XMLTV::parsefiles($output); };
499    printf STDERR "SHEPHERD: Completed XMLTV parsing from '%s' in %0.2f seconds\n",$plugin,tv_interval($parse_start_time);
500
501    if (defined $plugin_data->{$plugin}->{xmltv}) {
502        $plugin_data->{$plugin}->{valid} = 1;
503
504        my $xmltv = $plugin_data->{$plugin}->{xmltv};
505        my ($encoding, $credits, $chan, $progs) = @$xmltv;
506        $plugin_data->{$plugin}->{total_duration} = 0;
507        $plugin_data->{$plugin}->{programmes} = 0;
508
509        my $strptime = new DateTime::Format::Strptime( pattern => "%Y%m%d%H%M %z");
510        my $alt_strptime = new DateTime::Format::Strptime( pattern => "%Y%m%d%H%M"); # alternate format 1: oztivo doesn't seem to output timezone
511        my $seen_channels_with_data = 0;
512
513        # iterate thru channels
514        foreach my $ch (sort keys %{$channels}) {
515            my $seen_progs_on_this_channel = 0;
516
517            # iterate thru programmes per channel
518            foreach my $prog (@$progs) {
519                next if ($prog->{channel} ne $channels->{$ch});
520
521                my $t1 = $strptime->parse_datetime($prog->{start});
522                $t1 = $alt_strptime->parse_datetime($prog->{start}) if (!$t1);
523
524                my $t2 = $strptime->parse_datetime($prog->{stop});
525                $t2 = $alt_strptime->parse_datetime($prog->{stop}) if (!$t2);
526
527                next if (!$t1 || !$t2); # if we can't parse stop/start then clearly THIS data is bunk!
528
529                # store t1 and t2 times in the xmltv data for later on (shh.. ton't tell anyone..)
530                $prog->{start_epoch} = $t1->epoch;
531                $prog->{stop_epoch} = $t2->epoch;
532
533                # store plugin-specific stats
534                $plugin_data->{$plugin}->{programmes}++;
535                $plugin_data->{$plugin}->{total_duration} += ($t2->epoch - $t1->epoch);
536                $seen_progs_on_this_channel++;
537                $plugin_data->{$plugin}->{earliest_data_seen} = $t1->epoch if (!defined $plugin_data->{$plugin}->{earliest_data_seen});
538                $plugin_data->{$plugin}->{earliest_data_seen} = $t1->epoch if ($t1->epoch < $plugin_data->{$plugin}->{earliest_data_seen});
539                $plugin_data->{$plugin}->{latest_data_seen} = $t2->epoch if (!defined $plugin_data->{$plugin}->{latest_data_seen});
540                $plugin_data->{$plugin}->{latest_data_seen} = $t2->epoch if ($t2->epoch > $plugin_data->{$plugin}->{latest_data_seen});
541
542                # store channel-specific stats
543                $channel_data->{$ch}->{programmes}++;
544                $channel_data->{$ch}->{total_duration} += ($t2->epoch - $t1->epoch);
545
546                # store timeslot info
547                next if ($t1->epoch > $endtime);        # programme starts after timeslots we are interested .. nice that we have it ... but we really don't care about it!
548                next if ($t2->epoch < $starttime);      # programme ends  before timeslots we are interested .. nice that we have it ... but we really don't care about it!
549                my $start_slotnum;
550                if ($t1->epoch >= $starttime) {
551                    $start_slotnum = int(($t1->epoch - $starttime) / $timeslot_size);
552                } else {
553                    $start_slotnum = 0;
554                }
555                my $end_slotnum;
556                if ($t2->epoch < $endtime) {
557                    $end_slotnum = int(($t2->epoch - $starttime) / $timeslot_size);
558                } else {
559                    $end_slotnum = ($num_timeslots-1);
560                }
561
562                # add this programme into the global timeslots table for this channel
563                foreach my $slotnum ($start_slotnum..$end_slotnum) {
564                    $channel_data->{$ch}->{timeslots}[$slotnum]++;
565                }
566            }
567
568            $seen_channels_with_data++ if ($seen_progs_on_this_channel > 0);
569        }
570
571        # print some stats about what we saw!
572        printf "SHEPHERD: %s '%s' returned data for %d channels, %d programmes, %dd%02dh%02dm%02ds duration, %s%s\n",
573            ucfirst($plugintype), $plugin, $seen_channels_with_data, $plugin_data->{$plugin}->{programmes},
574            int($plugin_data->{$plugin}->{total_duration} / 86400),             # days
575            int(($plugin_data->{$plugin}->{total_duration} % 86400) / 3600),    # hours
576            int(($plugin_data->{$plugin}->{total_duration} % 3600) / 60),       # mins
577            int($plugin_data->{$plugin}->{total_duration} % 60),                # sec
578            (defined $plugin_data->{$plugin}->{earliest_data_seen} ? (strftime "%a %e %b %H:%M - ", localtime($plugin_data->{$plugin}->{earliest_data_seen})) : 'no data'),
579            (defined $plugin_data->{$plugin}->{latest_data_seen} ? (strftime "%a %e %b %H:%M", localtime($plugin_data->{$plugin}->{latest_data_seen})) : '');
580        $plugin_data->{$plugin}->{laststatus} = sprintf "%dch/%dpr/%dhrs %s-%s",
581            $seen_channels_with_data, $plugin_data->{$plugin}->{programmes},
582            int($plugin_data->{$plugin}->{total_duration} / 3600),
583            (defined $plugin_data->{$plugin}->{earliest_data_seen} ? (strftime "%a%d%b%H:%M", localtime($plugin_data->{$plugin}->{earliest_data_seen})) : 'no'),
584            (defined $plugin_data->{$plugin}->{latest_data_seen} ? (strftime "%a%d%b%H:%M", localtime($plugin_data->{$plugin}->{latest_data_seen})) : 'data');
585
586    } else {
587        printf "WARNING: Plugin %s didn't seem to return any valid XMLTV!\n",$plugin;
588        delete $plugin_data->{$plugin}->{valid};
589    }
590}
591
592
593# analyze grabber data - do we have all the data we want?
594# returns 1 if we need more data, 0 if we have all we want
595sub analyze_plugin_data
596{
597    my ($threshold,$analysistype) = @_;
598    my $retval = 0; # until proven otherwise
599    my $total_data_percent = 0, my $total_channels = 0;
600    my $statusstring = "";
601
602    # iterate across each channel
603    foreach my $ch (sort keys %{$channels}) {
604        $total_channels++;
605        if (defined $channel_data->{$ch}) {
606            my $data_in_channel = 0;
607            for my $slotnum (0..($num_timeslots-1)) {
608                $data_in_channel++ if ((defined $channel_data->{$ch}->{timeslots}[$slotnum]) && ($channel_data->{$ch}->{timeslots}[$slotnum] > 0));
609            }
610
611            # do we have enough data for this channel?
612            my $data_in_channel_percent = $data_in_channel / ($num_timeslots-1) * 100;
613            if ($data_in_channel_percent >= $threshold) {
614                $statusstring .= sprintf "%s: %0.1f%% [complete], ",$ch,$data_in_channel_percent;
615            } else {
616                $statusstring .= sprintf "%s: %0.1f%% [hungry], ",$ch,$data_in_channel_percent;
617                $retval = 1;
618            }
619            $total_data_percent += $data_in_channel_percent;
620        } else {
621            $statusstring .= sprintf "%s: 0%% [starving], ",$ch;
622            $retval = 1;
623        }
624    }
625
626    if ($total_channels > 0) {
627        $total_data_percent = $total_data_percent / $total_channels;
628    } else {
629        $total_data_percent = 0;
630    }
631
632    # print some stats about what our analysis says!
633    printf "SHEPHERD: %s ANALYSIS: %sTOTAL %0.2f%% %s %0.2f%%: %s\n",
634        uc($analysistype), $statusstring, $total_data_percent,
635        ($total_data_percent >= $channel_ok_threshold_percent ? ">" : "<"), $channel_ok_threshold_percent,
636        ($retval ? "WANT MORE DATA" : "COMPLETE");
637    return $retval;
638}
639
640
641# work out date range we are expecting data to be in
642sub calc_date_range
643{
644    # normalize starttime to beginning of hour
645    my $now = time;
646    my ($sec,$min,@rest) = localtime($now);
647
648    $starttime = $now - ((60 * $min) + $sec);
649
650    if ($days) {
651        $endtime = $starttime + ($days * 86400);
652    } else {
653        $endtime = $starttime + (7*86400);
654    }
655    $starttime += (86400 * $opt->{offset}) if ($opt->{offset});
656
657    $num_timeslots = ($endtime - $starttime) / $timeslot_size;
658}
659
660# -----------------------------------------
661# Subs: Reconciling data
662# -----------------------------------------
663
664# for all the data we have, try to pick the best bits!
665sub reconcile_data
666{
667    printf "\nReconciling data:\n\n";
668
669    my @proglist = [ ];
670    my @position_pointer = [ ];
671    my @grabber_order = [ ];
672    my $num_grabbers = 0;
673    my $pref_order;
674
675    printf "Preference for whose data we prefer as follows:\n";
676    foreach my $proggy (sort { $grabbers->{$a}->{order} <=> $grabbers->{$b}->{order} } keys %$grabbers) {
677        if ((!$grabbers->{$proggy}->{disabled}) && ($plugin_data->{$proggy}) && ($plugin_data->{$proggy}->{valid})) {
678            $grabber_order[$num_grabbers] = $proggy;
679            my $orig_prog = $plugin_data->{$proggy}->{xmltv}->[3];
680            my $prognum = 0;
681            foreach my $new_prog (sort order_channel_time @{$orig_prog}) {
682                $proglist[$num_grabbers]->[$prognum] = $new_prog;
683                $prognum++;
684            }
685
686            printf "  %d. %s (%d programmes)\n",($num_grabbers+1),$proggy,$prognum;
687            $num_grabbers++;
688        }
689    }
690
691    my %writer_args = ( encoding => 'ISO-8859-1' );
692    my $fh = new IO::File(">$input_postprocess_file") || die "can't open outputfile $input_postprocess_file: $!";
693    $writer_args{OUTPUT} = $fh;
694    my $writer = new XMLTV::Writer(%writer_args);
695
696    $writer->start({'source-info-url' => "about:blank", 'source-info-name' => "$progname $version", 'generator-info-name' => "$progname $version"} );
697
698    for my $ch (sort keys %$channels) {
699        $writer->write_channel({'display-name' => [[ $ch, "en" ]], 'id' => $channels->{$ch}} );
700    }
701
702    for my $ch (sort keys %$channels) {
703        printf "Reconciling channel: %s (%s)\n",$ch,$channels->{$ch};
704
705        #
706        # 1. position pointers to first piece of data for this channel
707        #
708
709        printf "REC#1: processing channel %s\n",$channels->{$ch} if $recdebug;
710        my $position_pointer;
711        for (my $i=0; $i < $num_grabbers; $i++) {
712            $position_pointer[$i] = 0;
713            while ((defined $proglist[$i]->[($position_pointer[$i])]) && ($proglist[$i]->[($position_pointer[$i])]->{'channel'} ne $channels->{$ch})) {
714                $position_pointer[$i]++;
715            }
716            if (!defined $proglist[$i]->[($position_pointer[$i])]) {
717                $position_pointer[$i] = -1;
718                printf "REC#1: no programmes found for channel %s from gradder %d\n",$channels->{$ch},$i if $recdebug;
719            } else {
720                printf "REC#1: advanced position_pointer to %d for grabber %d (first programme is \"%s\", start %d, stop %d)\n",
721                    $position_pointer[$i],$i,${XMLTV::best_name($langs,$proglist[$i]->[($position_pointer[$i])]->{title})}[0],
722                    $proglist[$i]->[($position_pointer[$i])]->{start_epoch},$proglist[$i]->[($position_pointer[$i])]->{stop_epoch} if $recdebug;
723            }
724        }
725
726        my $all_done_on_this_channel;
727        do {
728            #
729            # 2. find 'earliest' programme from all the choices
730            #
731
732            $all_done_on_this_channel = 1; # unless proven otherwise
733            my $earliest_programme_time = undef;
734            my $earliest_programme_slot = undef;
735            for (my $i=0; $i < $num_grabbers; $i++) {
736                next if ($position_pointer[$i] == -1); # skip if no programmes on this channel from this grabber
737                my $this_programme = $proglist[$i]->[($position_pointer[$i])];
738
739                if ((!defined $this_programme) || (!defined $this_programme->{title}) || ($this_programme->{channel} ne $channels->{$ch})) {
740                    # no more programmes on this channel for this grabber!
741                    printf "REC#2: no more programmes on grabber %d for this channel\n",$i if $recdebug;
742                    $position_pointer[$i] = -1;
743                } else {
744                    if ((!defined $earliest_programme_time) || ($earliest_programme_time > $this_programme->{'start_epoch'})) {
745                        $earliest_programme_time = $this_programme->{'start_epoch'};
746                        $earliest_programme_slot = $i;
747                        printf "REC#2: earliest programme (so far) on grabber %d, start %d (end %d) programme \"%s\"\n",
748                            $i,$this_programme->{start_epoch},$this_programme->{stop_epoch},${XMLTV::best_name($langs,$this_programme->{title})}[0] if $recdebug;
749                    } else {
750                        printf "REC#2:    programme on grabber %d was not earlier start %d (end %d) programme \"%s\"\n",
751                            $i,$this_programme->{start_epoch},$this_programme->{stop_epoch},${XMLTV::best_name($langs,$this_programme->{title})}[0] if $recdebug;
752                    }
753                }
754            }
755            if (!defined $earliest_programme_slot) {
756                # no programmes available on ANY grabber for this channel, skip this channel
757                printf "REC#2: no programmes on any grabbers for channel %s, skipping this channel\n",$channels->{$ch} if $recdebug;
758            } else {
759                #
760                # 3a. compare how many programmes on other grabbers overlap with it
761                # TODO (FUTURE): enhance this to do "majority voting" acrosa all grabbers based on identical start/stop
762                # TODO (FUTURE): where star/stop times match exactly but title names dont, record the mapping for known-as_to_most-preferred-title so we can use this in future transformations
763
764                my $preferred_programme = $proglist[$earliest_programme_slot]->[($position_pointer[$earliest_programme_slot])];
765                my $startpoint = $preferred_programme->{'start_epoch'};
766                my $stoppoint = $preferred_programme->{'stop_epoch'};
767                my $duration = $stoppoint - $startpoint;
768                my $extraduration = 0;
769                $extraduration = int($duration * ($reclogic{compare_overlapping_programmes_extra_overtime_duration_percent}/100)) if ($reclogic{compare_overlapping_programmes_extra_overtime_duration_percent} > 0);
770                $extraduration = $reclogic{compare_overlapping_programmes_extra_overtime_max} if ($extraduration > $reclogic{compare_overlapping_programmes_extra_overtime_max}); # upper limit
771                $stoppoint += $extraduration;
772                printf "REC#3: comparing other grabbers to see how many programmes fit in timeslot %d to %d (%d+%d)\n",$startpoint,$stoppoint,$duration,$extraduration if $recdebug;
773
774                my $max_progs_found = 0;
775                my $max_progs_slot = undef;
776                for (my $i=0; $i < $num_grabbers; $i++) {
777                    next if ($i == $earliest_programme_slot);
778                    next if ($position_pointer[$i] == -1);
779                    my $position_offset = 0;
780                    my $progs_found = 0;
781
782                    while ((defined $proglist[$i]->[($position_pointer[$i]+$position_offset)]) &&
783                           ($proglist[$i]->[($position_pointer[$i]+$position_offset)]->{'channel'} eq $channels->{$ch}) &&
784                           ($proglist[$i]->[($position_pointer[$i]+$position_offset)]->{'start_epoch'} >= $startpoint) &&
785                           ($proglist[$i]->[($position_pointer[$i]+$position_offset)]->{'stop_epoch'} <= $stoppoint)) {
786                        my $this_prog_duration = $proglist[$i]->[($position_pointer[$i]+$position_offset)]->{'stop_epoch'} - $proglist[$i]->[($position_pointer[$i]+$position_offset)]->{'start_epoch'};
787
788                        printf "REC#3a.  programme on grabber %d matched (start %d end %d), \"%s\"%s\n", $i,
789                            $proglist[$i]->[($position_pointer[$i]+$position_offset)]->{'start_epoch'},
790                            $proglist[$i]->[($position_pointer[$i]+$position_offset)]->{'stop_epoch'},
791                            ${XMLTV::best_name($langs,$proglist[$i]->[($position_pointer[$i]+$position_offset)]->{'title'})}[0],
792                            ($this_prog_duration <= $reclogic{min_time_override_for_duplicate} ? 
793                            ", but ignored because of min_time_override_for_duplicate ($reclogic{min_time_override_for_duplicate} sec)" : "") if $recdebug;
794
795                        $progs_found++ if ($this_prog_duration > $reclogic{min_time_override_for_duplicate});
796                        $position_offset++;
797                    }
798
799                    printf "REC#3a:  %d programmes on grabber %d within timeslot\n",$progs_found,$i if $recdebug;
800                    if ($progs_found > $max_progs_found) {
801                        $max_progs_found = $progs_found;
802                        $max_progs_slot = $i;
803                    }
804                }
805
806                # 3b. if there are 2 or more programmes on other channels, use those - otherwise use this one
807                if ((!defined $max_progs_slot) || ($max_progs_found <= 1)) {
808                    printf "REC#3b: no grabbers with more programmes in timeslot found\n" if $recdebug;
809                } else {
810                    printf "REC#3b: grabber %d has %d programmes between %d and %d - using THAT grabber as our preference now!\n",
811                        $max_progs_slot,$max_progs_found,$startpoint,$stoppoint if $recdebug;
812                    $earliest_programme_slot = $max_progs_slot;
813                }
814
815                #
816                # 4. populate our "reconciled" programme list with the programming chosen
817                #
818
819                my $chosen_prog = $proglist[$earliest_programme_slot]->[($position_pointer[$earliest_programme_slot])];
820                my $chosen_prog_name = ${XMLTV::best_name($langs,$chosen_prog->{'title'})}[0];
821                $startpoint = $chosen_prog->{'start_epoch'};
822                $stoppoint = $chosen_prog->{'stop_epoch'};
823                my $new_prog_entry = $chosen_prog;
824
825                printf "REC#4: chosen programme is from grabber %d: start %d, end %d, duration %d: \"%s\"\n",
826                    $earliest_programme_slot,$startpoint,$stoppoint,($stoppoint-$startpoint),$chosen_prog_name if $recdebug;
827
828                #
829                # 5a. see if we have it duplicated from multiple grabbers, first with exact match
830                # 5b. then with fuzz -/+ 5 mins max, -/+ 2.5mins for programmes <= 15 mins
831               
832                my $start1, my $start2, my $stop1, my $stop2;
833
834                if (($stoppoint - $startpoint) <= $reclogic{duplicate_programme_augment_data_short_cutoff}) {
835                    $start1 = $startpoint - $reclogic{duplicate_programme_augment_data_short_duration_threshold};
836                    $start2 = $startpoint + $reclogic{duplicate_programme_augment_data_short_duration_threshold};
837                    $stop1 = $stoppoint - $reclogic{duplicate_programme_augment_data_short_duration_threshold};
838                    $stop2 = $stoppoint + $reclogic{duplicate_programme_augment_data_short_duration_threshold};
839                } else {
840                    $start1 = $startpoint - $reclogic{duplicate_programme_augment_data_long_duration_threshold};
841                    $start2 = $startpoint + $reclogic{duplicate_programme_augment_data_long_duration_threshold};
842                    $stop1 = $stoppoint - $reclogic{duplicate_programme_augment_data_long_duration_threshold};
843                    $stop2 = $stoppoint + $reclogic{duplicate_programme_augment_data_long_duration_threshold};
844                }
845                if ($start2 >= $stop1) {
846                    $start2 = $startpoint;
847                    $stop1 = $stoppoint;
848                }
849
850                printf "REC#5a: looking in other grabbers for matching programmes within timeslot start %d and end %d (%d)\n",$startpoint,$stoppoint,($stoppoint-$startpoint) if $recdebug;
851                for (my $i=0; $i < $num_grabbers; $i++) {
852                    next if ($i == $earliest_programme_slot);
853                    next if ($position_pointer[$i] == -1);
854                    if ((defined $proglist[$i]->[($position_pointer[$i])]) &&
855                        ($proglist[$i]->[($position_pointer[$i])]->{'channel'} eq $channels->{$ch}) &&
856                        ($proglist[$i]->[($position_pointer[$i])]->{'start_epoch'} == $startpoint) &&
857                        ($proglist[$i]->[($position_pointer[$i])]->{'stop_epoch'} == $stoppoint)) {
858                        # winner .. matches our criteria ...
859                        my $match_prog = $proglist[$i]->[($position_pointer[$i])];
860                        my $match_prog_name = ${XMLTV::best_name($langs,$match_prog->{'title'})}[0];
861
862                        printf "REC#5a:   found programme on grabber %d: start %d, end %d: \"%s\"\n", $i,
863                            $match_prog->{'start_epoch'},$match_prog->{'stop_epoch'},$match_prog_name if $recdebug;
864
865                        if ($chosen_prog_name ne $match_prog_name) {
866                            # names differ - save it in our translation table for future reference!
867                            $title_translation_table->{$grabber_order[$i]}->{$match_prog_name} = $chosen_prog_name;
868                            printf "REC#5a: title-translation: higher-preference grabber '%s' called programme \"%s\" compared to '%s' calling it \"%s\"\n",
869                                $grabber_order[$earliest_programme_slot],$chosen_prog_name,$grabber_order[$i],$chosen_prog_name if $recdebug;
870                        }
871
872                        foreach my $field (keys %{$match_prog}) {
873                            next if ($field eq "start_epoch");
874                            next if ($field eq "stop_epoch");
875                            if (!defined $new_prog_entry->{$field}) {
876                                printf "REC#5a:      adding field \"%s\"\n",$field if $recdebug;
877                                $new_prog_entry->{$field} = $match_prog->{$field};
878                                # TODO (FUTURE): should we add to programme description to say where we got what data from?
879                            }
880                        }
881                    }
882                }
883
884                printf "REC#5b: looking in other grabbers for matching programmes within timeslot start %d-%d (%d) and end %d-%d (%d)\n",
885                    $start1,$start2,($start2-$start1),$stop1,$stop2,($stop2-$stop1) if $recdebug;
886
887                for (my $i=0; $i < $num_grabbers; $i++) {
888                    next if ($i == $earliest_programme_slot);
889                    next if ($position_pointer[$i] == -1);
890
891                    if ((defined $proglist[$i]->[($position_pointer[$i])]) &&
892                        ($proglist[$i]->[($position_pointer[$i])]->{'channel'} eq $channels->{$ch}) &&
893                        ($proglist[$i]->[($position_pointer[$i])]->{'start_epoch'} >= $start1) &&
894                        ($proglist[$i]->[($position_pointer[$i])]->{'start_epoch'} < $start2) &&
895                        ($proglist[$i]->[($position_pointer[$i])]->{'stop_epoch'} >= $stop1) &&
896                        ($proglist[$i]->[($position_pointer[$i])]->{'stop_epoch'} < $stop2)) {
897                        # winner .. matches our criteria ...
898                        my $match_prog = $proglist[$i]->[($position_pointer[$i])];
899
900                        printf "REC#5b:   found programme on grabber %d: start %d, end %d: \"%s\"\n", $i,
901                            $match_prog->{'start_epoch'},$match_prog->{'stop_epoch'},
902                            ${XMLTV::best_name($langs,$match_prog->{'title'})}[0] if $recdebug;
903
904                        foreach my $field (keys %{$match_prog}) {
905                            # 5b. pick fields from each one in order of our preferences
906                            next if ($field eq "start_epoch");
907                            next if ($field eq "stop_epoch");
908                            if (!defined $new_prog_entry->{$field}) {
909                                printf "REC#5b:      adding field \"%s\"\n",$field if $recdebug;
910                                $new_prog_entry->{$field} = $match_prog->{$field};
911                                # TODO (FUTURE): should we add to programme description to say where we got what data from?
912                            }
913                        }
914                    }
915                }
916
917                #
918                # 6.  write out new entry
919                #
920
921                printf "REC#6: writing out programme entry\n" if $recdebug;
922                &cleanup($new_prog_entry);
923
924                # scrub programme for known bogosities
925
926                # oztivo typically inserts blank 'director' details into 'credits' .. scrub them
927                if ((defined $new_prog_entry->{'credits'}) &&
928                    (defined $new_prog_entry->{'credits'}->{'director'}) &&
929                    (defined $new_prog_entry->{'credits'}->{'director'}->[0])) {
930                    my @director_list = $new_prog_entry->{'credits'}->{'director'}->[0];
931                    for my $i (0 .. $#director_list) {
932                        delete $new_prog_entry->{'credits'}->{'director'}->[$i] if ((defined $director_list[$i]) && ($director_list[$i] eq ""));
933                    }
934                }
935
936                # want to keep epoch start/stop for our own processing, but stop XMLTV whining about it in write_programme
937                # so temporarily remove them & reinsert them back afterwards
938                my ($orig_start_epoch,$orig_end_epoch) = ($new_prog_entry->{'start_epoch'},$new_prog_entry->{'stop_epoch'});
939                delete $new_prog_entry->{'start_epoch'};
940                delete $new_prog_entry->{'stop_epoch'};
941
942                # write out
943                $writer->write_programme($new_prog_entry);
944                ($new_prog_entry->{'start_epoch'},$new_prog_entry->{'stop_epoch'}) = ($orig_start_epoch,$orig_end_epoch);
945
946                # 7a. remove all programmes that end before this endtime
947                for (my $i=0; $i < $num_grabbers; $i++) {
948                    next if ($position_pointer[$i] == -1);
949                    while ((defined $proglist[$i]->[($position_pointer[$i])]) &&
950                           ($proglist[$i]->[($position_pointer[$i])]->{'channel'} eq $channels->{$ch}) &&
951                           ($proglist[$i]->[($position_pointer[$i])]->{'stop_epoch'} <= $stoppoint)) {
952                        printf "REC#7a: removing programme on grabber %d slot %d since it ends before inserted start: start %d end %s: \"%s\"\n",
953                            $i, $position_pointer[$i],
954                            $proglist[$i]->[($position_pointer[$i])]->{'start_epoch'},
955                            $proglist[$i]->[($position_pointer[$i])]->{'stop_epoch'},
956                            ${XMLTV::best_name($langs,$proglist[$i]->[($position_pointer[$i])]->{'title'})}[0] if $recdebug;
957                        delete $proglist[$i]->[($position_pointer[$i])];
958                        $position_pointer[$i]++;
959                    }
960                }
961
962                # 7b. adjust starttimes of any programmes to match endtime (with fuzz of +5 mins max)
963                for (my $i=0; $i < $num_grabbers; $i++) {
964                    next if ($position_pointer[$i] == -1);
965                    my $position_offset = 0;
966                    while ((defined $proglist[$i]->[($position_pointer[$i]+$position_offset)]) &&
967                           ($proglist[$i]->[($position_pointer[$i]+$position_offset)]->{'channel'} eq $channels->{$ch}) &&
968                           ($proglist[$i]->[($position_pointer[$i]+$position_offset)]->{'start_epoch'} < $stoppoint)) {
969                        my $this_prog = $proglist[$i]->[($position_pointer[$i]+$position_offset)];
970                        if (($this_prog->{'start_epoch'} + $reclogic{readjust_starttime_for_nonmatched_programmes}) >= $stoppoint) {
971                            printf "REC#7b: adjusting starttime on grabber %d slot %d since it starts <5mins before inserted end:\n",
972                                $i,($position_pointer[$i]+$position_offset) if $recdebug;
973                            printf "REC#7b:    orig: start %d end %d, now: start %d end %d: \"%s\"\n",
974                                $this_prog->{'start_epoch'}, $this_prog->{'stop_epoch'}, $stoppoint, $this_prog->{'stop_epoch'},
975                                ${XMLTV::best_name($langs,$this_prog->{'title'})}[0] if $recdebug;
976                            $proglist[$i]->[($position_pointer[$i]+$position_offset)]->{'start_epoch'} = $stoppoint;
977                            $proglist[$i]->[($position_pointer[$i]+$position_offset)]->{'start'} = sprintf "%s",(strftime "%Y%m%d%H%M", localtime($stoppoint));
978                            $position_offset++;
979                        } else {
980                            printf "REC#7b: removing grabber %d slot %d programme because it started too long before chosen programme: start %d end %d: \"%s\"\n",
981                                $i, ($position_pointer[$i]+$position_offset), $this_prog->{'start_epoch'}, $this_prog->{'stop_epoch'},
982                                ${XMLTV::best_name($langs,$this_prog->{'title'})}[0] if $recdebug;
983                            $position_pointer[$i]++;
984                        }
985                    }
986                }
987            }
988
989            # 8. check that we still have at least one pointer on current channel
990            for (my $i=0; $i < $num_grabbers; $i++) {
991                next if ($position_pointer[$i] == -1);
992                printf "REC#8: grabber %d is now at slot %d\n",$i,$position_pointer[$i] if $recdebug;
993                if ((defined $proglist[$i]->[($position_pointer[$i])]) &&
994                    ($proglist[$i]->[($position_pointer[$i])]->{'channel'} eq $channels->{$ch})) {
995                    $all_done_on_this_channel = 0;
996                    printf "REC#8:   grabber %d is at slot %d is still on channel \"%s\"\n",$i,$position_pointer[$i],$channels->{$ch} if $recdebug;
997                }
998            }
999            printf "REC#9:\n" if $recdebug;
1000        } until ($all_done_on_this_channel);
1001    }
1002    $writer->end();
1003}
1004
1005# sorting helper routine - sort by channel then by start-time
1006sub order_channel_time {
1007        my $chanresult = $a->{channel} cmp $b->{channel};
1008        return $chanresult if ($chanresult != 0);
1009        return ($a->{start_epoch} <=> $b->{start_epoch});
1010}
1011
1012# descend a structure and clean up various things, including stripping
1013# leading/trailing spaces in strings, translations of html stuff etc
1014#   -- taken & modified from Michael 'Immir' Smith's excellent tv_grab_au
1015my %amp;
1016BEGIN { %amp = ( nbsp => ' ', qw{ amp & lt < gt > apos ' quot " } ) }
1017
1018sub cleanup {
1019    my $x = shift;
1020    if    (ref $x eq "REF")   { cleanup($_) }
1021    elsif (ref $x eq "HASH")  { cleanup(\$_) for values %$x }
1022    elsif (ref $x eq "ARRAY") { cleanup(\$_) for @$x }
1023    elsif (defined $$x) {
1024        $$x =~ s/&(#(\d+)|(.*?));/ $2 ? chr($2) : $amp{$3}||' ' /eg; # scrub html
1025        # $$x =~ s/[^\x20-\x7f]/ /g;    # disabled (we want to keep non-std chars)
1026        $$x =~ s/(^\s+|\s+$)//g;        # strip leading/trailing spaces
1027    }
1028}
1029
1030# -----------------------------------------
1031# Subs: Postprocessing
1032# -----------------------------------------
1033
1034sub postprocess_data
1035{
1036    # for our first postprocessor, we feed it ALL of the XMLTV files we have
1037    # as each postprocessor runs, we feed in the output from the previous one
1038    # Shepherd checks the "completeness" of the data that comes out of a postprocessor & automatically
1039    # reverts back to the previous postprocessor if it was shown to be bad
1040
1041    # first time around: feed in reconciled data ($input_postprocess_file)
1042
1043    my $need_more_data;
1044
1045    printf "\nPostprocessing stage:\n";
1046
1047    foreach my $postprocessor (sort { $postprocessors->{$a} <=> $postprocessors->{$b} } keys %$postprocessors) {
1048        next if ($postprocessors->{$postprocessor}->{disabled});
1049
1050        $postprocessors->{$postprocessor}->{lastdata} = time;
1051        $postprocessors->{$postprocessor}->{laststatus} = "unknown";
1052
1053        printf "\nSHEPHERD: Using postprocessor: %s\n",$postprocessor;
1054
1055        my $output = "$POSTPROCESSOR_DIR/$postprocessor/output.xmltv";
1056        my $comm = "$POSTPROCESSOR_DIR/$postprocessor/$postprocessor " .
1057                   "--region $region " .
1058                   "--channels_file $channels_file " .
1059                   "--output $output";
1060        $comm .= " --days $days" if ($days);
1061        $comm .= " --offset $opt->{offset}" if ($opt->{offset});
1062        $comm .= " --debug" if ($debug);
1063        $comm .= " @ARGV" if (@ARGV);
1064        $comm .= " $input_postprocess_file";
1065        print "SHEPHERD: Excuting command: $comm\n";
1066
1067        chdir "$POSTPROCESSOR_DIR/$postprocessor/";
1068        system($comm);
1069        chdir $CWD;
1070
1071        #
1072        # soak up the data we just collected and check it
1073        # YES - these are the SAME routines we used in the previous 'grabber' phase
1074        # but the difference here is that we clear out our 'channel_data' beforehand
1075        # so we can independently analyze the impact of this postprocessor.
1076        # if it clearly returns bad data, don't use that data (go back one step) and
1077        # flag the postprocessor as having failed.  after 3 consecutive failures, disable it
1078        #
1079
1080        # clear out channel_data
1081        foreach my $ch (keys %{$channels}) {
1082            delete $channel_data->{$ch};
1083        }
1084
1085        # process and analyze it!
1086        &soak_up_data($postprocessor, $output, "postprocessor");
1087        $need_more_data = &analyze_plugin_data($postprocessor_ok_threshold_percent, "POSTPROCESSOR");
1088
1089        $postprocessors->{$postprocessor}->{laststatus} = $plugin_data->{$postprocessor}->{laststatus};
1090
1091        if (($need_more_data) && (!$insufficient_grabber_data)) {
1092            # urgh.  this postprocessor did a bad bad thing ...
1093            printf "SHEPHERD: XML data from postprocessor %s rejected, using XML from previous stage\n",$postprocessor;
1094
1095            if (defined $postprocessors->{$postprocessor}->{conescutive_failures}) {
1096                $postprocessors->{$postprocessor}->{conescutive_failures}++;
1097            } else {
1098                $postprocessors->{$postprocessor}->{conescutive_failures} = 1;
1099            }
1100            printf "SHEPHERD: Postprocessor \"%s\" has now failed %d times in a row.  %d more and it will be automatically disabled.\n",
1101                $postprocessor,
1102                $postprocessors->{$postprocessor}->{conescutive_failures},
1103                ($postprocessor_disable_failure_threshold - $postprocessors->{$postprocessor}->{conescutive_failures});
1104
1105            if ($postprocessors->{$postprocessor}->{conescutive_failures} >= $postprocessor_disable_failure_threshold) {
1106                printf "SHEPHERD: Disabling Postprocessor \"%s\".\n",$postprocessor;
1107                $postprocessors->{$postprocessor}->{disabled} = 1;
1108            }
1109        } else {
1110            # accept what this postprocessor did to our output ...
1111            printf "SHEPHERD: accepting output from postprocessor %s, feeding it into next stage\n",$postprocessor;
1112            $input_postprocess_file = $output;
1113            delete $postprocessors->{$postprocessor}->{conescutive_failures} if (defined $postprocessors->{$postprocessor}->{conescutive_failures});
1114        }
1115    }
1116}
1117
1118
1119sub output_data
1120{
1121    # $input_postprocess_file contains our final output
1122    # send it to whereever --output told us to!
1123
1124    my $output_filename = "$CWD/output.xmltv";
1125    $output_filename = $opt->{output} if ($opt->{output});
1126
1127    open(OUTFILE,">$output_filename") || die "could not open output file $output_filename for writing: $!\n";
1128
1129    if (!(open(INFILE,"<$input_postprocess_file"))) {
1130        printf "WARNING: could not open input file \"%s\": %s\n", $input_postprocess_file, $!;
1131        printf "Output XMLTV data may be damanged as a result!\n";
1132    } else {
1133        while (<INFILE>) {
1134            print OUTFILE $_;
1135        }
1136        close(INFILE);
1137        close(OUTFILE);
1138    }
1139
1140    printf "Final output stored in $output_filename.\n";
1141}
1142
1143# -----------------------------------------
1144# Subs: Updates & Installations
1145# -----------------------------------------
1146
1147sub update
1148{
1149    printf "\nChecking for updates:\n\n";
1150
1151    my $data;
1152    my $sites = "";
1153    $sites = "$mirror_site," if ($mirror_site);
1154    $sites .= $HOME;
1155
1156    foreach my $site (split(/,/,$sites)) {
1157        my $url = $site . "/status";
1158        print "Fetching status file: $url.\n";
1159        $data = LWP::Simple::get($url);
1160        last if $data;
1161
1162        print "Failed to retrieve status file from $url.\n";
1163    }
1164    return if (!$data);
1165
1166    my %glist = %$grabbers;
1167    my %plist = %$postprocessors;
1168    while ($data =~ /(.*):(.*):(.*)/g)
1169    {
1170        my ($proggy, $latestversion, $progtype) = ($1,$2,$3);
1171        update_component($proggy, $latestversion, $progtype);
1172        delete $glist{$proggy} if ($progtype eq "grabber");
1173        delete $plist{$proggy} if ($progtype eq "postprocessor");
1174    }
1175
1176    # work out what grabbers disappeared (if any)
1177    foreach (keys %glist) {
1178        unless ($grabbers->{$_}->{disabled}) {
1179            print "\nDeleted grabber: $_.\n";
1180            disable($_,"grabber");
1181            $made_changes = 1;
1182        }
1183    }
1184
1185    # work out what postprocessors disappeared (if any)
1186    foreach (keys %plist) {
1187        unless ($postprocessors->{$_}->{disabled}) {
1188            print "\nDeleted Postprocessor: $_.\n";
1189            disable($_,"postprocessor");
1190            $made_changes = 1;
1191        }
1192    }
1193}
1194
1195sub update_component
1196{
1197    my ($proggy, $latestversion, $progtype) = @_;
1198
1199    # handle new installs..
1200    if (($proggy eq $progname) && ($progtype eq "shepherd")) {
1201        # shepherd itself..
1202        if(! -e "$CWD/$progname") {
1203            print "Missing: $CWD/$progname\n";
1204            install($progname, $latestversion, $progtype);
1205            return;
1206        }
1207    } elsif ($progtype eq "grabber") {
1208        if (!defined $grabbers->{$proggy} or ! -e "$GRABBER_DIR/$proggy/$proggy") {
1209            print "New grabber: $proggy.\n";
1210            install($proggy, $latestversion, $progtype);
1211            return;
1212        }
1213        print "Warning: grabber $proggy disabled by config file.\n" if ($grabbers->{$proggy}->{disabled});
1214    } elsif ($progtype eq "postprocessor") {
1215        if (!defined $postprocessors->{$proggy} or ! -e "$POSTPROCESSOR_DIR/$proggy/$proggy") {
1216            print "New postprocessor: $proggy.\n";
1217            install($proggy, $latestversion, $progtype);
1218            return;
1219        }
1220        print "Warning: postprocessor $proggy disabled by config file.\n" if ($postprocessors->{$proggy}->{disabled});
1221    }
1222
1223    # upgrade/downgrades
1224    my $ver;
1225    if ($progtype eq "grabber") {
1226        $ver = ($proggy eq $progname ? $version : $grabbers->{$proggy}->{ver});
1227    } elsif ($progtype eq "postprocessor") {
1228        $ver = ($proggy eq $progname ? $version : $postprocessors->{$proggy}->{ver});
1229    } elsif (($proggy eq $progname) && ($progtype eq "shepherd")) {
1230        $ver = $version;
1231    } else {
1232        print "Warning: unknown type of programme: prog '$proggy' progtype '$progtype' not installed.\n";
1233        return;
1234    }
1235
1236    my $result = versioncmp($ver, $latestversion);
1237    if ($result == -1) {
1238        print "Upgrading $proggy from v$ver to v$latestversion.\n";
1239    } elsif ($result == 1) {
1240        print "Downgrading $proggy from v$ver to v$latestversion.\n";
1241    } else {
1242        print "Already have latest version of $proggy: v$ver.\n";
1243        return;
1244    }
1245    install($proggy, $latestversion, $progtype);
1246}
1247
1248sub install
1249{
1250    my ($proggy, $latestversion, $progtype) = @_;
1251
1252    print "Downloading $proggy v$latestversion.\n";
1253
1254    my $sites = "";
1255    $sites = "$mirror_site," if ($mirror_site);
1256    $sites .= $HOME;
1257
1258    my $rdir = "";
1259    my $ldir = $CWD;
1260    my $ver = "unknown";
1261
1262    if (($proggy eq $progname) && ($progtype eq "shepherd")) {
1263        $ver = $version;
1264    } elsif ($progtype eq "grabber") {
1265        $rdir = "grabbers";
1266        $ldir = "$GRABBER_DIR/$proggy";
1267        $ver = $grabbers->{$proggy}->{ver} if ((defined $grabbers->{$proggy}) && $grabbers->{$proggy}->{ver});
1268        -d $GRABBER_DIR or mkdir $GRABBER_DIR or die "Cannot create directory $GRABBER_DIR: $!";
1269    } elsif ($progtype eq "postprocessor") {
1270        $rdir = "postprocessors";
1271        $ldir = "$POSTPROCESSOR_DIR/$proggy";
1272        $ver = $postprocessors->{$proggy}->{ver} if ((defined $postprocessors->{$proggy}) && $postprocessors->{$proggy}->{ver});
1273        -d $POSTPROCESSOR_DIR or mkdir $POSTPROCESSOR_DIR or die "Cannot create directory $POSTPROCESSOR_DIR: $!";
1274    } else {
1275        print "Warning: unknown type of programme: prog '$proggy' progtype '$progtype' not installed.\n";
1276        return;
1277    }
1278    -d $ldir or mkdir $ldir or die "Cannot create directory $ldir: $!";
1279
1280    my $newfile = "$ldir/$proggy-$latestversion";
1281    my $rc;
1282
1283    foreach my $site (split(/,/,$sites)) {
1284        printf "Fetching $site/$rdir/$proggy v$latestversion.\n";
1285        $rc = LWP::Simple::getstore("$site/$rdir/$proggy", $newfile);
1286        last if (is_success($rc));
1287
1288        print "Failed to retrieve $site/$rdir/$proggy.\n";
1289    }
1290    return if (!is_success($rc));
1291
1292    # Make it executable
1293    system('chmod u+x ' . $newfile);
1294
1295    -d $ARCHIVE_DIR or mkdir $ARCHIVE_DIR or die "Cannot create directory $ARCHIVE_DIR: $!";
1296
1297    if (-e "$ldir/$proggy")
1298    {
1299        rename("$ldir/$proggy", "$ARCHIVE_DIR/$proggy-$ver");
1300    }
1301    rename($newfile, "$ldir/$proggy");
1302   
1303    print "Installed $proggy v$latestversion.\n" if ($debug);
1304
1305    # if the update was for shepherd itself, restart it
1306    if (($proggy eq $progname) && ($progtype eq "shepherd")) {
1307        print "\n*** Restarting ***\n\n";
1308        exec("$ldir/$proggy");
1309        # This exits.
1310    }
1311
1312    print "Testing $proggy...\n" if ($debug);
1313    my $result = test_proggy($ldir,"$ldir/$proggy");
1314
1315    if ($progtype eq "grabber") {
1316        $grabbers->{$proggy}->{ver} = $latestversion;
1317        $grabbers->{$proggy}->{ready} = $result;
1318        $grabbers->{$proggy}->{laststatus} = sprintf "updated to %s on %s", $latestversion, (strftime "%a%d%b%y",localtime(time));
1319    } elsif ($progtype eq "postprocessor") {
1320        $postprocessors->{$proggy}->{ver} = $latestversion;
1321        $postprocessors->{$proggy}->{ready} = $result;
1322        $postprocessors->{$proggy}->{laststatus} = sprintf "updated to %s on %s", $latestversion, (strftime "%a%d%b%y",localtime(time));
1323    }
1324
1325    $made_changes = 1;
1326}
1327
1328sub test_proggy
1329{
1330    my ($testdir,$proggyexec) = @_;
1331
1332    chdir($testdir);
1333    system("$proggyexec --ready");
1334    chdir ($CWD);
1335
1336    my $result = $?;
1337    print "Return value: $result\n" if ($debug);
1338
1339    print "\nprogramme $proggyexec did not exit cleanly!\n" .
1340         "It may require configuration.\n\n" if ($result);
1341    return !$result;
1342}
1343
1344sub enable
1345{
1346    my $proggy = shift;
1347
1348    # confirm it exists first
1349    if ((!$grabbers->{$proggy}) && (!$postprocessors->{$proggy})) {
1350        printf "No such grabber/postprocessor: \"%s\".\n",$proggy;
1351        return;
1352    }
1353    print "Enabling $proggy.\n";
1354
1355    if ($grabbers->{$proggy}) {
1356        delete $grabbers->{$proggy}->{disabled};
1357        $grabbers->{$proggy}->{laststatus} = sprintf "enabled on %s, not run yet",(strftime "%a%d%b%y", localtime(time));
1358    } elsif ($postprocessors->{$proggy}) {
1359        delete $postprocessors->{$proggy}->{disabled};
1360        $postprocessors->{$proggy}->{laststatus} = sprintf "enabled on %s, not run yet",(strftime "%a%d%b%y", localtime(time));
1361    }
1362    $made_changes = 1;
1363}
1364
1365sub disable
1366{
1367    my $proggy = shift;
1368
1369    # confirm it exists first
1370    if ((!$grabbers->{$proggy}) && (!$postprocessors->{$proggy})) {
1371        printf "No such grabber/postprocessor: \"%s\".\n",$proggy;
1372        return;
1373    }
1374    print "Disabling $proggy.\n";
1375
1376    if ($grabbers->{$proggy}) {
1377        $grabbers->{$proggy}->{disabled} = 1;
1378        $grabbers->{$proggy}->{laststatus} = sprintf "manually disabled on %s",(strftime "%a%d%b%y", localtime(time));
1379    } elsif ($postprocessors->{$proggy}) {
1380        $postprocessors->{$proggy}->{disabled} = 1;
1381        $postprocessors->{$proggy}->{laststatus} = sprintf "manually disabled on %s",(strftime "%a%d%b%y", localtime(time));
1382    }
1383    $made_changes = 1;
1384}
1385
1386sub set_order
1387{
1388    my ($quiet,$order) = @_;
1389    $pref_order = $order if ($order);
1390
1391    # reset current order to zero
1392    foreach my $proggy (keys %$grabbers) {
1393        $grabbers->{$proggy}->{order} = 0;
1394    }
1395
1396    # and now set order
1397    my $order_num = 1;
1398    if ($pref_order) {
1399        foreach my $proggy (split(/,/,$pref_order)) {
1400            if (defined $grabbers->{$proggy}) {
1401                $grabbers->{$proggy}->{order} = $order_num;
1402                $order_num++;
1403            }
1404        }
1405    }
1406
1407    # set order of any grabbers not specified in a random manner
1408    foreach my $proggy (sort keys %$grabbers) {
1409        if ((!defined $grabbers->{$proggy}->{order}) || ($grabbers->{$proggy}->{order} == 0)) {
1410            $grabbers->{$proggy}->{order} = $order_num+int(rand(1000));
1411        }
1412    }
1413
1414    # .. and finally normalize the order (& show the user the order we chose)
1415    print "Grabber order set as follows:\n" unless $quiet;
1416    $order_num = 0;
1417    foreach my $proggy (sort { $grabbers->{$a}->{order} <=> $grabbers->{$b}->{order} } keys %$grabbers) {
1418        $order_num++;
1419        $grabbers->{$proggy}->{order} = $order_num;
1420        printf " #%d. %s%s\n",$grabbers->{$proggy}->{order},$proggy,($grabbers->{$proggy}->{disabled} ? " [disabled]" : "") unless $quiet;
1421    }
1422
1423    $made_changes = 1;
1424}
1425
1426sub check
1427{
1428    my $result;
1429    foreach my $proggy (keys %$grabbers) {
1430        $result = test_proggy("$GRABBER_DIR/$proggy","$GRABBER_DIR/$proggy/$proggy");
1431        printf "Grabber %s: %s\n",$proggy,($result ? "OK" : "Failed");
1432        if (!$result ne !$grabbers->{$proggy}->{ready}) {
1433            $grabbers->{$proggy}->{ready} = $result;
1434            $made_changes = 1;
1435        }
1436    }
1437
1438    foreach my $proggy (keys %$postprocessors) {
1439        $result = test_proggy("$POSTPROCESSOR_DIR/$proggy","$POSTPROCESSOR_DIR/$proggy/$proggy");
1440        printf "Postprocessor %s: %s\n",$proggy,($result ? "OK" : "Failed");
1441        if (!$result ne !$postprocessors->{$proggy}->{ready}) {
1442            $postprocessors->{$proggy}->{ready} = $result;
1443            $made_changes = 1;
1444        }
1445    }
1446}
1447
1448# -----------------------------------------
1449# Subs: Setup
1450# -----------------------------------------
1451
1452sub read_config_file
1453{
1454    read_file($config_file, 'configuration');
1455
1456    # if we are updating from a previous rev of shepherd.conf we may not
1457    # have any 'order' fields set .. check here
1458    my $found_order = 1;
1459    foreach (keys %$grabbers)
1460    {
1461        $found_order = 0 if (!defined $grabbers->{$_}->{order});
1462    }
1463    if (($found_order == 0) && (!$opt->{setorder}))
1464    {
1465        # at least one 'order' was missing .. we need to put it in!
1466        printf "Legacy shepherd.conf file didn't contain any grabber order! Automatically updating using a random order, use --setorder to manually set this if you care.\n";
1467        &set_order(1);
1468    }
1469
1470    # if a mirror has been specified, add it into our config
1471    if ($opt->{mirror}) {
1472        $mirror_site = $opt->{mirror};
1473        $made_changes = 1;
1474        print "Adding mirror: $mirror_site\n";
1475    }
1476}
1477
1478sub read_channels_file
1479{
1480    read_file($channels_file, 'channels');
1481}
1482
1483sub read_file
1484{
1485    my $fn = shift;
1486    my $name = shift;
1487
1488    print "Reading $name file: $fn\n";
1489    unless (-r $fn)
1490    {
1491        unless ($opt->{configure})
1492        {
1493            print "\nNo $name file found.\n" .
1494                  ucfirst($progname) . " must be configured: " .
1495                  "configuring now.\n\n";
1496            $opt->{'configure'} = 1;
1497        }
1498        return;
1499    }
1500    local (@ARGV, $/) = ($fn);
1501    no warnings 'all';
1502    eval <>;
1503    if ($@ and !$opt->{configure})
1504    {
1505        die "\nError in $name file!\nDetails:\n$@";
1506    }
1507}
1508
1509sub write_config_file
1510{
1511    open(CONF, ">$config_file") or die "cannot write to $config_file: $!";
1512    print CONF Data::Dumper->Dump(
1513        [$region,  $pref_order,  $mirror_site,  $grabbers,  $postprocessors,  $title_translation_table  ],
1514        ["region", "pref_order", "mirror_site", "grabbers", "postprocessors", "title_translation_table" ]);
1515    close CONF;
1516    print "\nUpdated configuration file $config_file.\n" if ($debug);
1517}
1518
1519sub write_channels_file
1520{
1521    open(CHAN, ">$channels_file") or die "cannot write to $channels_file: $!";
1522    print CHAN Data::Dumper->Dump([$channels], ["channels"]);
1523    close CHAN;
1524    print "Updated channels file $channels_file.\n" if ($debug);
1525}
1526
1527sub get_initial_command_line_options
1528{
1529  GetOptions( 'config-file=s'   => \$opt->{configfile},
1530              'help'            => \$opt->{help},
1531              'configure'       => \$opt->{configure},
1532              'mirror=s'        => \$opt->{mirror},
1533              'dontcallgrabbers' => \$opt->{dontcallgrabbers},
1534              'debug'           => \$debug);
1535}
1536
1537sub get_remaining_command_line_options
1538{
1539    GetOptions(
1540              'version'         => \$opt->{status},
1541              'status'          => \$opt->{status},
1542              'list'            => \$opt->{list},
1543              'show-config'     => \$opt->{show_config},
1544
1545              'update'          => \$opt->{update},
1546              'noupdate'        => \$opt->{noupdate},
1547
1548              'disable=s'       => \$opt->{disable},
1549              'enable=s'        => \$opt->{enable},
1550              'setorder=s'      => \$opt->{setorder},
1551
1552              'days=i'          => \$days,
1553              'offset=i'        => \$opt->{offset},
1554              'show-channels'   => \$opt->{show_channels},
1555              'output=s'        => \$opt->{output},
1556              'check'           => \$opt->{check}
1557            );
1558}
1559
1560
1561# -----------------------------------------
1562# Subs: Configuration
1563# -----------------------------------------
1564
1565sub configure
1566{
1567    my $REGIONS = {
1568        "ACT" => 126,
1569        "NSW: Sydney" => 73,
1570        "NSW: Newcastle" => 184,
1571        "NSW: Central Coast" => 66,
1572        "NSW: Griffith" => 67,
1573        "NSW: Broken Hill" => 63,
1574        "NSW: Northern NSW" => 69,
1575        "NSW: Southern NSW" => 71,
1576        "NSW: Remote and Central" => 106,
1577        "NT: Darwin" => 74,
1578        "NT: Remote & Central" => 108,
1579        "QLD: Brisbane" => 75,
1580        "QLD: Gold Coast" => 78,
1581        "QLD: Regional" => 79,
1582        "QLD: Remote & Central" => 114,
1583        "SA: Adelaide" => 81,
1584        "SA: Renmark" => 82,
1585        "SA: Riverland" => 83,
1586        "SA: South East SA" => 85,
1587        "SA: Spencer Gulf" => 86,
1588        "SA: Remote & Central" => 107,
1589        "Tasmania" => 88,
1590        "VIC: Melbourne" => 94,
1591        "VIC: Geelong" => 93,
1592        "VIC: Eastern Victoria" => 90,
1593        "VIC: Mildura/Sunraysia" => 95,
1594        "VIC: Western Victoria" => 98,
1595        "WA: Perth" => 101,
1596        "WA: Regional" => 102
1597    };
1598
1599    print "\nConfiguring.\n\n" .
1600          "Select your region:\n";
1601    foreach (sort keys %$REGIONS)
1602    {
1603        printf(" (%3d) %s\n", $REGIONS->{$_}, $_);
1604    }
1605    $region = ask_choice("Enter region code:", "94", values %$REGIONS);
1606
1607    print "\nFetching channel information... ";
1608
1609    my @channellist = get_channels();
1610
1611    print "done.\n\n" .
1612          "For each channel you want guide data for, enter an XMLTV id\n" .
1613          "of your choice (e.g. \"seven.free.au\"). If you don't need\n" .
1614          "guide data for this channel, just press Enter.\n\n" .
1615          "Please don't subscribe to unneeded channels.\n\nChannels:\n";
1616    $channels = {};
1617    my $line;
1618    foreach (@channellist)
1619    {
1620        $line = ask(" \"$_\"? ");
1621        $channels->{$_} = $line if ($line);
1622    }
1623
1624
1625    print "\nRandomly selecting grabber order.\n\n";
1626    set_order(0);
1627
1628    show_channels();
1629    unless(ask_boolean("\nCreate configuration file?"))
1630    {
1631        print "Aborting configuration.\n";
1632        exit 0;
1633    }
1634
1635    write_config_file();
1636    write_channels_file();
1637
1638    print "Finished configuring.\n\n" .
1639          "Shepherd is installed into $CWD.\n\n";
1640   
1641    if ($invoked ne "$CWD/$progname" and $invoked =~ /$progname/)
1642    {
1643        print "Warning: you invoked this program as $invoked.\n" .
1644            "In the future, it should be run as $CWD/$progname,\n" .
1645            "to avoid constantly re-downloading the latest version.\n\n" .
1646            "MythTV users may wish to create the following symlink, by " .
1647            "doing this (as root):\n" .
1648            "\"ln -s $CWD/$progname /usr/bin/tv_grab_au\".\n\n" .
1649            "You may safely delete $invoked.\n\n";
1650    }
1651
1652    status();
1653
1654    unless (ask_boolean("\nGrab data now?"))
1655    {
1656        exit 0;
1657    }
1658}
1659
1660sub get_channels
1661{
1662    my @date = localtime;
1663    my $page = LWP::Simple::get(
1664        "http://au.tv.yahoo.com/results.html?rg=$region&dt=" .
1665        ($date[5] + 1900) . "-$date[4]-$date[3]");
1666    my @channellist;
1667    while ($page =~ /<tr class=rtb><td class=rth><a .*?>(.*?)<\/a>/g)
1668    {
1669        push @channellist, $1;
1670    }
1671    return @channellist;
1672}
1673
1674# -----------------------------------------
1675# Subs: Status & Help
1676# -----------------------------------------
1677
1678sub show_config
1679{
1680    print "\nConfiguration\n".
1681          "-------------\n" .
1682          "Config file: $config_file\n" .
1683          "Debug mode : " . is_set($debug) . "\n" .
1684          "Output file: " . ($opt->{output} ? $opt->{output} : "None") . "\n" .
1685          "Region ID  : $region\n";
1686  show_channels();
1687  print "\n";
1688  status();
1689  print "\n";
1690}
1691
1692sub show_channels
1693{
1694  print "Subscribed channels:\n";
1695  print "    $_ -> $channels->{$_}\n" for sort keys %$channels;
1696}
1697
1698sub is_set
1699{
1700    my $arg = shift;
1701    return $arg ? "Yes" : "No";
1702}
1703
1704sub status
1705{
1706    print " Grabber           Version Enabled Ready Last Run   Status\n" .
1707          " ----------------- ------- ------- ----- ---------- ---------------------------\n";
1708    foreach (sort { $grabbers->{$a}->{order} <=> $grabbers->{$b}->{order} } keys %$grabbers) {
1709        my $h = $grabbers->{$_};
1710        printf  " %-16s %8s %4s %6s  %11s %s\n",
1711                "$h->{order}. $_",
1712                ($h->{ver} ? $h->{ver} : "unknown"),
1713                $h->{disabled} ? '' : 'Y',
1714                $h->{ready} ? 'Y' : '',
1715                $h->{lastdata} ? (strftime "%a%d%b%y", localtime($h->{lastdata})) : 'never',
1716                $h->{laststatus} ? $h->{laststatus} : '';
1717    }
1718    printf "Grabbers shown in order of preference.\n\n";
1719
1720    print " Postprocessor     Version Enabled Ready Last Run   Status\n" .
1721          " ----------------- ------- ------- ----- ---------- ---------------------------\n";
1722    foreach (sort { $postprocessors->{$a} <=> $postprocessors->{$b} } keys %$postprocessors) {
1723        my $h = $postprocessors->{$_};
1724        printf  " %-16s %8s %4s %6s  %11s %s\n",
1725                $_,
1726                ($h->{ver} ? $h->{ver} : "unknown"),
1727                $h->{disabled} ? '' : 'Y',
1728                $h->{ready} ? 'Y' : '',
1729                $h->{lastdata} ? (strftime "%a%d%b%y", localtime($h->{lastdata})) : 'never',
1730                $h->{laststatus} ? $h->{laststatus} : '';
1731    }
1732    printf "Postprocessors shown in order of execution.\n\n";
1733}
1734
1735sub help
1736{
1737    print q{
1738Command-line options:
1739    --help                Print this message
1740
1741    --status              Print a list of grabbers maintained
1742    --list                Print a detailed list of grabbers
1743    --mirror <s>          Set URL <s> as primary location to check for updates
1744
1745    --configure           Setup
1746    --show-config         Print setup details
1747
1748    --setorder <s>        Set order of grabbers to <s> (comma-seperated list of grabbers)
1749
1750    --disable <s>         Don't ever use grabber/postprocessor <s>
1751    --enable <s>          Okay, maybe use it again then
1752    --uninstall <s>       Remove a disabled grabber/postprocessor
1753
1754    --noupdate            Do not attempt to update before running
1755    --update              Update only; do not grab data
1756
1757    --check               Check status of all grabbers and postprocessors
1758};
1759    exit 0;
1760}
1761
1762# -----------------------------------------
1763# Subs: override handlers for standard perl.
1764# -----------------------------------------
1765
1766# ugly hack. please don't try this at home kids!
1767sub my_die {
1768    my ($arg,@rest) = @_;
1769    my ($pack,$file,$line,$sub) = caller(1);
1770
1771    # check if we are in an eval()
1772    if ($^S) {
1773        printf STDERR "  shepherd caught a die() within eval{} from file $file line $line\n";
1774    } else {
1775        if (!ref($arg)) {
1776            printf STDERR "DIE at line %d in file %s\n",$line,$file;
1777            CORE::die(join("",@rest));
1778        } else {
1779            CORE::die($arg,@rest);
1780        }
1781    }
1782}
Note: See TracBrowser for help on using the browser.