root/shepherd @ 180

Revision 180, 68.3 kB (checked in by max, 7 years ago)

Compliance with XMLTV options --description and --capabilities.

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