root/shepherd @ 179

Revision 179, 67.9 kB (checked in by max, 7 years ago)

Whoops, removing debugging lines.

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