root/shepherd @ 91

Revision 91, 48.6 kB (checked in by max, 7 years ago)

Bugfix: When auto-restarting Shepherd, run with original options

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