root/shepherd @ 134

Revision 134, 61.8 kB (checked in by max, 7 years ago)

Always select best grabber unless --randomize option used (experimental).

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