root/shepherd @ 94

Revision 94, 48.8 kB (checked in by max, 7 years ago)

Oops, bugfix.

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