root/shepherd @ 96

Revision 96, 51.5 kB (checked in by max, 7 years ago)

More intelli-random grabber ordering: keeps track of what C1 grabbers have cached and favors that

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