root/shepherd @ 168

Revision 168, 67.2 kB (checked in by max, 7 years ago)

Removed unnecessary logging.

Line 
1#!/usr/bin/perl -w
2
3# "Shepherd"
4
5my $version = '0.2.29';
6
7# A wrapper for various Aussie TV guide data grabbers
8#
9# Use --help for command-line options.
10# See shepherd.txt for details.
11#
12#  A current version of this script, plus a README file, might be here:
13#  http://www.whuffy.com/tv_grab_au/
14#
15# Changelog:
16# 0.1.0   : Basic self-updating and grabber management
17# 0.2.0   : --configure
18# 0.2.1   : Has a home in ~/.shepherd/
19# 0.2.2   : --check
20# 0.2.3   : Bugfix: archives correctly
21# 0.2.5   : Multi-grabber (potentially with partial data)
22# 0.2.6   : Postprocessor support
23# 0.2.7   : Changed online file structure
24# 0.2.8   : Integrated reconciler
25# 0.2.9   : Grabber config support
26# 0.2.10  : Bugfix: don't call postprocessors that aren't ready,
27#           rework accept-data-or-not postprocessor logic
28# 0.2.11  : Dedicated external reconciler support
29# 0.2.13  : revert 'alawys run' added in 0.2.12, --setorder bugfix
30# 0.2.14  : Changed online status file format
31# 0.2.15  : Intelli-random grabber ordering now kinda works
32# 0.2.16  : config logic for HD channels
33# 0.2.17  : bugfix timezone bogosities
34# 0.2.18  : care less about missing data in early-morning/overnight
35#           care more about missing data in evening/night
36# 0.2.22  : remove ->{order}, order is now set by quality
37#           explicitly tell reconciler the preferred _title_ source
38# 0.2.24  : logging and log files
39# 0.2.25  : use open-with-pipe rather than system() and look at
40#           return codes from called programmes
41# 0.2.27  : Identify self in useragent when fetching shepherd files
42# 0.2.28  : Changing status file format again
43
44BEGIN { *CORE::GLOBAL::die = \&my_die; }
45
46use strict;
47no strict 'refs';
48
49use LWP::UserAgent;
50use Sort::Versions;
51use Cwd;
52use Getopt::Long;
53use Data::Dumper;
54use XMLTV;
55use XMLTV::Ask;
56use POSIX qw(strftime mktime);
57use Date::Manip;
58use Algorithm::Diff;
59use List::Compare;
60use Compress::Zlib;
61
62# ---------------------------------------------------------------------------
63# --- Global Variables
64# ---------------------------------------------------------------------------
65
66my $progname = 'shepherd';
67
68my $HOME = 'http://www.whuffy.com/shepherd';
69
70my $invoked = Cwd::realpath($0);
71my @options = @ARGV;
72
73# By default, Shepherd runs from ~/.shepherd/. If it's not run as a user,
74# it will try /opt/shepherd/ instead.
75my $CWD = ($ENV{HOME} ? $ENV{HOME} . "/." : "/opt/") . $progname;
76-d $CWD or mkdir $CWD or die "Cannot create directory $CWD: $!";
77chdir($CWD);
78
79my $ARCHIVE_DIR = "$CWD/archive";
80
81
82#### analyzer settings ####
83# the following thresholds are used to control whether we keep calling grabbers or
84# not.
85
86my %policy;
87$policy{timeslot_size} = (5 * 60);      # 5 minute slots
88
89# PEAK timeslots -
90#  between 4.30pm and 11.30pm every day, only allow a maximum of
91#  15 minutes "programming data" missing
92#  if there is more than this, we will continue asking grabbers for more
93#  programming on this channel
94$policy{peak_max_missing} = 15*60;              # up to 15 mins max allowed missing
95$policy{peak_start} = (16*(60*60))+(30*60);     # 4.30pm
96$policy{peak_stop} = (23*(60*60))+(30*60);      # 11.30pm
97
98# NON-PEAK timeslots -
99#  between midnight and 6.15am every day, only allow up to 6 hours missing
100#  if there is more than this, we will continue asking grabbers for more
101#  programming on this channel
102$policy{nonpeak_max_missing} = 6*(60*60);       # up to 6 hours can be missing
103$policy{nonpeak_start} = 0;                     # midnight
104$policy{nonpeak_stop} = (6*(60*60))+(15*60);    # 6.15am
105
106# all other timeslots - (6.15am-4.30pm, 11.30pm-midnight)
107#  allow up to 60 minutes maximum missing programming
108$policy{other_max_missing} = 60*60;             # up to 60 mins max allowed missing
109
110# if a postprocessor failed 5 times in a row, automatically disable it
111$policy{postprocessor_disable_failure_threshold} = 5;
112
113#### end analyzer section ####
114
115my $opt;
116my $pref_title_source;
117my $mirror_site;
118my $debug = 0;
119my $components = { };
120my $gscore;
121my $region;
122my $channels;
123my $opt_channels;
124my $config_file =   "$CWD/$progname.conf";
125my $channels_file = "$CWD/channels.conf";
126my $log_file = "$CWD/$progname.log";
127my $days = 7;
128my $missing;
129my $timeslice;
130my $grabbed;
131my $gmt_offset;
132my $grabber_found_all_data;
133
134# postprocessing
135my $langs = [ 'en' ];
136my $plugin_data = { };
137my $channel_data = { };
138my $reconciler_found_all_data;
139my $input_postprocess_file = "";
140
141# OBSOLETE: will be removed
142my $preferred;
143my $title_translation_table;
144my $pref_order;
145
146# ---------------------------------------------------------------------------
147# --- Setup
148# ---------------------------------------------------------------------------
149
150$| = 1;
151
152print ucfirst($progname) . " v$version\n\n";
153
154# Any options Shepherd doesn't understand, we'll pass to the grabber(s)
155Getopt::Long::Configure(qw/pass_through/);
156
157&get_initial_command_line_options;
158
159&help if ($opt->{help});
160
161&read_config_file;
162&read_channels_file;
163
164&get_remaining_command_line_options;
165
166if ($opt->{status})
167{
168    &status;
169    exit;
170}
171
172if ($opt->{show_config})
173{
174    &show_config;
175    exit;
176}
177
178&open_logfile unless ($opt->{nolog});
179
180&process_setup_commands;
181
182# ---------------------------------------------------------------------------
183# --- Update
184# ---------------------------------------------------------------------------
185
186unless ($opt->{noupdate})
187{
188    if (&update($progname, $version) and !$opt->{configure}) 
189    {
190        &write_config_file;
191    }
192}
193
194if ($opt->{configure})
195{
196    &configure;
197}
198
199# ---------------------------------------------------------------------------
200# --- Go!
201# ---------------------------------------------------------------------------
202
203unless ($opt->{update})
204{
205    calc_date_range();
206    grab_data();
207    reconcile_data();
208    postprocess_data();
209    output_data();
210    write_config_file();
211}
212
213&log("Done.\n");
214&close_logfile() unless $opt->{nolog};
215
216# ---------------------------------------------------------------------------
217# --- Subroutines
218# ---------------------------------------------------------------------------
219
220# -----------------------------------------
221# Subs: Grabbing
222# -----------------------------------------
223
224sub grab_data
225{
226    my $used_grabbers = 0;
227
228    &log("\nGrabber stage.\n");
229
230    &analyze_plugin_data("",1);   
231
232    while (my $grabber = choose_grabber())
233    {
234        $grabber_found_all_data = 0;
235        $used_grabbers++;
236
237        $components->{$grabber}->{lastdata} = time;
238        $components->{$grabber}->{laststatus} = "unknown";
239
240        &log((sprintf "\nSHEPHERD: Using grabber: (%d) %s\n", $used_grabbers, $grabber));
241
242        my $output = "$CWD/grabbers/$grabber/output.xmltv";
243
244        my $comm = "$CWD/grabbers/$grabber/$grabber " .
245                   "--region $region " .
246                   "--output $output";
247
248        # Category 1 grabbers (i.e. slow ones) are requested to only fetch the timeslice
249        # that we need. Category 2 grabbers are requested to get everything, since there's
250        # very little cost in grabbing that extra data, and we can use it in the reconciler
251        # to verify that everything looks OK.
252        if (query_config($grabber, 'category') == 1)
253        {
254            &log("$grabber is Category 1: grabbing timeslice.\n") if ($debug);
255
256            record_requested_chandays($grabber, $timeslice);
257
258            if ($timeslice->{start} != 0)
259            {
260                $comm .= " " . 
261                         query_config($grabber, 'option_days_offset') .
262                         " " .
263                         $timeslice->{start};
264            }
265
266            my $n = $timeslice->{stop} + 1;
267            if ($timeslice->{start} != 0 
268                    and 
269                !query_config($grabber, 'option_offset_eats_days'))
270            {
271                $n -= $timeslice->{start};
272            }
273            $comm .= " " .
274                     query_config($grabber, 'option_days') .
275                     " " . 
276                     $n;
277           
278            # Write a temporary channels file specifying only the channels we want
279            my $tmpchans;
280            foreach (@{$timeslice->{chans}})
281            {
282                $tmpchans->{$_} = $channels->{$_};
283            }
284            my $tmpcf = "$CWD/channels.conf.tmp";
285            write_file($tmpcf, 'temporary channels', [ $tmpchans ], [ 'channels' ]);
286            $comm .= " --channels_file $tmpcf";
287        }
288        else
289        {
290            &log("$grabber is category 2: grabbing everything.\n") if ($debug);
291            $comm .= " --days $days" if ($days);
292            $comm .= " --offset $opt->{offset}" if ($opt->{offset});
293            $comm .= " --channels_file $channels_file";
294        }
295        $comm .= " --debug" if ($debug);
296        $comm .= " @ARGV" if (@ARGV);
297
298        my $retval = 0;
299        if ((defined $opt->{dontcallgrabbers}) && ($opt->{dontcallgrabbers})) {
300            &log("SHEPHERD: not calling grabber because of --dontcallgrabbers option, but will instead use existing $output\n");
301            &log("SHEPHERD: would have called: $comm\n") if ($debug);
302        } else {
303            &log("SHEPHERD: Excuting command: $comm\n");
304            chdir "$CWD/grabbers/$grabber/";
305            $retval = call_prog($comm);
306            chdir $CWD;
307        }
308
309        if ($retval != 0) {
310            &log("grabber returned with non-zero return code $retval: assuming it failed.\n");
311            next;
312        }
313
314        # soak up the data we just collected
315        &soak_up_data($grabber, $output, "grabber");
316        $components->{$grabber}->{laststatus} = $plugin_data->{$grabber}->{laststatus};
317
318        # check to see if we have all the data we want
319        $grabber_found_all_data = &analyze_plugin_data("analysis of all grabbers so far");
320
321        # Record what we grabbed from cacheable C1 grabbers
322        if (query_config($grabber, 'category') == 1 and query_config($grabber, 'cache'))
323        {
324            my $missing_before = convert_dayhash_to_list($missing);
325            my $missing_after = convert_dayhash_to_list(detect_missing_data());
326            my $list = List::Compare->new($missing_before, $missing_after);
327            my @grabbed = $list->get_symmetric_difference();
328            &log("Grabbed: " . join (', ', @grabbed) . ".\n") if ($debug);
329            record_cached($grabber, @grabbed);
330            write_config_file();
331        }
332
333        last if ($grabber_found_all_data);
334    }
335
336
337    if ($used_grabbers == 0)
338    {
339        &log("No valid grabbers installed/enabled!\n");
340        return;
341    }
342
343    unless ($grabber_found_all_data)
344    {
345        &log("SHEPHERD: Ran through ALL grabbers but still missing data!!! :(\n");
346        return;
347    }
348}
349
350# -----------------------------------------
351# Subs: Intelli-random grabber selection
352# -----------------------------------------
353
354sub choose_grabber
355{
356    if (defined $gscore)        # Reset score hash
357    {
358        foreach (keys %$gscore)
359        {
360            $gscore->{$_} = 0;
361        }
362    }
363    else                        # Create score hash
364    {
365        foreach (query_grabbers())
366        {
367            unless ($components->{$_}->{disabled})
368            {
369                $gscore->{$_} = 0;
370                if (query_config($_, 'category') == 1 and query_config($_, 'cache'))
371                {
372                    $gscore->{$_ . ' [cache]'} = 0;
373                }
374            }
375        }
376    }
377
378    $missing = detect_missing_data();
379    $timeslice = find_best_timeslice();
380
381    if ($debug)
382    {
383        &log((sprintf "Best timeslice: day%s of channels %s (%d chandays).\n",
384                    ($timeslice->{start} == $timeslice->{stop} ?
385                        " $timeslice->{start}" :
386                        "s $timeslice->{start} - $timeslice->{stop}"),
387                    join(', ', @{$timeslice->{chans}}),
388                    $timeslice->{chandays}));
389    }
390
391    my $total = score_grabbers();
392 
393    if ($debug)
394    {
395        &log("Grabber selection:\n");
396        foreach (sort { $gscore->{$b} <=> $gscore->{$a} } keys %$gscore)
397        {
398            next if ($_ =~ /\[cache\]/);
399
400            my $score  = $gscore->{$_};
401            my $cscore = $gscore->{"$_ [cache]"};
402            my $cstr   = $cscore ? "(inc. $cscore cache pts)" : "";
403
404            if ($opt->{randomize})
405            {
406                &log((sprintf "%15s %6.1f%% %9s %s\n", 
407                        $_, 
408                        ($total ? 100* $score / $total : 0), 
409                        "$score pts",
410                        $cstr));
411            }
412            else
413            {
414                &log((sprintf   "%15s %4s pts %s\n", 
415                        $_, 
416                        $score,
417                        $cstr));
418            }
419        }
420    }
421
422    return undef unless ($total);
423
424    # Select a grabber
425    #
426    # Either do it randomly based on scores, or just return the
427    # highest-scoring grabber, depending on whether --randomize has
428    # been used.
429
430    my $r = int(rand($total));
431    my $c = 0;
432    my $best;
433
434    foreach my $grabber (keys %$gscore)
435    {
436        next if (!$gscore->{$grabber} or $grabber =~ /\[cache\]/);
437        if ($opt->{randomize})
438        {
439            if ($r >= $c and $r < ($c + $gscore->{$grabber}))
440            {
441                return select_grabber($grabber, $gscore);
442            }
443            $c += $gscore->{$grabber};
444        }
445        else
446        {
447            if (!$best or $gscore->{$grabber} > $gscore->{$best})
448            {
449                $best = $grabber;
450            }
451        }
452    }
453
454    if ($opt->{randomize} or !$best)
455    {
456        die "ERROR: failed to choose grabber.";
457    }
458    return select_grabber($best, $gscore);
459}
460
461sub select_grabber
462{
463    my ($grabber, $gscore) = @_;
464
465    &log("Selected $grabber.\n") if ($debug);
466    if (query_config($grabber, 'category') == 2)
467    {
468        # We might want to run C1 grabbers multiple times
469        # to grab various timeslices, but not C2 grabbers,
470        # which should get everything at once.
471        delete $gscore->{$grabber};
472    }
473    return $grabber;
474}
475
476# Grabbers earn 1 point for each slot or chanday they can fill.
477# This score is multiplied if the grabber:
478# * is a category 2 grabber (i.e. fast/cheap)
479# * is a category 1 grabber that has the data we want in a cache
480# * can supply high-quality data
481# Very low quality grabbers score 0 unless we need them; i.e. they're backups.
482sub score_grabbers
483{
484    my ($score, $total, $day, $catbonus, $dqbonus, $mult, $key);
485
486    my $bestdq = 0;
487
488    # Compare C2 grabbers against the raw missing file, because we'll get
489    # everything. But compare C1 grabbers against the timeslice, because we'll
490    # only ask them for a slice. This goes for the [cache] and regular C1s.
491    foreach my $grabber (keys %$gscore)
492    {
493        # for each slot, say whether we can fill it or not -- that is,
494        # whether we support this channel and this day #.
495
496        my $hits = 0;
497        my $cat = query_config($grabber, 'category');
498        my $dq = query_config($grabber, 'quality');
499
500        if ($cat == 1)
501        {
502            $key = cut_down_missing($grabber);
503            # &log("Grabber $grabber is Category 1: comparing capability to best timeslice.\n") if ($debug);
504        }
505        else
506        {
507            $key = $missing;
508            # &log("Grabber $grabber is Category 2: comparing capability to all wanted channels and days.\n") if ($debug);
509        }
510
511        if ($grabber =~ /\[cache\]/)
512        {
513            $hits = find_cache_hits($grabber, $key);
514        }
515        else
516        {
517            foreach my $day (sort keys %$key)
518            {
519                my $val = supports_day($grabber, $day);
520                next unless ($val);
521                # &log("Day $day:") if ($debug);
522                foreach my $ch (@{$key->{$day}})
523                {
524                    if (supports_channel($grabber, $ch, $day))
525                    {
526                        # &log(" $ch") if ($debug);
527                        $hits += $val;
528                    }
529                }
530                # &log("\n") if $debug;
531                $hits = 1 if ($hits > 0 and $hits < 1);
532            }
533        }
534
535        my $catbonus = 1;
536        $catbonus = 3 if ($cat == 2);
537        if ($grabber =~ /\[cache\]/)
538        {
539            # Bonus is on a sliding scale between 1 and 2 depending on
540            # % of required data in cache
541            $catbonus += $hits / $timeslice->{chandays};
542        }
543
544        my $dqbonus = 2 ** ($dq-1);
545
546        my $mult = $dq ** $catbonus;
547
548        my $score = int($hits * $mult);
549
550        if ($debug)
551        {
552            my $str = sprintf "Grabber %s can supply %d chandays",
553                                $grabber, $hits;
554            if ($hits)
555            {
556                $str .= sprintf " at x%.1f (cat: %d, DQ: %d): %d pts",
557                            $mult,
558                            $cat,
559                            $dq,
560                            $score;
561            }
562            &log("$str.\n");
563        }
564
565        $gscore->{$grabber} += $score;
566        $total += $score;
567        if ($grabber =~ /\[cache\]/)
568        {
569            $gscore->{query_name($grabber)} += $score;
570        }
571
572        if ($score and $dq > $bestdq)
573        {
574            $bestdq = $dq;
575        }
576    }
577
578    # Eliminate grabbers of data quality 1 if there are any better-quality
579    # alternatives when using randomize.
580    if ($opt->{randomize})
581    {
582        foreach (keys %$gscore)
583        {
584            if ($gscore->{$_}
585                    and
586                query_config($_, 'quality') == 1
587                    and
588                $bestdq > 1)
589            {
590                $total -= $gscore->{$_};
591                $gscore->{$_} = 0;
592                &log("Zeroing grabber $_ due to low data quality.\n") if ($debug);
593            }
594        }
595    }
596
597    return $total;
598}
599
600# Return 1 if the grabber can provide data for this channel, else 0.
601sub supports_channel
602{
603    my ($grabber, $ch, $day) = @_;
604
605    my $mdpc = query_config($grabber, 'max_days_per_chan');
606    if ($mdpc)
607    {
608        if ($mdpc->{$ch})
609        {
610            return ($mdpc->{$ch} > $day);
611        }
612    }
613
614    my $channels_supported = query_config($grabber, 'channels');
615    unless (defined $channels_supported)
616    {
617        &log("WARNING: Grabber $grabber has no channel support " .
618              "specified in config.\n");
619        $channels_supported = '';
620    }
621
622    return 1 unless ($channels_supported); # Empty string means we support all
623   
624    $ch =~ s/ /_/g;
625    my $match = ($channels_supported =~ /\b$ch\b/);
626    my $exceptions = ($channels_supported =~/^-/);
627    return ($match != $exceptions);
628}
629
630# Return 0 if the grabber can't provide data for this day,
631# 1 if it can reliably, and 0.5 if it can unreliably.
632#
633# Note that a max_days of 7 means the grabber can retrieve data for
634# today plus 6 days.
635sub supports_day
636{
637    my ($grabber, $day) = @_;
638
639    return 0 unless ($day < query_config($grabber, 'max_days'));
640    return 0.5 if ($day >= query_config($grabber, 'max_reliable_days'));
641    return 1;
642}
643
644sub find_cache_hits
645{
646    my ($grabber, $key) = @_;
647
648    $grabber = query_name($grabber);
649
650    return 0 unless ($components->{$grabber}->{cached});
651
652    my $hits = 0;
653
654    foreach my $day (keys %$key)
655    {
656        my $date = substr(DateCalc("today", "+ $day days"), 0, 8);
657        foreach my $ch (@{$key->{$day}})
658        {
659            $hits++ if (grep(/^$date:$ch$/, @{$components->{$grabber}->{cached}}));
660        }
661    }
662    return $hits;
663}
664
665# Build a dayhash of what channel/day data we're currently missing.
666# I think granularity of one day is good for now; could possibly be
667# made more fine-grained if we think grabbers will support that.
668sub detect_missing_data
669{
670    my $m = { };
671
672    my $chandays = 0;
673    foreach my $ch (keys %$channels)
674    {
675        # is this channel missing too much data?
676        unless ($channel_data->{$ch}->{analysis}->{data_ok}) {
677            # not ok - record which days are bad
678            foreach my $day (@{($channel_data->{$ch}->{analysis}->{day})}) {
679                push(@{($m->{($day->{num})})}, $ch) unless ($day->{day_ok});
680            }
681        }
682    }
683
684    foreach my $day (keys %$m)
685    {
686        $m->{$day} = [ sort @{$m->{$day}} ];
687        $chandays += scalar(@{$m->{$day}}) if ($debug);
688    }
689
690    if ($debug)
691    {
692        &log("Need data for days " . join(", ", sort keys %$m) . 
693             " ($chandays chandays).\n");
694    }
695    return $m;
696}
697
698# Find the largest timeslice in the current $missing dayhash; i.e.
699# something like "Days 4 - 6 of ABC and SBS." This works by iterating
700# through the days and looking for overlaps where consecutive days
701# want the same channels.
702sub find_best_timeslice
703{
704    my ($overlap, $a);
705    my $slice = { 'chandays' => 0 };
706
707    foreach my $day (0 .. $days-1)
708    {
709        consider_slice($slice, $day, $day, @{$missing->{$day}});
710        $overlap = $missing->{$day};
711        foreach my $nextday (($day + 1) .. $days-1)
712        {
713            last unless ($missing->{$nextday});
714            $a = Algorithm::Diff::LCS($overlap, $missing->{$nextday});
715            last unless ($a and @{$a});
716            consider_slice($slice, $day, $nextday, @{$a});
717            $overlap = $a;
718        }
719    }
720    return $slice;
721}
722
723sub consider_slice
724{
725    my ($slice, $startday, $stopday, @chans) = @_;
726
727    my $challenger = ($stopday - $startday + 1) * scalar(@chans);
728    return unless ($challenger > $slice->{chandays});
729
730    # We have a winner!
731    $slice->{start} = $startday;
732    $slice->{stop} = $stopday;
733    $slice->{chans} = [ @chans ];
734    $slice->{chandays} = $challenger;
735}
736
737# Record what a cacheable C1 grabber has just retrieved for us,
738# so we know next time that this data can be grabbed quickly.
739sub record_cached
740{
741    my ($grabber, @grabbed) = @_;
742
743    &log("Recording cache for grabber $grabber.\n") if ($debug);
744
745    my $gcache = $components->{$grabber}->{cached};
746    $gcache = [ ] unless ($gcache);
747    my @newcache;
748    my $today = strftime("%Y%m%d", localtime);
749
750    # remove old chandays
751    foreach my $chanday (@$gcache)
752    {
753        $chanday =~ /(\d+):(.*)/;
754        if ($1 >= $today)
755        {
756            push (@newcache, $chanday);
757        }
758    }
759
760    # record new chandays
761    foreach my $chanday (@grabbed)
762    {
763        push (@newcache, $chanday) unless (grep(/^$chanday$/, @newcache));
764    }
765    $components->{$grabber}->{cached} = [ @newcache ];
766}
767
768# Takes a dayhash and returns it as a list like this:
769# ( "20061018:ABC", "20061018:Seven", ... )
770sub convert_dayhash_to_list
771{
772    my $h = shift;
773
774    my @ret;
775    foreach my $day (keys %$h)
776    {
777        my $date = substr(DateCalc("today", "+ $day days"), 0, 8);
778        foreach my $ch (@{$h->{$day}})
779        {
780            push (@ret, "$date:$ch");
781        }
782    }
783    @ret = sort @ret;
784    return \@ret;
785}
786
787# If we're about to re-try a grabber, make sure that we're not asking
788# it for the same data. That is, prevent a broken C1 grabber causing
789# an infinite loop.
790sub record_requested_chandays
791{
792    my ($grabber, $slice) = @_;
793
794    &log("Recording timeslice request; will not request these chandays " .
795         "from $grabber again.\n") if ($debug);
796
797    my @requested;
798    for my $day ($slice->{start} .. $slice->{stop})
799    {
800        foreach my $ch (@{$slice->{chans}})
801        {
802            push @requested, "$day:$ch";
803        }
804    }
805    if ($grabbed->{$grabber})
806    {
807        push @{$grabbed->{$grabber}}, @requested;
808    }
809    else
810    {
811        $grabbed->{$grabber} = [ @requested ];
812    }
813}
814
815# If this grabber has been called previously, remove those chandays
816# from the current request -- we don't want to ask it over and over
817# for a timeslice that it has already failed to provide.
818sub cut_down_missing
819{
820    my $grabber = shift;
821
822    $grabber = query_name($grabber);
823    my $dayhash = {};
824
825    # Take the timeslice and expand it to a dayhash, while pruning
826    # any chandays that have previously been requested from this
827    # grabber.
828    foreach my $day ($timeslice->{start} .. $timeslice->{stop})
829    {
830        my @chans;
831        foreach my $ch (@{$timeslice->{chans}})
832        {
833            unless ($grabbed->{$grabber} and grep(/$day:$ch/, @{$grabbed->{$grabber}}))
834            {
835                push (@chans, $ch)
836            }
837        }
838        $dayhash->{$day} = [ @chans ] if (@chans);
839    }
840
841    return $dayhash;
842}
843
844# -----------------------------------------
845# Subs: Analyzing data
846# -----------------------------------------
847
848# interpret xmltv data from this grabber/postprocessor
849sub soak_up_data
850{
851    my ($plugin, $output, $plugintype) = @_;
852
853    if (! -r $output) {
854        &log((sprintf "SHEPHERD: Warning: plugin '%s' output file '%s' does not exist\n",$plugin,$output));
855        return;
856    }
857
858    my $this_plugin = $plugin_data->{$plugin};
859    &log((sprintf "SHEPHERD: Started parsing XMLTV from '%s' in '%s' .. any errors below are from parser:\n",$plugin,$output));
860    eval { $this_plugin->{xmltv} = XMLTV::parsefiles($output); };
861    &log((sprintf "SHEPHERD: Completed XMLTV parsing from '%s'\n",$plugin));
862
863    if (!($this_plugin->{xmltv})) {
864        &log("WARNING: Plugin $plugin didn't seem to return any valid XMLTV!\n");
865        return;
866    }
867
868    $this_plugin->{valid} = 1;
869    $this_plugin->{output_filename} = $output;
870
871    my $xmltv = $this_plugin->{xmltv};
872    my ($encoding, $credits, $chan, $progs) = @$xmltv;
873    $this_plugin->{total_duration} = 0;
874    $this_plugin->{programmes} = 0;
875    $this_plugin->{progs_with_invalid_date} = 0;        # explicitly track unparsable dates
876    $this_plugin->{progs_with_unknown_channel} = 0;     # explicitly track unknown channels
877
878    my $seen_channels_with_data = 0;
879
880    #
881    # first iterate through all programmes and see if there are any channels we don't know about
882    #
883    my %chan_xml_list;
884    foreach my $ch (sort keys %{$channels}) {
885        $chan_xml_list{($channels->{$ch})} = 1;
886    }
887    foreach my $prog (@$progs) {
888        if (!defined $chan_xml_list{($prog->{channel})}) {
889            $this_plugin->{progs_with_unknown_channel}++;
890            &log((sprintf " - WARNING: plugin '%s' returned data for unknown channel '%s': ignored.\n",$plugin,$prog->{channel}));
891            $chan_xml_list{($prog->{channel})} = 1;     # so we warn only once
892        }
893    }
894       
895    # iterate thru channels
896    foreach my $ch (sort keys %{$channels}) {
897        my $seen_progs_on_this_channel = 0;
898
899        # iterate thru programmes per channel
900        foreach my $prog (@$progs) {
901            next if ($prog->{channel} ne $channels->{$ch});
902
903            my $t1 = &parse_xmltv_date($prog->{start});
904            my $t2 = &parse_xmltv_date($prog->{stop});
905
906            if (!$t1 || !$t2) {
907                &log((sprintf " - WARNING: plugin '%s' returned programme data with invalid timestamp format: \"%s\": can't parse.\n",
908                    $plugin,(!$t1 ? $prog->{start} : $prog->{stop}))) if (!$this_plugin->{progs_with_invalid_date});
909                $this_plugin->{progs_with_invalid_date}++;
910                next;
911            }
912
913            # store plugin-specific stats
914            $this_plugin->{programmes}++;
915            $this_plugin->{total_duration} += ($t2 - $t1);
916            $seen_progs_on_this_channel++;
917            $this_plugin->{earliest_data_seen} = $t1 if (!defined $this_plugin->{earliest_data_seen});
918            $this_plugin->{earliest_data_seen} = $t1 if ($t1 < $this_plugin->{earliest_data_seen});
919            $this_plugin->{latest_data_seen} = $t2 if (!defined $this_plugin->{latest_data_seen});
920            $this_plugin->{latest_data_seen} = $t2 if ($t2 > $this_plugin->{latest_data_seen});
921
922            # store channel-specific stats
923            $channel_data->{$ch}->{programmes}++;
924            $channel_data->{$ch}->{total_duration} += ($t2 - $t1);
925
926            # programme is outside the timeslots we are interested in.
927            next if ($t1 > $policy{endtime});
928            next if ($t2 < $policy{starttime});
929
930            # store timeslot info
931            my $start_slotnum = 0;
932            $start_slotnum = int(($t1 - $policy{starttime}) / $policy{timeslot_size})
933                if ($t1 >= $policy{starttime});
934
935            my $end_slotnum = ($policy{num_timeslots}-1);
936            $end_slotnum = int(($t2 - $policy{starttime}) / $policy{timeslot_size})
937                if ($t2 < $policy{endtime});
938
939            # add this programme into the global timeslots table for this channel
940            foreach my $slotnum ($start_slotnum..$end_slotnum) {
941                $channel_data->{$ch}->{timeslots}[$slotnum]++;
942            }
943        }
944
945        $seen_channels_with_data++ if ($seen_progs_on_this_channel > 0);
946    }
947
948    # print some stats about what we saw!
949    &log((sprintf "SHEPHERD: %s '%s' returned data for %d channels, %d programmes, %dd%02dh%02dm%02ds duration, %s%s\n",
950        ucfirst($plugintype), $plugin, $seen_channels_with_data, $this_plugin->{programmes},
951        int($this_plugin->{total_duration} / 86400),            # days
952        int(($this_plugin->{total_duration} % 86400) / 3600),   # hours
953        int(($this_plugin->{total_duration} % 3600) / 60),      # mins
954        int($this_plugin->{total_duration} % 60),               # sec
955        (defined $this_plugin->{earliest_data_seen} ? (strftime "%a %e %b %H:%M - ", localtime($this_plugin->{earliest_data_seen})) : 'no data'),
956        (defined $this_plugin->{latest_data_seen} ? (strftime "%a %e %b %H:%M", localtime($this_plugin->{latest_data_seen})) : '')));
957
958    $this_plugin->{laststatus} = sprintf "%dch/%dpr/%dhrs %s-%s",
959        $seen_channels_with_data, $this_plugin->{programmes},
960        int($this_plugin->{total_duration} / 3600),
961        (defined $this_plugin->{earliest_data_seen} ? (strftime "%a%d%b", localtime($this_plugin->{earliest_data_seen})) : 'no'),
962        (defined $this_plugin->{latest_data_seen} ? (strftime "%a%d%b", localtime($this_plugin->{latest_data_seen})) : 'data');
963
964    $plugin_data->{$plugin} = $this_plugin;
965}
966
967
968# analyze grabber data - do we have all the data we want?
969sub analyze_plugin_data
970{
971    my ($analysistype,$quiet) = @_;
972    &log("SHEPHERD: $analysistype:\n") unless $quiet;
973
974    my $total_channels = 0;
975
976    my $overall_data_ok = 1; # until proven otherwise
977
978    # iterate across each channel
979    foreach my $ch (sort keys %{$channels}) {
980        $total_channels++;
981
982        my $data;
983        my $lastpol = "";
984        $data->{data_ok} = 1; # unless proven otherwise
985        $data->{have} = 0;
986        $data->{missing} = 0;
987
988        for my $slotnum (0..($policy{num_timeslots}-1)) {
989            my $bucket_start_offset = ($slotnum * $policy{timeslot_size});
990
991            # work out day number of when this bucket is.
992            # number from 0 onwards.  (i.e. today=0).
993            # for a typical 7 day grabber this will actually mean 8 days of data (0-7)
994            # with days 0 and 7 truncated to half-days
995            my $day = int(($bucket_start_offset + $policy{first_bucket_offset}) / 86400);
996
997            if (!defined $data->{day}->[$day]) {
998                $data->{day}->[$day]->{num} = $day;
999                $data->{day}->[$day]->{have} = 0;
1000                $data->{day}->[$day]->{missing} = 0;
1001                $data->{day}->[$day]->{missing_peak} = 0;
1002                $data->{day}->[$day]->{missing_nonpeak} = 0;
1003                $data->{day}->[$day]->{missing_other} = 0;
1004
1005                $data->{day}->[$day]->{day_ok} = 1; # until proven otherwise
1006
1007                # day changed, dump any 'already_missing' data
1008                &dump_already_missing($data);
1009            }
1010
1011            # we have programming data for this bucket.  great!  process next bucket
1012            if ((defined $channel_data->{$ch}->{timeslots}[$slotnum]) &&
1013                ($channel_data->{$ch}->{timeslots}[$slotnum] > 0)) {
1014
1015                # if we have missing data queued up, push it now
1016                &dump_already_missing($data);
1017
1018                &dump_already_missing_period($data->{day}->[$day],$lastpol)
1019                  if ($lastpol ne "");
1020
1021                $data->{day}->[$day]->{have} += $policy{timeslot_size};
1022                $data->{have} += $policy{timeslot_size};
1023                next;
1024            }
1025
1026            # we don't have programming for this channel for this bucket
1027
1028            # some grabbers take HOURS to run. if this bucket (missing data) is for
1029            # a time period now in the past, then don't include it
1030            next if (($bucket_start_offset + $policy{starttime}) < time);
1031
1032            # work out the localtime of when this bucket is
1033            my $bucket_seconds_offset = ($bucket_start_offset + $policy{first_bucket_offset}) % 86400;
1034
1035            # store details of where we are missing data
1036            if (!defined $data->{already_missing}) {
1037                $data->{already_missing} = sprintf "#%d/%02d:%02d",
1038                  $day,
1039                  int($bucket_seconds_offset / 3600),
1040                  int(($bucket_seconds_offset % 3600) / 60);
1041            }
1042            $data->{already_missing_last} = $bucket_seconds_offset + $policy{timeslot_size} - 1;
1043
1044            $data->{day}->[$day]->{missing} += $policy{timeslot_size};
1045            $data->{missing} += $policy{timeslot_size};
1046
1047            # work out what policy missing data for this bucket fits into
1048            my $pol;
1049            if (($bucket_seconds_offset >= $policy{peak_start}) &&
1050                (($bucket_seconds_offset+$policy{timeslot_size}) <= $policy{peak_stop})) {
1051                $pol = "peak";
1052            } elsif (($bucket_seconds_offset >= $policy{nonpeak_start}) &&
1053                     (($bucket_seconds_offset+$policy{timeslot_size}) <= $policy{nonpeak_stop})) {
1054                $pol = "nonpeak";
1055            } else {
1056                $pol = "other";
1057            }
1058
1059            &dump_already_missing_period($data->{day}->[$day],$lastpol)
1060              if (($lastpol ne $pol) && ($lastpol ne ""));
1061
1062            $lastpol = $pol;
1063
1064            $data->{day}->[$day]->{"missing_".$pol} += $policy{timeslot_size};
1065
1066            $data->{day}->[$day]->{"already_missing_".$pol."_start"} = $bucket_seconds_offset
1067              if (!defined $data->{day}->[$day]->{"already_missing_".$pol."_start"});
1068            $data->{day}->[$day]->{"already_missing_".$pol."_stop"} = $bucket_seconds_offset + $policy{timeslot_size} - 1;
1069
1070            $data->{day}->[$day]->{day_ok} = 0 if ($data->{day}->[$day]->{missing_peak} > $policy{peak_max_missing});
1071            $data->{day}->[$day]->{day_ok} = 0 if ($data->{day}->[$day]->{missing_nonpeak} > $policy{nonpeak_max_missing});
1072            $data->{day}->[$day]->{day_ok} = 0 if ($data->{day}->[$day]->{missing_other} > $policy{other_max_missing});
1073            $data->{data_ok} = 0 if ($data->{day}->[$day]->{day_ok} == 0);
1074            $overall_data_ok = 0 if ($data->{data_ok} == 0);
1075        }
1076
1077        # finished all timeslots in this channel.
1078        # if we have missing data queued up, push it now
1079        &dump_already_missing($data);
1080
1081        # fill in any last missing period data
1082        foreach my $day (@{($data->{day})}) {
1083            &dump_already_missing_period($day,"peak");
1084            &dump_already_missing_period($day,"nonpeak");
1085            &dump_already_missing_period($day,"other");
1086        }
1087
1088        my $statusstring = sprintf " > ch %s: %s programming: %s\n", 
1089          $ch, pretty_duration($data->{have}),
1090          $data->{data_ok} ? "PASS (within thresholds)" : "FAIL, missing data over policy threshold:";
1091
1092        # display per-day missing data statistics
1093        foreach my $day (@{($data->{day})}) {
1094            unless ($day->{day_ok}) {
1095                $statusstring .= sprintf "\t".(strftime("%a %e %b",localtime($policy{starttime}+($day->{num}*86400)))).": ";
1096
1097                # do we have any data for this day?
1098                $statusstring .= "peak ".join(", ",(@{($day->{missing_peak_table})}))
1099                  if (($day->{missing_peak}) && ($day->{missing_peak} > $policy{peak_max_missing}));
1100
1101                $statusstring .= sprintf "%snon-peak %s",
1102                  ($day->{missing_peak} ? " / " : ""),
1103                  join(", ",(@{($day->{missing_nonpeak_table})}))
1104                  if (($day->{missing_nonpeak}) && ($day->{missing_nonpeak} > $policy{nonpeak_max_missing}));
1105
1106                $statusstring .= sprintf "%sother %s",
1107                  (($day->{missing_peak} + $day->{missing_nonpeak}) > 0 ? " / " : ""),
1108                  join(", ",(@{($day->{missing_other_table})}))
1109                  if (($day->{missing_other}) && ($day->{missing_other} > $policy{other_max_missing}));
1110
1111                $statusstring .= "\n";
1112            }
1113        }
1114        &log($statusstring) unless $quiet;
1115
1116        delete $channel_data->{$ch}->{analysis} if (defined $channel_data->{$ch}->{analysis});
1117        $channel_data->{$ch}->{analysis} = $data;
1118    }
1119
1120    &log((sprintf " > OVERALL: %s\n", ($overall_data_ok ? "PASS" : "FAIL"))) unless $quiet;
1121
1122    return $overall_data_ok; # return 1 for good, 0 for need more
1123}
1124
1125# helper routine for filling in 'missing_all' array
1126sub dump_already_missing
1127{
1128    my $d = shift;
1129    if (defined $d->{already_missing}) {
1130        $d->{already_missing} .= sprintf "-%02d:%02d",
1131          int($d->{already_missing_last} / 3600),
1132          int(($d->{already_missing_last} % 3600) / 60)
1133          if (defined $d->{already_missing_last});
1134        push(@{($d->{missing_all})}, $d->{already_missing});
1135        delete $d->{already_missing};
1136        delete $d->{already_missing_last};
1137    }
1138}
1139
1140# helper routine for filling in per-day missing data
1141# specific to peak/nonpeak/other
1142sub dump_already_missing_period
1143{
1144    my ($d,$p) = @_;
1145    my $startvar = "already_missing_".$p."_start";
1146    my $stopvar = "already_missing_".$p."_stop";
1147
1148    if (defined $d->{$startvar}) {
1149        push(@{($d->{"missing_".$p."_table"})},
1150          sprintf "%02d:%02d-%02d:%02d",
1151            int($d->{$startvar} / 3600),
1152            int(($d->{$startvar} % 3600) / 60),
1153            int($d->{$stopvar} / 3600),
1154            int(($d->{$stopvar} % 3600) / 60));
1155        delete $d->{$startvar};
1156        delete $d->{$stopvar};
1157    }
1158}
1159
1160# given a duration (seconds), return it in a pretty "{days}d{hr}h{min}m" string
1161# and indication of whether the duration is over its threshold or not
1162sub pretty_duration
1163{
1164    my ($d,$crit) = @_;
1165    my $s = "";
1166    $s .= sprintf "%dd",int($d / (60*60*24)) if ($d >= (60*60*24));
1167    $s .= sprintf "%dh",int(($d % (60*60*24)) / (60*60)) if (($d % (60*60*24)) >= (60*60));
1168    $s .= sprintf "%dm",int(($d % (60*60)) / 60) if (($d % (60*60)) >= 60);
1169    $s .= "no" if ($s eq "");
1170
1171    if (defined $crit) {
1172        $s .= "[!]" if ($d > $crit);
1173    }
1174    return $s;
1175}
1176
1177# work out date range we are expecting data to be in
1178sub calc_date_range
1179{
1180    # work out GMT offset - we only do this once
1181    if (!$gmt_offset) {
1182        # work out our gmt offset
1183        my @l = localtime(43200), my @g = gmtime(43200);
1184        $gmt_offset = (($l[2] - $g[2])*(60*60)) + (($l[1] - $g[1])*60);
1185    }
1186
1187    $policy{starttime} = time;
1188
1189    # set endtime as per $days less 1 day + hours left today
1190    $policy{endtime} = $policy{starttime} + ((60*60*24)*($days-1)) + (86400 - (($policy{starttime} + $gmt_offset) % 86400));
1191
1192    # normalize starttime to beginning of next bucket
1193    $policy{starttime} += ($policy{timeslot_size} - ($policy{starttime} % $policy{timeslot_size}));
1194
1195    # work out how many seconds into a day our first bucket starts
1196    $policy{first_bucket_offset} = ($policy{starttime} + $gmt_offset) % 86400;
1197
1198    # normalize endtime to end of previous bucket
1199    $policy{endtime} -= ($policy{endtime} % $policy{timeslot_size});
1200
1201    # if we are working with an --offset, apply it now.
1202    $policy{starttime} += (86400 * $opt->{offset}) if ($opt->{offset});
1203
1204    # work out number of buckets
1205    $policy{num_timeslots} = ($policy{endtime} - $policy{starttime}) / $policy{timeslot_size};
1206}
1207
1208
1209# strptime type date parsing - BUT - if no timezone is present, treat time as being in localtime
1210# rather than the various other perl implementation which treat it as being in UTC/GMT
1211sub parse_xmltv_date
1212{
1213    my $datestring = shift;
1214    my @t; # 0=sec,1=min,2=hour,3=day,4=month,5=year,6=wday,7=yday,8=isdst
1215    my $tz_offset = 0;
1216
1217    if ($datestring =~ /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})/) {
1218        ($t[5],$t[4],$t[3],$t[2],$t[1],$t[0]) = (int($1)-1900,int($2)-1,int($3),int($4),int($5),0);
1219        ($t[6],$t[7],$t[8]) = (-1,-1,-1);
1220
1221        # if input data has a timezone offset, then offset by that
1222        if ($datestring =~ /\+(\d{2})(\d{2})/) {
1223            $tz_offset = $gmt_offset - (($1*(60*60)) + ($2*60));
1224        } elsif ($datestring =~ /\-(\d{2})(\d{2})/) {
1225            $tz_offset = $gmt_offset + (($1*(60*60)) + ($2*60));
1226        }
1227
1228        my $e = mktime(@t);
1229        return ($e+$tz_offset) if ($e > 1);
1230    }
1231    return undef;
1232}
1233
1234# -----------------------------------------
1235# Subs: Reconciling data
1236# -----------------------------------------
1237
1238# for all the data we have, try to pick the best bits!
1239sub reconcile_data
1240{
1241    &log("\nReconciling data:\n\n");
1242
1243    my $num_grabbers = 0;
1244    my $input_files = "";
1245    my @input_file_list;
1246
1247    &log("Preferred title preferences from '$pref_title_source'\n")
1248        if ((defined $pref_title_source) &&
1249            ($plugin_data->{$pref_title_source}) &&
1250            ($plugin_data->{$pref_title_source}->{valid}));
1251
1252    &log("Preference for whose data we prefer as follows:\n");
1253    foreach my $proggy (sort { $components->{$b}->{config}->{quality} <=> $components->{$a}->{config}->{quality} } query_grabbers()) {
1254        if ((!$components->{$proggy}->{disabled}) && ($plugin_data->{$proggy}) && ($plugin_data->{$proggy}->{valid})) {
1255            $num_grabbers++;
1256            &log((sprintf "  %d. %s (%s)\n", $num_grabbers, $proggy, $plugin_data->{$proggy}->{output_filename}));
1257
1258            $input_files .= $plugin_data->{$proggy}->{output_filename}." ";
1259            push(@input_file_list,$plugin_data->{$proggy}->{output_filename});
1260        }
1261    }
1262
1263    if ($num_grabbers == 0) {
1264        die "Nothing to reconcile!  There is no valid grabber data!\n";
1265    }
1266
1267    foreach my $reconciler (sort { $components->{$a} <=> $components->{$b} } query_reconcilers()) {
1268        next if ($components->{$reconciler}->{disabled});
1269        next if (!$components->{$reconciler}->{ready});
1270
1271        $reconciler_found_all_data = &call_data_processor("reconciler",$reconciler,$input_files);
1272
1273        if ((!$reconciler_found_all_data) && ($grabber_found_all_data)) {
1274            # urgh.  this reconciler did a bad bad thing ...
1275            &log("SHEPHERD: XML data from reconciler $reconciler appears bogus, will try to use another reconciler\n");
1276        } else {
1277            &log("SHEPHERD: Data from reconciler $reconciler looks good\n");
1278            $input_postprocess_file = $plugin_data->{$reconciler}->{output_filename};
1279        }
1280
1281        last if ($input_postprocess_file ne "");
1282    }
1283
1284    if ($input_postprocess_file eq "") {
1285        # no reconcilers worked!!
1286        &log("SHEPHERD: WARNING: No reconcilers seemed to work!  Falling back to concatenating the data together!\n");
1287
1288        my %w_args = ();
1289        $input_postprocess_file = "$CWD/input_preprocess.xmltv";
1290        my $fh = new IO::File ">$input_postprocess_file" || die "could not open $input_postprocess_file for writing: $!\n";
1291        %w_args = (OUTPUT => $fh);
1292        XMLTV::catfiles(\%w_args, @input_file_list);
1293    }
1294}
1295
1296
1297# -----------------------------------------
1298# Subs: Postprocessing
1299# -----------------------------------------
1300
1301sub postprocess_data
1302{
1303    # for our first postprocessor, we feed it ALL of the XMLTV files we have
1304    # as each postprocessor runs, we feed in the output from the previous one
1305    # Shepherd checks the "completeness" of the data that comes out of a postprocessor & automatically
1306    # reverts back to the previous postprocessor if it was shown to be bad
1307
1308    # first time around: feed in reconciled data ($input_postprocess_file)
1309
1310    &log("\nPostprocessing stage:\n");
1311
1312    foreach my $postprocessor (sort { $components->{$a} <=> $components->{$b} } query_postprocessors()) {
1313        next if ($components->{$postprocessor}->{disabled});
1314        next if (!$components->{$postprocessor}->{ready});
1315
1316        my $found_all_data = call_data_processor("postprocessor",$postprocessor,$input_postprocess_file);
1317
1318        if ($found_all_data) {
1319            # accept what this postprocessor did to our output ...
1320            &log("SHEPHERD: accepting output from postprocessor $postprocessor, feeding it into next stage\n");
1321            $input_postprocess_file = $plugin_data->{$postprocessor}->{output_filename};
1322            delete $components->{$postprocessor}->{conescutive_failures} if (defined $components->{$postprocessor}->{conescutive_failures});
1323            next;
1324        }
1325
1326        # urgh.  this postprocessor did a bad bad thing ...
1327        &log("SHEPHERD: XML data from postprocessor $postprocessor rejected, using XML from previous stage\n");
1328
1329        if (defined $components->{$postprocessor}->{conescutive_failures}) {
1330            $components->{$postprocessor}->{conescutive_failures}++;
1331        } else {
1332            $components->{$postprocessor}->{conescutive_failures} = 1;
1333        }
1334        &log((sprintf "SHEPHERD: Postprocessor \"%s\" has now failed %d times in a row.  %d more and it will be automatically disabled.\n",
1335            $postprocessor,
1336            $components->{$postprocessor}->{conescutive_failures},
1337            ($policy{postprocessor_disable_failure_threshold} - $components->{$postprocessor}->{conescutive_failures})));
1338
1339        if ($components->{$postprocessor}->{conescutive_failures} >= $policy{postprocessor_disable_failure_threshold}) {
1340            &log("SHEPHERD: Disabling Postprocessor \"$postprocessor\".\n");
1341            $components->{$postprocessor}->{disabled} = 1;
1342        }
1343    }
1344}
1345
1346
1347# -----------------------------------------
1348# Subs: Postprocessing/Reconciler helpers
1349# -----------------------------------------
1350
1351sub call_data_processor
1352{
1353    my ($data_processor_type, $data_processor_name, $input_files) = @_;
1354
1355    $components->{$data_processor_name}->{lastdata} = time;
1356    $components->{$data_processor_name}->{laststatus} = "unknown";
1357
1358    &log((sprintf "\nSHEPHERD: Using %s: %s\n",$data_processor_type,$data_processor_name));
1359
1360    my $output = sprintf "%s/%ss/%s/output.xmltv",$CWD,$data_processor_type,$data_processor_name;
1361    my $comm = sprintf "%s/%ss/%s/%s",$CWD,$data_processor_type,$data_processor_name,$data_processor_name;
1362    $comm .= " --region $region" .
1363             " --channels_file $channels_file" .
1364             " --output $output";
1365    $comm .= " --days $days" if ($days);
1366    $comm .= " --offset $opt->{offset}" if ($opt->{offset});
1367    $comm .= " --debug" if ($debug);
1368    $comm .= " @ARGV" if (@ARGV);
1369
1370    $comm .= " --preftitle ".$plugin_data->{$pref_title_source}->{output_filename}
1371      if (($data_processor_type eq "reconciler") &&
1372          (defined $pref_title_source) &&
1373          ($plugin_data->{$pref_title_source}) &&
1374          ($plugin_data->{$pref_title_source}->{valid}));
1375
1376    $comm .= " $input_files";
1377    &log("SHEPHERD: Excuting command: $comm\n");
1378
1379    my $dir = sprintf "%s/%ss/%s/",$CWD,$data_processor_type,$data_processor_name;
1380    chdir $dir;
1381    my $retval = call_prog($comm);
1382    chdir $CWD;
1383
1384    if ($retval != 0) {
1385        &log("$data_processor_type returned with non-zero return code $retval: assuming it failed.\n");
1386        return 0;
1387    }
1388
1389    #
1390    # soak up the data we just collected and check it
1391    # YES - these are the SAME routines we used in the previous 'grabber' phase
1392    # but the difference here is that we clear out our 'channel_data' beforehand
1393    # so we can independently analyze the impact of this postprocessor.
1394    # if it clearly returns bad data, don't use that data (go back one step) and
1395    # flag the postprocessor as having failed.  after 3 consecutive failures, disable it
1396    #
1397
1398    # clear out channel_data
1399    foreach my $ch (keys %{$channels}) {
1400        delete $channel_data->{$ch};
1401    }
1402
1403    # process and analyze it!
1404    &soak_up_data($data_processor_name, $output, $data_processor_type);
1405    my $have_all_data = &analyze_plugin_data("$data_processor_type $data_processor_name");
1406
1407    $components->{$data_processor_name}->{laststatus} = $plugin_data->{$data_processor_name}->{laststatus};
1408
1409    return $have_all_data;
1410}
1411
1412
1413sub output_data
1414{
1415    # $input_postprocess_file contains our final output
1416    # send it to whereever --output told us to!
1417
1418    my $output_filename = "$CWD/output.xmltv";
1419    $output_filename = $opt->{output} if ($opt->{output});
1420
1421    open(OUTFILE,">$output_filename") || die "could not open output file $output_filename for writing: $!\n";
1422
1423    if (!(open(INFILE,"<$input_postprocess_file"))) {
1424        &log((sprintf "WARNING: could not open input file \"%s\": %s\n", $input_postprocess_file, $!));
1425        &log("Output XMLTV data may be damanged as a result!\n");
1426    } else {
1427        while (<INFILE>) {
1428            print OUTFILE $_;
1429        }
1430        close(INFILE);
1431        close(OUTFILE);
1432    }
1433
1434    &log("Final output stored in $output_filename.\n");
1435}
1436
1437# -----------------------------------------
1438# Subs: Updates & Installations
1439# -----------------------------------------
1440
1441sub update
1442{
1443    &log("\nChecking for updates:\n\n");
1444
1445    my $data = fetch_shepherd_file("status");
1446
1447    return 0 unless ($data);
1448
1449    my $made_changes = 0;
1450    my %clist = %$components;
1451
1452    # TEMPORARY CODE FOR TRANSITION TO NEW FORMAT: REMOVE THIS LATER
1453    if ($data =~ /:/)
1454    {
1455        while ($data =~ /(.*):(.*):(.*)/g)
1456        {
1457            my ($progtype, $proggy, $latestversion) = ($1,$2,$3);
1458            # TEMP HACK FOR TRANSITION: REMOVE LATER
1459            if ($latestversion eq 'shepherd')
1460            {
1461                $latestversion = $proggy;
1462                $proggy = 'shepherd';
1463            }
1464            if (update_component($proggy, $latestversion, $progtype))
1465            {
1466                $made_changes = 1;
1467            }
1468            delete $clist{$proggy};
1469        }
1470    }
1471    else
1472    {
1473    # END TEMPORARY CODE
1474        while ($data =~ /(\S+)\s+(\S+)\s+(\S+)/g)
1475        {
1476            my ($progtype, $proggy, $latestversion) = ($1,$2,$3);
1477            if (update_component($proggy, $latestversion, $progtype))
1478            {
1479                $made_changes = 1;
1480            }
1481            delete $clist{$proggy};
1482        }
1483    }
1484
1485    # work out what components disappeared (if any)
1486    foreach (keys %clist) {
1487        unless ($components->{$_}->{disabled}) {
1488            &log("\nDeleted component: $_.\n");
1489            disable($_, 2);
1490            $made_changes = 1;
1491        }
1492    }
1493    $made_changes;
1494}
1495
1496sub update_component
1497{
1498    my ($proggy, $latestversion, $progtype) = @_;
1499
1500    my $ver = 0;
1501
1502    if ($progtype eq "shepherd")
1503    {
1504        $ver = $version if (-e "$CWD/$progname");
1505    } else {
1506        $ver = $components->{$proggy}->{ver} if (defined $components->{$proggy} and -e ($progtype . "s/$proggy/$proggy"));
1507    }
1508
1509    my $result = versioncmp($ver, $latestversion);
1510    my $action =    $result == -1 ? ($ver ? "UPGRADING to v$latestversion" : "NEW") :
1511                    $result ==  1 ? "DOWNGRADING to v$latestversion" :
1512                    "up to date";
1513    &log(sprintf    "* %-40s %30s\n",
1514                    ucfirst($progtype) . " $proggy" .
1515                        ($ver ? " v$ver" : '') . "...",
1516                    $action);
1517    return 0 unless ($result);
1518    install($proggy, $latestversion, $progtype);
1519    return 1;
1520}
1521
1522sub install
1523{
1524    my ($proggy, $latestversion, $progtype) = @_;
1525    my $config;
1526
1527    &log("Downloading $proggy v$latestversion.\n");
1528
1529    my $rdir = "";
1530    my $ldir = $CWD;
1531    my $ver = "unknown";
1532
1533    if ($progtype eq "shepherd") {
1534        $ver = $version;
1535    } else {
1536        $rdir = $progtype . "s";
1537        $ldir = "$CWD/$progtype" . "s/$proggy";
1538        $ver = $components->{$proggy}->{ver} if ((defined $components->{$proggy}) && $components->{$proggy}->{ver});
1539        -d ("$CWD/$progtype" . "s") or mkdir ("$CWD/$progtype" . "s") or die "Cannot create directory $CWD/$progtype" . "s: $!";
1540    }
1541    -d $ldir or mkdir $ldir or die "Cannot create directory $ldir: $!";
1542
1543    my $newfile = "$ldir/$proggy-$latestversion";
1544    my $rfile = "$rdir/$proggy";
1545
1546    return unless (fetch_shepherd_file($rfile, $newfile));
1547
1548    # Fetch grabber config file
1549    if ($progtype eq 'grabber')
1550    {
1551        $rfile .= ".conf";
1552        $config = fetch_shepherd_file($rfile);
1553        return unless ($config); # grabbers MUST have config files
1554        eval $config;
1555    }
1556
1557    # Make component executable
1558    chmod 0755,$newfile;
1559
1560    -d $ARCHIVE_DIR or mkdir $ARCHIVE_DIR or die "Cannot create directory $ARCHIVE_DIR: $!";
1561
1562    if (-e "$ldir/$proggy")
1563    {
1564        rename("$ldir/$proggy", "$ARCHIVE_DIR/$proggy-$ver");
1565    }
1566    rename($newfile, "$ldir/$proggy");
1567   
1568    &log("Installed $proggy v$latestversion.\n") if ($debug);
1569
1570    # if the update was for shepherd itself, restart it
1571    if ($progtype eq "shepherd") {
1572        &log("\n*** Restarting ***\n\n");
1573        &close_logfile unless $opt->{nolog};
1574        exec("$ldir/$proggy @options");
1575        # This exits.
1576    }
1577
1578    &log("Testing $proggy...\n") if ($debug);
1579    my $result = test_proggy($ldir,"$ldir/$proggy");
1580
1581    $components->{$proggy}->{type} = $progtype;
1582    $components->{$proggy}->{ver} = $latestversion;
1583    $components->{$proggy}->{ready} = $result;
1584    $components->{$proggy}->{config} = $config if ($progtype eq 'grabber');
1585
1586    # If this component was disabled automatically, re-enable it.
1587    # But if it was disabled manually, leave it off.
1588    my $d = $components->{$proggy}->{disabled};
1589    if ($d and $d == 2)
1590    {
1591        enable($proggy);
1592    }
1593
1594    $components->{$proggy}->{laststatus} = sprintf "updated to %s on %s", $latestversion, (strftime "%a%d%b%y",localtime(time));
1595
1596}
1597
1598sub fetch_shepherd_file
1599{
1600    my ($fn, $store) = @_;
1601
1602    my $sites = "";
1603    $sites = "$mirror_site," if ($mirror_site);
1604    $sites .= $HOME;
1605
1606    my $ret;
1607    foreach my $site (split(/,/,$sites)) 
1608    {
1609        $ret = fetch_file("$site/$fn", $store, 1);
1610        return $ret if ($ret);
1611    }
1612    return undef;
1613}
1614
1615sub test_proggy
1616{
1617    my ($testdir,$proggyexec) = @_;
1618
1619    chdir($testdir);
1620    my $result = call_prog("$proggyexec --ready");
1621    chdir ($CWD);
1622
1623    print "Return value: $result\n" if ($debug);
1624
1625    print "\nComponent $proggyexec did not exit cleanly!\n" .
1626         "It may require configuration.\n\n" if ($result);
1627    return !$result;
1628}
1629
1630sub enable
1631{
1632    my $proggy = shift;
1633
1634    # confirm it exists first
1635    if (!$components->{$proggy}) {
1636        printf "No such component: \"%s\".\n",$proggy;
1637        return;
1638    }
1639    print "Enabling $proggy.\n";
1640
1641    delete $components->{$proggy}->{disabled};
1642    $components->{$proggy}->{laststatus} = sprintf "enabled on %s, not run yet",(strftime "%a%d%b%y", localtime(time));
1643}
1644
1645sub disable
1646{
1647    my ($proggy, $n) = @_;
1648
1649    # confirm it exists first
1650    if (!$components->{$proggy}) {
1651        printf "No such component: \"%s\".\n",$proggy;
1652        return;
1653    }
1654    print "Disabling $proggy.\n";
1655   
1656    $n ||= 1;
1657    $components->{$proggy}->{disabled} = $n;
1658    $components->{$proggy}->{laststatus} = sprintf "manually disabled on %s",(strftime "%a%d%b%y", localtime(time));
1659}
1660
1661sub check
1662{
1663    my $result;
1664    foreach my $proggy (keys %$components) {
1665        my $progtype = $components->{$proggy}->{type};
1666        $result = test_proggy("$CWD/$progtype" . "s/$proggy", "$CWD/$progtype" . "s/$proggy/$proggy");
1667        printf "%s %s: %s\n",ucfirst($progtype), $proggy,($result ? "OK" : "Failed");
1668        if (!$result ne !$components->{$proggy}->{ready}) {
1669            $components->{$proggy}->{ready} = $result;
1670        }
1671    }
1672}
1673# -----------------------------------------
1674# Subs: Utilities
1675# -----------------------------------------
1676#
1677
1678sub query_grabbers
1679{
1680    my ($conf, $val) = @_;
1681    return query_component_type('grabber',$conf,$val);
1682}
1683
1684sub query_reconcilers
1685{
1686    return query_component_type('reconciler');
1687}
1688
1689sub query_postprocessors
1690{
1691    return query_component_type('postprocessor');
1692}
1693
1694sub query_component_type
1695{
1696    my ($progtype,$conf,$val) = @_;
1697
1698    my @ret = ();
1699    foreach (keys %$components)
1700    {
1701        if ($components->{$_}->{type} eq $progtype) {
1702            if (defined $conf) {
1703                push (@ret, $_) if (query_config($_,$conf) eq $val);
1704            } else {
1705                push (@ret, $_);
1706            }
1707        }
1708    }
1709    return @ret;
1710}
1711
1712sub query_name
1713{
1714    my $str = shift;
1715    if ($str =~ /(.*) \[cache\]/)
1716    {
1717        return $1;
1718    }
1719    return $str;
1720}
1721
1722sub query_config
1723{
1724    my ($grabber, $key) = @_;
1725
1726    $grabber = query_name($grabber);
1727    return undef unless ($components->{$grabber});
1728    return $components->{$grabber}->{config}->{$key};
1729}
1730
1731sub rotate_logfiles
1732{
1733    # keep last 4 log files
1734    my $num;
1735    for ($num = 4; $num > 0; $num--) {
1736        my $f1 = sprintf "%s.%d.gz",$log_file,$num;
1737        my $f2 = sprintf "%s.%d.gz",$log_file,$num+1;
1738        unlink($f2);
1739        rename($f1,$f2);
1740    }
1741
1742    my $f2 = sprintf "%s.1",$log_file;
1743    rename($log_file,$f2);
1744}
1745
1746sub compress_file
1747{
1748    my $infile = shift;
1749    my $outfile = sprintf "%s.gz",$infile;
1750    my $gz;
1751
1752    if (!(open(INFILE,"<$infile"))) {
1753        warn "could not open file $infile for reading: $!\n";
1754        return;
1755    }
1756
1757    if (!($gz = gzopen($outfile,"wb"))) {
1758        warn "could not open file $outfile for writing: $!\n";
1759        return;
1760    }
1761
1762    while (<INFILE>) {
1763        my $byteswritten = $gz->gzwrite($_);
1764        warn "error writing to compressed file: error $gz->gzerror"
1765          if ($byteswritten == 0);
1766    }
1767    close(INFILE);
1768    $gz->gzclose();
1769    unlink($infile);
1770}
1771
1772sub open_logfile
1773{
1774    &rotate_logfiles;
1775    printf "Logging to $log_file.\n";
1776    open(LOG_FILE,">$log_file") || die "can't open log file $log_file for writing: $!\n";
1777
1778    my $now = localtime(time);
1779    printf LOG_FILE "$progname version $version started at $now\n\n";
1780}
1781
1782sub close_logfile
1783{
1784    close(LOG_FILE);
1785    compress_file($log_file.".1");
1786}
1787
1788sub log
1789{
1790    my $entry = shift;
1791    print $entry;
1792    printf LOG_FILE "%s",$entry unless $opt->{nolog};
1793}
1794
1795sub call_prog
1796{
1797    my $prog = shift;
1798    if (!(open(PROG,"$prog|"))) {
1799        &log("warning: couldn't exec \"$prog\": $!\n");
1800        return -1;
1801    }
1802    while(<PROG>) {
1803        &log($_);
1804    }
1805    close(PROG);
1806
1807    if ($? == -1) {
1808        &log("Failed to execute prog: $!\n");
1809        return -1;
1810    } elsif ($? & 127) {
1811        &log((sprintf "prog died with signal %d, %s coredump\n",
1812          ($? & 127),  ($? & 128) ? "with" : "without"));
1813        return $?;
1814    } else {
1815        &log((sprintf "prog exited with value %d\n", $? >> 8));
1816        return ($? >> 8);
1817    }
1818}
1819
1820sub fetch_file
1821{
1822    my ($url, $store, $id_self) = @_;
1823
1824    &log("Fetching $url.\n");
1825   
1826    my $ua = LWP::UserAgent->new();
1827    if ($id_self)
1828    {
1829        $ua->agent(ucfirst("$progname/$version"));
1830    }
1831    else
1832    {
1833        $ua->agent('Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; .NET CLR 1.1.4322')
1834    }
1835
1836    my $response = $ua->get($url);
1837    if ($response->is_success())
1838    {
1839        if ($store)
1840        {
1841            open (FILE, ">$store") 
1842                or (&log("ERROR: Unable to open $store for writing.\n") and return undef);
1843            print FILE $response->content();
1844            close FILE;
1845            return 1;
1846        }
1847        else 
1848        {
1849            return $response->content();
1850        } 
1851    }
1852    &log("Failed to retrieve $url!\n" . $response->status_line() . "\n");
1853    return undef;
1854}
1855
1856# -----------------------------------------
1857# Subs: Setup
1858# -----------------------------------------
1859
1860sub read_config_file
1861{
1862    read_file($config_file, 'configuration');
1863}
1864
1865sub read_channels_file
1866{
1867    read_file($channels_file, 'channels');
1868}
1869
1870sub read_file
1871{
1872    my $fn = shift;
1873    my $name = shift;
1874
1875    print "Reading $name file: $fn\n";
1876    unless (-r $fn)
1877    {
1878        unless ($opt->{configure})
1879        {
1880            print "\nNo $name file found.\n" .
1881                  ucfirst($progname) . " must be configured: " .
1882                  "configuring now.\n\n";
1883            $opt->{'configure'} = 1;
1884        }
1885        return;
1886    }
1887    local (@ARGV, $/) = ($fn);
1888    no warnings 'all';
1889    eval <>;
1890    if ($@ and !$opt->{configure})
1891    {
1892        warn "\nERROR in $name file! Details:\n$@";
1893        print "You may wish to CTRL-C and fix this.\n\nContinuing anyway in:";
1894        foreach (1 .. 5)
1895        {
1896            print " " . (6 - $_);
1897            sleep 1;
1898        }
1899        print "\n";
1900    }
1901}
1902
1903sub write_config_file
1904{
1905    write_file($config_file, 'configuration', 
1906        [$region,  $pref_title_source,  $mirror_site,  $components ],
1907        ["region", "pref_title_source", "mirror_site", "components" ]);
1908}
1909
1910sub write_channels_file
1911{
1912    write_file($channels_file, 'channels',
1913        [ $channels,  $opt_channels ],
1914        [ 'channels', 'opt_channels' ]);
1915}
1916
1917sub write_file
1918{
1919    my ($fn, $name, $vars, $varnames) = @_;
1920    open (FN, ">$fn") or die "Can't write to $name file $fn: $!";
1921    print FN Data::Dumper->Dump($vars, $varnames);
1922    close FN;
1923    print "Wrote $name file $fn.\n" if ($debug);
1924}
1925
1926sub get_initial_command_line_options
1927{
1928  GetOptions( 'config-file=s'   => \$opt->{configfile},
1929              'help'            => \$opt->{help},
1930              'configure'       => \$opt->{configure},
1931              'setmirror=s'     => \$opt->{setmirror},
1932              'setpreftitle=s'  => \$opt->{setpreftitlesource},
1933              'clearpreftitle'  => \$opt->{clearpreftitlesource},
1934              'dontcallgrabbers' => \$opt->{dontcallgrabbers},
1935              'debug'           => \$debug);
1936}
1937
1938sub get_remaining_command_line_options
1939{
1940    GetOptions(
1941              'version'         => \$opt->{status},
1942              'status'          => \$opt->{status},
1943              'list'            => \$opt->{list},
1944              'show-config'     => \$opt->{show_config},
1945
1946              'update'          => \$opt->{update},
1947              'noupdate'        => \$opt->{noupdate},
1948
1949              'disable=s'       => \$opt->{disable},
1950              'enable=s'        => \$opt->{enable},
1951
1952              'nolog'           => \$opt->{nolog},
1953
1954              'days=i'          => \$days,
1955              'offset=i'        => \$opt->{offset},
1956              'show-channels'   => \$opt->{show_channels},
1957              'output=s'        => \$opt->{output},
1958              'randomize'       => \$opt->{randomize}, # experimental
1959              'check'           => \$opt->{check}
1960            );
1961}
1962
1963sub process_setup_commands
1964{
1965    my @opts = qw( enable disable setorder check \
1966                   setpreftitlesource clearpreftitlesource setmirror );
1967
1968    my $run = 0;
1969    foreach (@opts)
1970    {
1971        if ($opt->{$_})
1972        {
1973            $run = 1;
1974            &$_($opt->{$_});
1975        }
1976    }
1977    return unless ($run);
1978    write_config_file();
1979    status();
1980    exit;
1981}
1982
1983# if a preferred title source has been specified, add it to our config
1984sub setpreftitlesource
1985{
1986    my $arg = shift;
1987    $pref_title_source = $arg;
1988    print "Added preferred title source: $pref_title_source\n";
1989    1;
1990}
1991
1992# if requesting to clear preferred title and we have one, remove it
1993sub clearpreftitlesource
1994{
1995    $pref_title_source = undef;
1996    print "Removed preferred title source $pref_title_source\n";
1997    1;
1998}
1999
2000# if a mirror has been specified, add it into our config
2001sub setmirror
2002{
2003    my $arg = shift;
2004    $mirror_site = $arg;
2005    print "Setting mirror site(s): $mirror_site\n";
2006}
2007
2008# -----------------------------------------
2009# Subs: Configuration
2010# -----------------------------------------
2011
2012sub configure
2013{
2014    my $REGIONS = {
2015        "ACT" => 126,
2016        "NSW: Sydney" => 73,
2017        "NSW: Newcastle" => 184,
2018        "NSW: Central Coast" => 66,
2019        "NSW: Griffith" => 67,
2020        "NSW: Broken Hill" => 63,
2021        "NSW: Northern NSW" => 69,
2022        "NSW: Southern NSW" => 71,
2023        "NSW: Remote and Central" => 106,
2024        "NT: Darwin" => 74,
2025        "NT: Remote & Central" => 108,
2026        "QLD: Brisbane" => 75,
2027        "QLD: Gold Coast" => 78,
2028        "QLD: Regional" => 79,
2029        "QLD: Remote & Central" => 114,
2030        "SA: Adelaide" => 81,
2031        "SA: Renmark" => 82,
2032        "SA: Riverland" => 83,
2033        "SA: South East SA" => 85,
2034        "SA: Spencer Gulf" => 86,
2035        "SA: Remote & Central" => 107,
2036        "Tasmania" => 88,
2037        "VIC: Melbourne" => 94,
2038        "VIC: Geelong" => 93,
2039        "VIC: Eastern Victoria" => 90,
2040        "VIC: Mildura/Sunraysia" => 95,
2041        "VIC: Western Victoria" => 98,
2042        "WA: Perth" => 101,
2043        "WA: Regional" => 102
2044    };
2045
2046    print "\nConfiguring.\n\n" .
2047          "Select your region:\n";
2048    foreach (sort keys %$REGIONS)
2049    {
2050        printf(" (%3d) %s\n", $REGIONS->{$_}, $_);
2051    }
2052    $region = ask_choice("Enter region code:", "94", values %$REGIONS);
2053
2054    print "\nFetching channel information... ";
2055
2056    my @channellist = get_channels();
2057
2058    print "done.\n\n" .
2059          "For each channel you want guide data for, enter an XMLTV id\n" .
2060          "of your choice (e.g. \"seven.free.au\"). If you don't need\n" .
2061          "guide data for this channel, just press Enter.\n\n" .
2062          "Please don't subscribe to unneeded channels.\n\nChannels:\n";
2063    $channels = {};
2064    my $line;
2065    foreach (@channellist)
2066    {
2067        $line = ask(" \"$_\"? ");
2068        $channels->{$_} = $line if ($line);
2069    }
2070
2071    print "\nHigh Definition TV (HDTV)\n".
2072          "Most Australian TV networks broadcast at least some\n".
2073          "programmes in HDTV each week, but for the most part\n".
2074          "either upsample SD to HD or play a rolling demonstration\n".
2075          "HD clip when they don't have the programme in HD format.\n\n".
2076          "If you have a HDTV capable system and are interested in\n".
2077          "having Shepherd's postprocessors populate HDTV content\n".
2078          "then Shepherd will need to know the XMLTV IDs for the HD\n".
2079          "channels also.\n";
2080    if (ask_boolean("\nDo you wish to include HDTV channels?")) {
2081        print "\nFor each channel you want guide data for, enter an XMLTV id\n" .
2082              "of your choice (e.g. \"sevenhd.free.au\"). If you don't need\n" .
2083              "guide data for this channel, just press Enter.\n\n";
2084
2085        $opt_channels = {};
2086        foreach (@channellist)
2087        {
2088            next if (($_ =~ /ABC2/i) || ($_ =~ /SBS News/i) || ($_ =~ /31/));
2089            $_ .= "HD";
2090            $line = ask(" \"$_\"? ");
2091            $opt_channels->{$_} = $line if ($line);
2092        }
2093    }
2094
2095
2096    print "\nHave you been running a grabber previously?\n\n".
2097          "Some data sources show the programme \"Spicks & Specks\"\n".
2098          "with an '&', some show it as \"Spicks and Specks\" with 'and'.\n".
2099          "It doesn't matter which way it is - other than if you have an existing\n".
2100          "recording policy set one way or the other, it would be preferable\n".
2101          "to not have to go and re-enter recording schedules.\n\n".
2102          "Shepherd's \"reconciler\" is smart enough to work out many subtle\n".
2103          "differences in title names and will automatically rename programmes\n".
2104          "to match the same format as previously seen.\n\n".
2105          "If you have been using a grabber in the past, if you let Shepherd\n".
2106          "know what it is, it can use that to ask the reconciler to 'learn'\n".
2107          "the subtle programme name changes between different grabbers,\n".
2108          "hopefully resulting in consistent programme names, regardless of\n".
2109          "the data source.\n\n".
2110          "Were you using one of the following data sources previously?\n".
2111          "If so, type the name of it here or leave blank if you haven't\n".
2112          "run a grabber previously or don't care about having to fix existing\n".
2113          "recording schedules.\n\n".
2114          "Choose from: ".join(", ",query_grabbers())."\nChoice: ";
2115
2116    my $pref = ask("Order? ");
2117    $pref_title_source = $pref if ($pref);
2118
2119    print "\n";
2120    show_channels();
2121    unless(ask_boolean("\nCreate configuration file?"))
2122    {
2123        print "Aborting configuration.\n";
2124        exit 0;
2125    }
2126
2127    write_config_file();
2128    write_channels_file();
2129
2130    print "Finished configuring.\n\n" .
2131          "Shepherd is installed into $CWD.\n\n";
2132   
2133    if ($invoked ne "$CWD/$progname" and $invoked =~ /$progname/)
2134    {
2135        print "Warning: you invoked this program as $invoked.\n" .
2136            "In the future, it should be run as $CWD/$progname,\n" .
2137            "to avoid constantly re-downloading the latest version.\n\n" .
2138            "MythTV users may wish to create the following symlink, by " .
2139            "doing this (as root):\n" .
2140            "\"ln -s $CWD/$progname /usr/bin/tv_grab_au\".\n\n" .
2141            "You may safely delete $invoked.\n\n";
2142    }
2143
2144    status();
2145
2146    unless (ask_boolean("\nGrab data now?"))
2147    {
2148        exit 0;
2149    }
2150}
2151
2152sub get_channels
2153{
2154    my @date = localtime;
2155    my $page = fetch_file(
2156        "http://au.tv.yahoo.com/results.html?rg=$region&dt=" .
2157        ($date[5] + 1900) . "-$date[4]-$date[3]");
2158    my @channellist;
2159    while ($page =~ /<tr class=rtb><td class=rth><a .*?>(.*?)<\/a>/g)
2160    {
2161        push @channellist, $1;
2162    }
2163    return @channellist;
2164}
2165
2166# -----------------------------------------
2167# Subs: Status & Help
2168# -----------------------------------------
2169
2170sub show_config
2171{
2172    print "\nConfiguration\n".
2173          "-------------\n" .
2174          "Config file: $config_file\n" .
2175          "Debug mode : " . is_set($debug) . "\n" .
2176          "Output file: " . ($opt->{output} ? $opt->{output} : "None") . "\n" .
2177          "Region ID  : $region\n";
2178  show_channels();
2179  print "\n";
2180  status();
2181  print "\n";
2182}
2183
2184sub show_channels
2185{
2186  print "Subscribed channels:\n";
2187  print "    $_ -> $channels->{$_}\n" for sort keys %$channels;
2188  print "Optional (HDTV) channels:\n";
2189  print "    $_ -> $opt_channels->{$_}\n" for sort keys %$opt_channels;
2190}
2191
2192sub is_set
2193{
2194    my $arg = shift;
2195    return $arg ? "Yes" : "No";
2196}
2197
2198sub pretty_print
2199{
2200    my ($p, $len) = @_;
2201    my $spaces = ' ' x (79-$len);
2202    my $ret = "";
2203
2204    while (length($p) > 0) {
2205        if (length($p) <= $len) {
2206            $ret .= $p;
2207            $p = "";
2208        } else {
2209            # find a space to the left of cutoff
2210            my $len2 = $len;
2211            while ((substr($p,$len2,1) ne ' ') && ($len2 > 0)) {
2212                $len2--;
2213            }
2214            if ($len2 == 0) {
2215                # no space - just print it with cutoff
2216                $ret .= substr($p,0,$len);
2217                $p = substr($p,$len,(length($p)-$len));
2218            } else {
2219                # print up to space
2220                $ret .= substr($p,0,$len2);
2221                $p = substr($p,($len2+1),(length($p)-$len2+1));
2222            }
2223            # print whitespace
2224            $ret .= "\n".$spaces;
2225        }
2226    }
2227    return $ret;
2228}
2229
2230sub status
2231{
2232    print "\nThe following plugins are known:\n",
2233          " Type     Name           Version Description\n".
2234          " -------- -------------- ------- ----------------------------------------------\n";
2235
2236    foreach (sort { $components->{$a}->{type} cmp $components->{$b}->{type} } keys %{$components}) {
2237        printf " %-8s %-15s%7s %46s\n",
2238          substr($components->{$_}->{type},0,8), 
2239          length($_) > 15 ? substr($_,0,13).".." : $_,
2240          ($components->{$_}->{ver} ? substr($components->{$_}->{ver},0,7) : "unknown"),
2241          (defined $components->{$_}->{config}->{desc} ?
2242            pretty_print($components->{$_}->{config}->{desc},46) : "");
2243    }
2244    printf "\n";
2245
2246    print "Grabbers, listed in order of quality:\n".
2247          "                   Enabled/\n".
2248          " Grabber        Qual Ready Last Run   Status\n" .
2249          " -------------- ---- ----- ---------- -----------------------------------------\n";
2250    my %qual_table = ( 3 => "Best", 2 => "Good", 1 => "Avg" );
2251    foreach (sort { $components->{$b}->{config}->{quality} <=> $components->{$a}->{config}->{quality} } query_grabbers()) {
2252        my $h = $components->{$_};
2253        printf  " %-15s%-4s  %1s/%1s %11s %s\n",
2254          length($_) > 15 ? substr($_,0,13).".." : $_,
2255          $qual_table{($h->{config}->{quality})},
2256          $h->{disabled} ? 'N' : 'Y',
2257          $h->{ready} ? 'Y' : 'N',
2258          $h->{lastdata} ? (strftime "%a%d%b%y", localtime($h->{lastdata})) : 'never',
2259          $h->{laststatus} ? pretty_print($h->{laststatus},41) : '';
2260    }
2261
2262    print "\n".
2263          "              Enabled/\n".
2264          " Reconciler     Ready Last Run   Status\n" .
2265          " -------------- ----- ---------- ----------------------------------------------\n";
2266    foreach (sort { $components->{$a} <=> $components->{$b} } query_reconcilers()) {
2267        my $h = $components->{$_};
2268        printf  " %-15s %1s/%1s %11s %s\n",
2269          length($_) > 15 ? substr($_,0,13).".." : $_,
2270          $h->{disabled} ? 'N' : 'Y',
2271          $h->{ready} ? 'Y' : 'N',
2272          $h->{lastdata} ? (strftime "%a%d%b%y", localtime($h->{lastdata})) : 'never',
2273          $h->{laststatus} ? pretty_print($h->{laststatus},46) : '';
2274    }
2275
2276    print "\n".
2277          "              Enabled/\n".
2278          " Postprocessor  Ready Last Run   Status\n" .
2279          " -------------- ----- ---------- ----------------------------------------------\n";
2280    foreach (sort { $components->{$a} <=> $components->{$b} } query_postprocessors()) {
2281        my $h = $components->{$_};
2282        printf  " %-15s %1s/%1s %11s %s\n",
2283          length($_) > 15 ? substr($_,0,13).".." : $_,
2284          $h->{disabled} ? 'N' : 'Y',
2285          $h->{ready} ? 'Y' : 'N',
2286          $h->{lastdata} ? (strftime "%a%d%b%y", localtime($h->{lastdata})) : 'never',
2287          $h->{laststatus} ? pretty_print($h->{laststatus},46) : '';
2288    }
2289    printf "\nPreferred titles from grabber '%s'\n",$pref_title_source if ($pref_title_source);
2290    printf "\n";
2291}
2292
2293sub help
2294{
2295    print q{
2296Command-line options:
2297    --help                Print this message
2298
2299    --status              Print a list of grabbers maintained
2300    --list                Print a detailed list of grabbers
2301    --setmirror <s>       Set URL <s> as primary location to check for updates
2302
2303    --configure           Setup
2304    --show-config         Print setup details
2305
2306    --setpreftitle <s>    Set preferred 'title' source as grabber <s>
2307    --clearpreftitle      clear preferred 'title' source
2308
2309    --disable <s>         Don't ever use grabber/postprocessor <s>
2310    --enable <s>          Okay, maybe use it again then
2311    --uninstall <s>       Remove a disabled grabber/postprocessor
2312
2313    --noupdate            Do not attempt to update before running
2314    --update              Update only; do not grab data
2315
2316    --check               Check status of all grabbers and postprocessors
2317
2318    --nolog               Don't write a logfile
2319};
2320    exit 0;
2321}
2322
2323# -----------------------------------------
2324# Subs: override handlers for standard perl.
2325# -----------------------------------------
2326
2327# ugly hack. please don't try this at home kids!
2328sub my_die {
2329    my ($arg,@rest) = @_;
2330    my ($pack,$file,$line,$sub) = caller(0);
2331
2332    # check if we are in an eval()
2333    if ($^S) {
2334        printf STDERR "  shepherd caught a die() within eval{} from file $file line $line\n";
2335    } else {
2336            printf STDERR "\nDIE: line %d in file %s\n",$line,$file;
2337            if ($arg) {
2338                CORE::die($arg,@rest);
2339            } else {
2340                CORE::die(join("",@rest));
2341            }
2342    }
2343}
Note: See TracBrowser for help on using the browser.