root/shepherd @ 172

Revision 172, 67.1 kB (checked in by max, 7 years ago)

More consistent check 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" : "NEW") :
1511                    $result ==  1 ? "DOWNGRADING" :
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    my $result = test_proggy($ldir,"$ldir/$proggy");
1579
1580    $components->{$proggy}->{type} = $progtype;
1581    $components->{$proggy}->{ver} = $latestversion;
1582    $components->{$proggy}->{ready} = $result;
1583    $components->{$proggy}->{config} = $config if ($progtype eq 'grabber');
1584
1585    # If this component was disabled automatically, re-enable it.
1586    # But if it was disabled manually, leave it off.
1587    my $d = $components->{$proggy}->{disabled};
1588    if ($d and $d == 2)
1589    {
1590        enable($proggy);
1591    }
1592
1593    $components->{$proggy}->{laststatus} = sprintf "updated to %s on %s", $latestversion, (strftime "%a%d%b%y",localtime(time));
1594
1595}
1596
1597sub fetch_shepherd_file
1598{
1599    my ($fn, $store) = @_;
1600
1601    my $sites = "";
1602    $sites = "$mirror_site," if ($mirror_site);
1603    $sites .= $HOME;
1604
1605    my $ret;
1606    foreach my $site (split(/,/,$sites)) 
1607    {
1608        $ret = fetch_file("$site/$fn", $store, 1);
1609        return $ret if ($ret);
1610    }
1611    return undef;
1612}
1613
1614sub test_proggy
1615{
1616    my ($testdir,$proggyexec) = @_;
1617
1618    &log("Testing $proggyexec...\n");
1619
1620    chdir($testdir);
1621    my $result = call_prog("$proggyexec --ready");
1622    chdir ($CWD);
1623
1624    print "Return value: $result\n" if ($debug);
1625
1626    if ($result)
1627    {
1628        &log("\nComponent $proggyexec did not exit cleanly!\n" .
1629             "It may require configuration.\n\n");
1630    }
1631    else
1632    {
1633        &log("OK.\n");
1634    }
1635    return !$result;
1636}
1637
1638sub enable
1639{
1640    my $proggy = shift;
1641
1642    # confirm it exists first
1643    if (!$components->{$proggy}) {
1644        printf "No such component: \"%s\".\n",$proggy;
1645        return;
1646    }
1647    print "Enabling $proggy.\n";
1648
1649    delete $components->{$proggy}->{disabled};
1650    $components->{$proggy}->{laststatus} = sprintf "enabled on %s, not run yet",(strftime "%a%d%b%y", localtime(time));
1651}
1652
1653sub disable
1654{
1655    my ($proggy, $n) = @_;
1656
1657    # confirm it exists first
1658    if (!$components->{$proggy}) {
1659        printf "No such component: \"%s\".\n",$proggy;
1660        return;
1661    }
1662    print "Disabling $proggy.\n";
1663   
1664    $n ||= 1;
1665    $components->{$proggy}->{disabled} = $n;
1666    $components->{$proggy}->{laststatus} = sprintf "manually disabled on %s",(strftime "%a%d%b%y", localtime(time));
1667}
1668
1669sub check
1670{
1671    my $result;
1672    foreach my $proggy (keys %$components) {
1673        my $progtype = $components->{$proggy}->{type};
1674        $result = test_proggy("$CWD/$progtype" . "s/$proggy", "$CWD/$progtype" . "s/$proggy/$proggy");
1675        if (!$result ne !$components->{$proggy}->{ready}) {
1676            $components->{$proggy}->{ready} = $result;
1677        }
1678    }
1679}
1680# -----------------------------------------
1681# Subs: Utilities
1682# -----------------------------------------
1683#
1684
1685sub query_grabbers
1686{
1687    my ($conf, $val) = @_;
1688    return query_component_type('grabber',$conf,$val);
1689}
1690
1691sub query_reconcilers
1692{
1693    return query_component_type('reconciler');
1694}
1695
1696sub query_postprocessors
1697{
1698    return query_component_type('postprocessor');
1699}
1700
1701sub query_component_type
1702{
1703    my ($progtype,$conf,$val) = @_;
1704
1705    my @ret = ();
1706    foreach (keys %$components)
1707    {
1708        if ($components->{$_}->{type} eq $progtype) {
1709            if (defined $conf) {
1710                push (@ret, $_) if (query_config($_,$conf) eq $val);
1711            } else {
1712                push (@ret, $_);
1713            }
1714        }
1715    }
1716    return @ret;
1717}
1718
1719sub query_name
1720{
1721    my $str = shift;
1722    if ($str =~ /(.*) \[cache\]/)
1723    {
1724        return $1;
1725    }
1726    return $str;
1727}
1728
1729sub query_config
1730{
1731    my ($grabber, $key) = @_;
1732
1733    $grabber = query_name($grabber);
1734    return undef unless ($components->{$grabber});
1735    return $components->{$grabber}->{config}->{$key};
1736}
1737
1738sub rotate_logfiles
1739{
1740    # keep last 4 log files
1741    my $num;
1742    for ($num = 4; $num > 0; $num--) {
1743        my $f1 = sprintf "%s.%d.gz",$log_file,$num;
1744        my $f2 = sprintf "%s.%d.gz",$log_file,$num+1;
1745        unlink($f2);
1746        rename($f1,$f2);
1747    }
1748
1749    my $f2 = sprintf "%s.1",$log_file;
1750    rename($log_file,$f2);
1751}
1752
1753sub compress_file
1754{
1755    my $infile = shift;
1756    my $outfile = sprintf "%s.gz",$infile;
1757    my $gz;
1758
1759    if (!(open(INFILE,"<$infile"))) {
1760        warn "could not open file $infile for reading: $!\n";
1761        return;
1762    }
1763
1764    if (!($gz = gzopen($outfile,"wb"))) {
1765        warn "could not open file $outfile for writing: $!\n";
1766        return;
1767    }
1768
1769    while (<INFILE>) {
1770        my $byteswritten = $gz->gzwrite($_);
1771        warn "error writing to compressed file: error $gz->gzerror"
1772          if ($byteswritten == 0);
1773    }
1774    close(INFILE);
1775    $gz->gzclose();
1776    unlink($infile);
1777}
1778
1779sub open_logfile
1780{
1781    &rotate_logfiles;
1782    printf "Logging to $log_file.\n";
1783    open(LOG_FILE,">$log_file") || die "can't open log file $log_file for writing: $!\n";
1784
1785    my $now = localtime(time);
1786    printf LOG_FILE "$progname version $version started at $now\n\n";
1787}
1788
1789sub close_logfile
1790{
1791    close(LOG_FILE);
1792    compress_file($log_file.".1");
1793}
1794
1795sub log
1796{
1797    my $entry = shift;
1798    print $entry;
1799    printf LOG_FILE "%s",$entry unless $opt->{nolog};
1800}
1801
1802sub call_prog
1803{
1804    my $prog = shift;
1805    if (!(open(PROG,"$prog|"))) {
1806        &log("warning: couldn't exec \"$prog\": $!\n");
1807        return -1;
1808    }
1809    while(<PROG>) {
1810        &log($_);
1811    }
1812    close(PROG);
1813
1814    if ($? == -1) {
1815        &log("Failed to execute prog: $!\n");
1816        return -1;
1817    } elsif ($? & 127) {
1818        &log((sprintf "prog died with signal %d, %s coredump\n",
1819          ($? & 127),  ($? & 128) ? "with" : "without"));
1820        return $?;
1821    } else {
1822        &log((sprintf "prog exited with value %d\n", $? >> 8)) if ($debug or $?);
1823        return ($? >> 8);
1824    }
1825}
1826
1827sub fetch_file
1828{
1829    my ($url, $store, $id_self) = @_;
1830
1831    &log("Fetching $url.\n");
1832   
1833    my $ua = LWP::UserAgent->new();
1834    if ($id_self)
1835    {
1836        $ua->agent(ucfirst("$progname/$version"));
1837    }
1838    else
1839    {
1840        $ua->agent('Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; .NET CLR 1.1.4322')
1841    }
1842
1843    my $response = $ua->get($url);
1844    if ($response->is_success())
1845    {
1846        if ($store)
1847        {
1848            open (FILE, ">$store") 
1849                or (&log("ERROR: Unable to open $store for writing.\n") and return undef);
1850            print FILE $response->content();
1851            close FILE;
1852            return 1;
1853        }
1854        else 
1855        {
1856            return $response->content();
1857        } 
1858    }
1859    &log("Failed to retrieve $url!\n" . $response->status_line() . "\n");
1860    return undef;
1861}
1862
1863# -----------------------------------------
1864# Subs: Setup
1865# -----------------------------------------
1866
1867sub read_config_file
1868{
1869    read_file($config_file, 'configuration');
1870}
1871
1872sub read_channels_file
1873{
1874    read_file($channels_file, 'channels');
1875}
1876
1877sub read_file
1878{
1879    my $fn = shift;
1880    my $name = shift;
1881
1882    print "Reading $name file: $fn\n";
1883    unless (-r $fn)
1884    {
1885        unless ($opt->{configure})
1886        {
1887            print "\nNo $name file found.\n" .
1888                  ucfirst($progname) . " must be configured: " .
1889                  "configuring now.\n\n";
1890            $opt->{'configure'} = 1;
1891        }
1892        return;
1893    }
1894    local (@ARGV, $/) = ($fn);
1895    no warnings 'all';
1896    eval <>;
1897    if ($@ and !$opt->{configure})
1898    {
1899        warn "\nERROR in $name file! Details:\n$@";
1900        print "You may wish to CTRL-C and fix this.\n\nContinuing anyway in:";
1901        foreach (1 .. 5)
1902        {
1903            print " " . (6 - $_);
1904            sleep 1;
1905        }
1906        print "\n";
1907    }
1908}
1909
1910sub write_config_file
1911{
1912    write_file($config_file, 'configuration', 
1913        [$region,  $pref_title_source,  $mirror_site,  $components ],
1914        ["region", "pref_title_source", "mirror_site", "components" ]);
1915}
1916
1917sub write_channels_file
1918{
1919    write_file($channels_file, 'channels',
1920        [ $channels,  $opt_channels ],
1921        [ 'channels', 'opt_channels' ]);
1922}
1923
1924sub write_file
1925{
1926    my ($fn, $name, $vars, $varnames) = @_;
1927    open (FN, ">$fn") or die "Can't write to $name file $fn: $!";
1928    print FN Data::Dumper->Dump($vars, $varnames);
1929    close FN;
1930    print "Wrote $name file $fn.\n" if ($debug);
1931}
1932
1933sub get_initial_command_line_options
1934{
1935  GetOptions( 'config-file=s'   => \$opt->{configfile},
1936              'help'            => \$opt->{help},
1937              'configure'       => \$opt->{configure},
1938              'setmirror=s'     => \$opt->{setmirror},
1939              'setpreftitle=s'  => \$opt->{setpreftitlesource},
1940              'clearpreftitle'  => \$opt->{clearpreftitlesource},
1941              'dontcallgrabbers' => \$opt->{dontcallgrabbers},
1942              'debug'           => \$debug);
1943}
1944
1945sub get_remaining_command_line_options
1946{
1947    GetOptions(
1948              'version'         => \$opt->{status},
1949              'status'          => \$opt->{status},
1950              'list'            => \$opt->{list},
1951              'show-config'     => \$opt->{show_config},
1952
1953              'update'          => \$opt->{update},
1954              'noupdate'        => \$opt->{noupdate},
1955
1956              'disable=s'       => \$opt->{disable},
1957              'enable=s'        => \$opt->{enable},
1958
1959              'nolog'           => \$opt->{nolog},
1960
1961              'days=i'          => \$days,
1962              'offset=i'        => \$opt->{offset},
1963              'show-channels'   => \$opt->{show_channels},
1964              'output=s'        => \$opt->{output},
1965              'randomize'       => \$opt->{randomize}, # experimental
1966              'check'           => \$opt->{check}
1967            );
1968}
1969
1970sub process_setup_commands
1971{
1972    my @opts = qw( enable disable setorder check \
1973                   setpreftitlesource clearpreftitlesource setmirror );
1974
1975    my $run = 0;
1976    foreach (@opts)
1977    {
1978        if ($opt->{$_})
1979        {
1980            $run = 1;
1981            &$_($opt->{$_});
1982        }
1983    }
1984    return unless ($run);
1985    write_config_file();
1986    status();
1987    exit;
1988}
1989
1990# if a preferred title source has been specified, add it to our config
1991sub setpreftitlesource
1992{
1993    my $arg = shift;
1994    $pref_title_source = $arg;
1995    print "Added preferred title source: $pref_title_source\n";
1996    1;
1997}
1998
1999# if requesting to clear preferred title and we have one, remove it
2000sub clearpreftitlesource
2001{
2002    $pref_title_source = undef;
2003    print "Removed preferred title source $pref_title_source\n";
2004    1;
2005}
2006
2007# if a mirror has been specified, add it into our config
2008sub setmirror
2009{
2010    my $arg = shift;
2011    $mirror_site = $arg;
2012    print "Setting mirror site(s): $mirror_site\n";
2013}
2014
2015# -----------------------------------------
2016# Subs: Configuration
2017# -----------------------------------------
2018
2019sub configure
2020{
2021    my $REGIONS = {
2022        "ACT" => 126,
2023        "NSW: Sydney" => 73,
2024        "NSW: Newcastle" => 184,
2025        "NSW: Central Coast" => 66,
2026        "NSW: Griffith" => 67,
2027        "NSW: Broken Hill" => 63,
2028        "NSW: Northern NSW" => 69,
2029        "NSW: Southern NSW" => 71,
2030        "NSW: Remote and Central" => 106,
2031        "NT: Darwin" => 74,
2032        "NT: Remote & Central" => 108,
2033        "QLD: Brisbane" => 75,
2034        "QLD: Gold Coast" => 78,
2035        "QLD: Regional" => 79,
2036        "QLD: Remote & Central" => 114,
2037        "SA: Adelaide" => 81,
2038        "SA: Renmark" => 82,
2039        "SA: Riverland" => 83,
2040        "SA: South East SA" => 85,
2041        "SA: Spencer Gulf" => 86,
2042        "SA: Remote & Central" => 107,
2043        "Tasmania" => 88,
2044        "VIC: Melbourne" => 94,
2045        "VIC: Geelong" => 93,
2046        "VIC: Eastern Victoria" => 90,
2047        "VIC: Mildura/Sunraysia" => 95,
2048        "VIC: Western Victoria" => 98,
2049        "WA: Perth" => 101,
2050        "WA: Regional" => 102
2051    };
2052
2053    print "\nConfiguring.\n\n" .
2054          "Select your region:\n";
2055    foreach (sort keys %$REGIONS)
2056    {
2057        printf(" (%3d) %s\n", $REGIONS->{$_}, $_);
2058    }
2059    $region = ask_choice("Enter region code:", "94", values %$REGIONS);
2060
2061    print "\nFetching channel information... ";
2062
2063    my @channellist = get_channels();
2064
2065    print "done.\n\n" .
2066          "For each channel you want guide data for, enter an XMLTV id\n" .
2067          "of your choice (e.g. \"seven.free.au\"). If you don't need\n" .
2068          "guide data for this channel, just press Enter.\n\n" .
2069          "Please don't subscribe to unneeded channels.\n\nChannels:\n";
2070    $channels = {};
2071    my $line;
2072    foreach (@channellist)
2073    {
2074        $line = ask(" \"$_\"? ");
2075        $channels->{$_} = $line if ($line);
2076    }
2077
2078    print "\nHigh Definition TV (HDTV)\n".
2079          "Most Australian TV networks broadcast at least some\n".
2080          "programmes in HDTV each week, but for the most part\n".
2081          "either upsample SD to HD or play a rolling demonstration\n".
2082          "HD clip when they don't have the programme in HD format.\n\n".
2083          "If you have a HDTV capable system and are interested in\n".
2084          "having Shepherd's postprocessors populate HDTV content\n".
2085          "then Shepherd will need to know the XMLTV IDs for the HD\n".
2086          "channels also.\n";
2087    if (ask_boolean("\nDo you wish to include HDTV channels?")) {
2088        print "\nFor each channel you want guide data for, enter an XMLTV id\n" .
2089              "of your choice (e.g. \"sevenhd.free.au\"). If you don't need\n" .
2090              "guide data for this channel, just press Enter.\n\n";
2091
2092        $opt_channels = {};
2093        foreach (@channellist)
2094        {
2095            next if (($_ =~ /ABC2/i) || ($_ =~ /SBS News/i) || ($_ =~ /31/));
2096            $_ .= "HD";
2097            $line = ask(" \"$_\"? ");
2098            $opt_channels->{$_} = $line if ($line);
2099        }
2100    }
2101
2102
2103    print "\nHave you been running a grabber previously?\n\n".
2104          "Some data sources show the programme \"Spicks & Specks\"\n".
2105          "with an '&', some show it as \"Spicks and Specks\" with 'and'.\n".
2106          "It doesn't matter which way it is - other than if you have an existing\n".
2107          "recording policy set one way or the other, it would be preferable\n".
2108          "to not have to go and re-enter recording schedules.\n\n".
2109          "Shepherd's \"reconciler\" is smart enough to work out many subtle\n".
2110          "differences in title names and will automatically rename programmes\n".
2111          "to match the same format as previously seen.\n\n".
2112          "If you have been using a grabber in the past, if you let Shepherd\n".
2113          "know what it is, it can use that to ask the reconciler to 'learn'\n".
2114          "the subtle programme name changes between different grabbers,\n".
2115          "hopefully resulting in consistent programme names, regardless of\n".
2116          "the data source.\n\n".
2117          "Were you using one of the following data sources previously?\n".
2118          "If so, type the name of it here or leave blank if you haven't\n".
2119          "run a grabber previously or don't care about having to fix existing\n".
2120          "recording schedules.\n\n".
2121          "Choose from: ".join(", ",query_grabbers())."\nChoice: ";
2122
2123    my $pref = ask("Order? ");
2124    $pref_title_source = $pref if ($pref);
2125
2126    print "\n";
2127    show_channels();
2128    unless(ask_boolean("\nCreate configuration file?"))
2129    {
2130        print "Aborting configuration.\n";
2131        exit 0;
2132    }
2133
2134    write_config_file();
2135    write_channels_file();
2136
2137    print "Finished configuring.\n\n" .
2138          "Shepherd is installed into $CWD.\n\n";
2139   
2140    if ($invoked ne "$CWD/$progname" and $invoked =~ /$progname/)
2141    {
2142        print "Warning: you invoked this program as $invoked.\n" .
2143            "In the future, it should be run as $CWD/$progname,\n" .
2144            "to avoid constantly re-downloading the latest version.\n\n" .
2145            "MythTV users may wish to create the following symlink, by " .
2146            "doing this (as root):\n" .
2147            "\"ln -s $CWD/$progname /usr/bin/tv_grab_au\".\n\n" .
2148            "You may safely delete $invoked.\n\n";
2149    }
2150
2151    status();
2152
2153    unless (ask_boolean("\nGrab data now?"))
2154    {
2155        exit 0;
2156    }
2157}
2158
2159sub get_channels
2160{
2161    my @date = localtime;
2162    my $page = fetch_file(
2163        "http://au.tv.yahoo.com/results.html?rg=$region&dt=" .
2164        ($date[5] + 1900) . "-$date[4]-$date[3]");
2165    my @channellist;
2166    while ($page =~ /<tr class=rtb><td class=rth><a .*?>(.*?)<\/a>/g)
2167    {
2168        push @channellist, $1;
2169    }
2170    return @channellist;
2171}
2172
2173# -----------------------------------------
2174# Subs: Status & Help
2175# -----------------------------------------
2176
2177sub show_config
2178{
2179    print "\nConfiguration\n".
2180          "-------------\n" .
2181          "Config file: $config_file\n" .
2182          "Debug mode : " . is_set($debug) . "\n" .
2183          "Output file: " . ($opt->{output} ? $opt->{output} : "None") . "\n" .
2184          "Region ID  : $region\n";
2185  show_channels();
2186  print "\n";
2187  status();
2188  print "\n";
2189}
2190
2191sub show_channels
2192{
2193  print "Subscribed channels:\n";
2194  print "    $_ -> $channels->{$_}\n" for sort keys %$channels;
2195  print "Optional (HDTV) channels:\n";
2196  print "    $_ -> $opt_channels->{$_}\n" for sort keys %$opt_channels;
2197}
2198
2199sub is_set
2200{
2201    my $arg = shift;
2202    return $arg ? "Yes" : "No";
2203}
2204
2205sub pretty_print
2206{
2207    my ($p, $len) = @_;
2208    my $spaces = ' ' x (79-$len);
2209    my $ret = "";
2210
2211    while (length($p) > 0) {
2212        if (length($p) <= $len) {
2213            $ret .= $p;
2214            $p = "";
2215        } else {
2216            # find a space to the left of cutoff
2217            my $len2 = $len;
2218            while ((substr($p,$len2,1) ne ' ') && ($len2 > 0)) {
2219                $len2--;
2220            }
2221            if ($len2 == 0) {
2222                # no space - just print it with cutoff
2223                $ret .= substr($p,0,$len);
2224                $p = substr($p,$len,(length($p)-$len));
2225            } else {
2226                # print up to space
2227                $ret .= substr($p,0,$len2);
2228                $p = substr($p,($len2+1),(length($p)-$len2+1));
2229            }
2230            # print whitespace
2231            $ret .= "\n".$spaces;
2232        }
2233    }
2234    return $ret;
2235}
2236
2237sub status
2238{
2239    print "\nThe following plugins are known:\n",
2240          " Type     Name           Version Description\n".
2241          " -------- -------------- ------- ----------------------------------------------\n";
2242
2243    foreach (sort { $components->{$a}->{type} cmp $components->{$b}->{type} } keys %{$components}) {
2244        printf " %-8s %-15s%7s %46s\n",
2245          substr($components->{$_}->{type},0,8), 
2246          length($_) > 15 ? substr($_,0,13).".." : $_,
2247          ($components->{$_}->{ver} ? substr($components->{$_}->{ver},0,7) : "unknown"),
2248          (defined $components->{$_}->{config}->{desc} ?
2249            pretty_print($components->{$_}->{config}->{desc},46) : "");
2250    }
2251    printf "\n";
2252
2253    print "Grabbers, listed in order of quality:\n".
2254          "                   Enabled/\n".
2255          " Grabber        Qual Ready Last Run   Status\n" .
2256          " -------------- ---- ----- ---------- -----------------------------------------\n";
2257    my %qual_table = ( 3 => "Best", 2 => "Good", 1 => "Avg" );
2258    foreach (sort { $components->{$b}->{config}->{quality} <=> $components->{$a}->{config}->{quality} } query_grabbers()) {
2259        my $h = $components->{$_};
2260        printf  " %-15s%-4s  %1s/%1s %11s %s\n",
2261          length($_) > 15 ? substr($_,0,13).".." : $_,
2262          $qual_table{($h->{config}->{quality})},
2263          $h->{disabled} ? 'N' : 'Y',
2264          $h->{ready} ? 'Y' : 'N',
2265          $h->{lastdata} ? (strftime "%a%d%b%y", localtime($h->{lastdata})) : 'never',
2266          $h->{laststatus} ? pretty_print($h->{laststatus},41) : '';
2267    }
2268
2269    print "\n".
2270          "              Enabled/\n".
2271          " Reconciler     Ready Last Run   Status\n" .
2272          " -------------- ----- ---------- ----------------------------------------------\n";
2273    foreach (sort { $components->{$a} <=> $components->{$b} } query_reconcilers()) {
2274        my $h = $components->{$_};
2275        printf  " %-15s %1s/%1s %11s %s\n",
2276          length($_) > 15 ? substr($_,0,13).".." : $_,
2277          $h->{disabled} ? 'N' : 'Y',
2278          $h->{ready} ? 'Y' : 'N',
2279          $h->{lastdata} ? (strftime "%a%d%b%y", localtime($h->{lastdata})) : 'never',
2280          $h->{laststatus} ? pretty_print($h->{laststatus},46) : '';
2281    }
2282
2283    print "\n".
2284          "              Enabled/\n".
2285          " Postprocessor  Ready Last Run   Status\n" .
2286          " -------------- ----- ---------- ----------------------------------------------\n";
2287    foreach (sort { $components->{$a} <=> $components->{$b} } query_postprocessors()) {
2288        my $h = $components->{$_};
2289        printf  " %-15s %1s/%1s %11s %s\n",
2290          length($_) > 15 ? substr($_,0,13).".." : $_,
2291          $h->{disabled} ? 'N' : 'Y',
2292          $h->{ready} ? 'Y' : 'N',
2293          $h->{lastdata} ? (strftime "%a%d%b%y", localtime($h->{lastdata})) : 'never',
2294          $h->{laststatus} ? pretty_print($h->{laststatus},46) : '';
2295    }
2296    printf "\nPreferred titles from grabber '%s'\n",$pref_title_source if ($pref_title_source);
2297    printf "\n";
2298}
2299
2300sub help
2301{
2302    print q{
2303Command-line options:
2304    --help                Print this message
2305
2306    --status              Print a list of grabbers maintained
2307    --list                Print a detailed list of grabbers
2308    --setmirror <s>       Set URL <s> as primary location to check for updates
2309
2310    --configure           Setup
2311    --show-config         Print setup details
2312
2313    --setpreftitle <s>    Set preferred 'title' source as grabber <s>
2314    --clearpreftitle      clear preferred 'title' source
2315
2316    --disable <s>         Don't ever use grabber/postprocessor <s>
2317    --enable <s>          Okay, maybe use it again then
2318    --uninstall <s>       Remove a disabled grabber/postprocessor
2319
2320    --noupdate            Do not attempt to update before running
2321    --update              Update only; do not grab data
2322
2323    --check               Check status of all grabbers and postprocessors
2324
2325    --nolog               Don't write a logfile
2326};
2327    exit 0;
2328}
2329
2330# -----------------------------------------
2331# Subs: override handlers for standard perl.
2332# -----------------------------------------
2333
2334# ugly hack. please don't try this at home kids!
2335sub my_die {
2336    my ($arg,@rest) = @_;
2337    my ($pack,$file,$line,$sub) = caller(0);
2338
2339    # check if we are in an eval()
2340    if ($^S) {
2341        printf STDERR "  shepherd caught a die() within eval{} from file $file line $line\n";
2342    } else {
2343            printf STDERR "\nDIE: line %d in file %s\n",$line,$file;
2344            if ($arg) {
2345                CORE::die($arg,@rest);
2346            } else {
2347                CORE::die(join("",@rest));
2348            }
2349    }
2350}
Note: See TracBrowser for help on using the browser.