root/shepherd @ 38

Revision 38, 62.8 kB (checked in by max, 7 years ago)

Combined $grabbers and $postprocessors into $components to cut down
on redundant code.

Line 
1#!/usr/bin/perl -w
2
3# "Shepherd"
4
5my $version = '0.2.8';
6
7# A wrapper for various Aussie TV guide data grabbers
8#
9# Use --help for command-line options.
10# See shepherd.txt for details.
11#
12#  A current version of this script, plus a README file, might be here:
13#  http://www.whuffy.com/tv_grab_au/
14#
15# Changelog:
16# 0.1.0   : Basic self-updating and grabber management
17# 0.2.0   : --configure
18# 0.2.1   : Has a home in ~/.shepherd/
19# 0.2.2   : --check
20# 0.2.3   : Bugfix: archives correctly
21# 0.2.5   : Multi-grabber (potentially with partial data)
22# 0.2.6   : Postprocessor support
23# 0.2.7   : Changed online file structure
24# 0.2.8   : Integrated reconciler
25#
26#   have a type, and query_grabbers() and query_postprocessors() returning
27#   lists of each for easy iteration.
28
29BEGIN { *CORE::GLOBAL::die = \&my_die; }
30
31use strict;
32
33use LWP::Simple;
34use Sort::Versions;
35use Cwd;
36use Getopt::Long;
37use Data::Dumper;
38use XMLTV;
39use XMLTV::Ask;
40use DateTime::Format::Strptime;
41use POSIX qw(strftime);
42use Time::HiRes qw(gettimeofday tv_interval);
43
44# ---------------------------------------------------------------------------
45# --- Global Variables
46# ---------------------------------------------------------------------------
47
48my $progname = 'shepherd';
49
50my $HOME = 'http://www.whuffy.com/shepherd';
51
52my $invoked = Cwd::realpath($0);
53
54# By default, Shepherd runs from ~/.shepherd/. If it's not run as a user,
55# it will try /opt/shepherd/ instead.
56my $CWD = ($ENV{HOME} ? $ENV{HOME} . "/." : "/opt/") . $progname;
57-d $CWD or mkdir $CWD or die "Cannot create directory $CWD: $!";
58chdir($CWD);
59
60my $ARCHIVE_DIR = "$CWD/archive";
61
62
63#### analyzer settings ####
64
65my $timeslot_size = (15 * 60);                  # 15 minute slots
66my $channel_ok_threshold_percent = 90;          # 90% these may need to be
67                                                # tweaked but look ok for now
68my $postprocessor_ok_threshold_percent = 80;    # 80% these may need to be
69                                                # tweaked but look ok for now
70my $postprocessor_disable_failure_threshold = 5;# number of times a
71                                                # postprocessor has to fail
72                                                # in a row before it is
73                                                # automatically disabled
74
75#### reconciler settings ####
76my %reclogic;
77
78# when setting the upper-bounds of a timeslot to look for overlapping
79# programmes between grabbers, cap the upper time window that we are prepared
80# to tolerate programmes ending within.
81# disabled by default (max overrides percent for upper bounds)
82
83$reclogic{compare_overlapping_programmes_extra_overtime_duration_percent} = 5;
84$reclogic{compare_overlapping_programmes_extra_overtime_max} = 0;
85
86# if there is a conflict between two grabbers, choose the grabber
87# with the MOST programmes in the timeslot WITHOUT COUNTING
88# any programmes of <= $reclogic{min_time_override_for_duplicate}
89
90$reclogic{min_time_override_for_duplicate} = (5 * 60);  # default = 5 minutes
91
92# when we've selected one grabber to insert data from, we look at all the other
93# grabbers for overlapping fields we can use to augment our primary grabber
94# set some thresholds for when to consider programming to be the 'same'
95# (with fuzz -/+ 5 mins max), -/+ 2.5mins for programmes <= 15 mins
96
97$reclogic{duplicate_programme_augment_data_short_cutoff} = (15*60); # <=15mins = short, >15mins = long
98$reclogic{duplicate_programme_augment_data_long_duration_threshold} = (5*60); # +/-5 mins
99$reclogic{duplicate_programme_augment_data_short_duration_threshold} = (2.5*60); # +/- 2min30
100
101# for any programmes we didn't choose, reset the starttime up to a maximum
102# window of 5 minutes to the same value as our "chosen" endtime.
103# if can't readjust, then ignore (remove) that programme
104
105$reclogic{readjust_starttime_for_nonmatched_programmes} = (5*60);
106
107#### end reconciler settings ####
108
109my $opt;
110my $pref_order;
111my $mirror_site;
112my $made_changes = 0;
113my $debug = 1;
114my $recdebug = 0;
115my $components = { };
116my $gscore;
117my $region;
118my $channels;
119my $config_file =   "$CWD/$progname.conf";
120my $channels_file = "$CWD/channels.conf";
121my $days = 7;
122
123# postprocessing
124my $title_translation_table = { };
125my $langs = [ 'en' ];
126my $num_timeslots;
127my $plugin_data = { };
128my $channel_data = { };
129my $starttime, my $endtime;
130my $input_postprocess_file = "$CWD/reconciled_output_pre_postprocessors.xml";
131my $insufficient_grabber_data = 0;
132
133# OBSOLETE: will be removed
134my $grabbers;
135my $postprocessors;
136my $preferred;
137
138# ---------------------------------------------------------------------------
139# --- Setup
140# ---------------------------------------------------------------------------
141
142print ucfirst($progname) . " v$version\n\n";
143#print "Cwd: $CWD.\n";
144
145# Any options Shepherd doesn't understand, we'll pass to the grabber(s)
146Getopt::Long::Configure(qw/pass_through/);
147
148get_initial_command_line_options();
149
150help() if ($opt->{help});
151
152unless ($opt->{configure})
153{
154    read_config_file();
155    read_channels_file();
156}
157
158get_remaining_command_line_options();
159
160if ($opt->{status})
161{
162    status();
163    exit;
164}
165
166if ($opt->{show_config})
167{
168    show_config();
169    exit;
170}
171
172if ($opt->{enable})
173{
174    enable($opt->{enable});
175}
176
177if ($opt->{disable})
178{
179    disable($opt->{disable});
180}
181
182if ($opt->{setorder})
183{
184    set_order(0, $opt->{setorder}); 
185}
186
187if ($opt->{check})
188{
189    check();
190}
191
192if ($opt->{enable} or
193    $opt->{disable} or
194    $opt->{setorder} or
195    $opt->{check} or
196    $opt->{mirror})
197{
198    set_order(1) if $made_changes;
199    write_config_file() if $made_changes;
200    status();
201    exit;
202}
203
204# ---------------------------------------------------------------------------
205# --- Update
206# ---------------------------------------------------------------------------
207
208unless ($opt->{noupdate})
209{
210    update($progname, $version);
211    set_order(1) if $made_changes;
212    write_config_file() if (($made_changes) && (! $opt->{configure}))
213}
214
215if ($opt->{configure})
216{
217    configure();
218}
219
220# ---------------------------------------------------------------------------
221# --- Go!
222# ---------------------------------------------------------------------------
223
224unless ($opt->{update})
225{
226    calc_date_range();
227    grab_data();
228    reconcile_data();
229    postprocess_data();
230    output_data();
231}
232
233print "Done.\n";
234
235status();
236write_config_file();
237
238# ---------------------------------------------------------------------------
239# --- Subroutines
240# ---------------------------------------------------------------------------
241
242# -----------------------------------------
243# Subs: Grabbing
244# -----------------------------------------
245
246sub grab_data
247{
248    my $used_grabbers = 0;
249    my $need_more_data = 1;
250
251    print "\nGrabber stage:\n";
252
253    my $grabber;
254   
255    while ($grabber = choose_grabber())
256    {
257        $used_grabbers++;
258
259        $components->{$grabber}->{lastdata} = time;
260        $components->{$grabber}->{laststatus} = "unknown";
261
262        printf "\nSHEPHERD: Using grabber: (%d) %s\n", $used_grabbers, $grabber;
263
264        my $output = "$CWD/grabbers/$grabber/output.xmltv";
265
266        my $comm = "$CWD/grabbers/$grabber/$grabber " .
267                   "--region $region " .
268                   "--channels_file $channels_file " .
269                   "--output $output";
270
271        # NOTE: ideally a grabber could be instructed to fetch partial data through --channel, --starttime & --endtime
272        # we don't have that for now so instead whenever there is missing data, ALL 7 days for all channels will be collected
273        # FIXME FUTURE: call grabbers just with what we want...
274        $comm .= " --days $days" if ($days);
275        $comm .= " --offset $opt->{offset}" if ($opt->{offset});
276        $comm .= " --debug" if ($debug);
277        $comm .= " @ARGV" if (@ARGV);
278
279        if ((defined $opt->{dontcallgrabbers}) && ($opt->{dontcallgrabbers})) {
280            printf "SHEPHERD: not calling grabber because of --dontcallgrabbers option, but will instead use existing $output\n";
281        } else {
282            print "SHEPHERD: Excuting command: $comm\n";
283            chdir "$CWD/grabbers/$grabber/";
284            system($comm);
285            chdir $CWD;
286        }
287
288        # soak up the data we just collected
289        &soak_up_data($grabber, $output, "grabber");
290        $components->{$grabber}->{laststatus} = $plugin_data->{$grabber}->{laststatus};
291
292        # check to see if we have all the data we want
293        $need_more_data = &analyze_plugin_data($channel_ok_threshold_percent, "AGGREGATE GRABBER");
294
295        last if (!$need_more_data);
296    }
297
298
299    if ($used_grabbers == 0)
300    {
301        print "No valid grabbers installed/enabled!\n";
302        return;
303    }
304
305    if ($need_more_data)
306    {
307        print "SHEPHERD: Ran through ALL grabbers but still missing data!!! :(\n";
308        $insufficient_grabber_data = 1;
309        return;
310    }
311}
312
313sub choose_grabber
314{
315    unless (defined $gscore)
316    {
317        foreach (query_grabbers())
318        {
319            unless ($components->{$_}->{disabled})
320            {
321                $gscore->{$_} = 0;
322            }
323        }
324    }
325
326    my $total = score_grabbers();
327
328    return undef unless ($total);
329
330    if ($debug)
331    {
332        print "Grabber selection probabilities:\n";
333        foreach (keys %$gscore)
334        {
335            printf "%15s %6.1f%% %12s\n", 
336                   $_, 100 * $gscore->{$_} / $total, "($gscore->{$_} pts)";
337        }
338    }
339
340    return undef unless (scalar keys %$gscore);
341
342    my $r = int(rand($total));
343#    print "Total score: $total.\nRand: $r.\n";
344
345    my $c = 0;
346    foreach (keys %$gscore)
347    {
348        next if (!$gscore->{$_});
349        if ($r >= $c and $r < ($c + $gscore->{$_}))
350        {
351            delete $gscore->{$_};
352            print "Selected $_.\n" if ($debug);
353            return $_;
354        }
355        $c += $gscore->{$_};
356    }
357    die "ERROR: failed to choose grabber.";
358}
359
360sub score_grabbers
361{
362    my ($score, $total, $niceness, $granularity, $m);
363
364    my $missing = detect_missing_data();
365
366    foreach my $grabber (keys %$gscore)
367    {
368        # for each slot, say whether we can fill it or not -- that is,
369        # whether we support this channel and this day #.
370       
371        my $hits = 0;
372        foreach (@$missing)
373        {
374            my ($ch, $day) = split(/:/);
375            my $result = (can_support_channel($grabber, $ch)
376                          and
377                          can_support_day($grabber, $day));
378            $hits += $result;
379#           print "$grabber vs $ch:$day: " . ($result ? "OK" : "no") . "\n";
380        }
381        $niceness = $components->{$grabber}->{config}->{niceness};
382        unless ($niceness)
383        {
384            print "WARNING: Grabber $grabber has no niceness support " .
385                  "specified in config.\n";
386            $niceness = 5;
387        }
388        $granularity = $components->{$grabber}->{config}->{granularity};
389        unless (defined $granularity)
390        {
391            print "WARNING: Grabber $grabber has no granularity support " .
392                "specified in config.\n";
393            $granularity = '';
394        }
395        # TODO: use granularity more intelligently ('c' vs 'd') -- Max.
396        $granularity = length ($granularity);
397        my $total_channeldays = $days * scalar (keys %$channels);
398        $granularity *= int((($total_channeldays - 1 ) / scalar(@$missing))/2);
399
400        $score = $hits * ($niceness + $granularity);
401        print "Grabber $grabber can fill $hits empty slots with $niceness niceness and $granularity granularity: scoring $score pts.\n";
402        $gscore->{$grabber} = $score;
403        $total += $score;
404    }
405    return $total;
406}
407
408sub can_support_channel
409{
410    my ($grabber, $ch) = @_;
411
412    my $channels_supported = $components->{$grabber}->{config}->{channels};
413    unless (defined $channels_supported)
414    {
415        print "WARNING: Grabber $grabber has no channel support " .
416              "specified in config.\n";
417        $channels_supported = '';
418    }
419
420    return 1 unless ($channels_supported); # Empty string means we support all
421   
422    my $match = ($channels_supported =~ /\b$ch\b/);
423#    $match ||= 0;
424    my $exceptions = ($channels_supported =~/^-/);
425#    $exceptions ||= 0;
426#       print "M: " . Dumper ($match) . "E:" . Dumper ($exceptions) . "\n";
427#    print "Can $grabber support channel $ch: " . ($match != $exceptions ? 1 : 0) .
428        #      " (match $match, exceptions: $exceptions).\n";
429    return ($match != $exceptions);
430}
431
432sub can_support_day
433{
434    my ($grabber, $day) = @_;
435
436    my $days_supported = $components->{$grabber}->{config}->{max_days};
437    unless ($days_supported)
438    {
439        print "WARNING: Grabber $grabber has no max_days support " .
440              "specified in config.\n";
441        $days_supported = 2;
442    }
443#    print "Can $grabber support day $day: " . ($day <= $days_supported) . ".\n";
444    return $day <= $days_supported;
445}
446
447#
448# Build a little hash of what channel/day data we're currently missing.
449# I think granularity of one day is good for now; could possibly be
450# made more fine-grained if we think grabbers will support that.
451#
452sub detect_missing_data
453{
454    my @missing;
455
456    my $timeslots_per_day = (24 * 60 * 60) / $timeslot_size;
457
458    foreach my $ch (keys %$channels)
459    {
460        if (defined $channel_data->{$ch})
461        {
462            my $slotnum;
463            for ($slotnum = 0; $slotnum < $num_timeslots-1; $slotnum++) 
464            {
465                if (!@{$channel_data->{$ch}->{timeslots}}[$slotnum])
466                {
467                    my $day = int($slotnum / $timeslots_per_day) + 1;
468                    push @missing, "$ch:$day";
469                    $slotnum += $timeslots_per_day -
470                                ($slotnum % $timeslots_per_day);
471
472                }
473            }
474        }
475        else
476        {
477            for my $i (1 .. $days)
478            {
479                push (@missing, "$ch:$i");
480            }
481        }
482
483    }
484
485    print "Need data for @missing.\n" if ($debug);
486    return \@missing;
487}
488
489
490# interpret xmltv data from this grabber/postprocessor
491sub soak_up_data
492{
493    my ($plugin, $output, $plugintype) = @_;
494
495    if (! -r $output) {
496        printf "SHEPHERD: Warning: plugin '%s' output file '%s' does not exist\n",$plugin,$output;
497        return;
498    }
499
500    my $parse_start_time = [gettimeofday];
501    printf STDERR "SHEPHERD: Started parsing XMLTV from '%s' in '%s' .. any errors below are from parser:\n",$plugin,$output;
502    eval { $plugin_data->{$plugin}->{xmltv} = XMLTV::parsefiles($output); };
503    printf STDERR "SHEPHERD: Completed XMLTV parsing from '%s' in %0.2f seconds\n",$plugin,tv_interval($parse_start_time);
504
505    if (defined $plugin_data->{$plugin}->{xmltv}) {
506        $plugin_data->{$plugin}->{valid} = 1;
507
508        my $xmltv = $plugin_data->{$plugin}->{xmltv};
509        my ($encoding, $credits, $chan, $progs) = @$xmltv;
510        $plugin_data->{$plugin}->{total_duration} = 0;
511        $plugin_data->{$plugin}->{programmes} = 0;
512
513        my $strptime = new DateTime::Format::Strptime( pattern => "%Y%m%d%H%M %z");
514        my $alt_strptime = new DateTime::Format::Strptime( pattern => "%Y%m%d%H%M"); # alternate format 1: oztivo doesn't seem to output timezone
515        my $seen_channels_with_data = 0;
516
517        # iterate thru channels
518        foreach my $ch (sort keys %{$channels}) {
519            my $seen_progs_on_this_channel = 0;
520
521            # iterate thru programmes per channel
522            foreach my $prog (@$progs) {
523                next if ($prog->{channel} ne $channels->{$ch});
524
525                my $t1 = $strptime->parse_datetime($prog->{start});
526                $t1 = $alt_strptime->parse_datetime($prog->{start}) if (!$t1);
527
528                my $t2 = $strptime->parse_datetime($prog->{stop});
529                $t2 = $alt_strptime->parse_datetime($prog->{stop}) if (!$t2);
530
531                next if (!$t1 || !$t2); # if we can't parse stop/start then clearly THIS data is bunk!
532
533                # store t1 and t2 times in the xmltv data for later on (shh.. ton't tell anyone..)
534                $prog->{start_epoch} = $t1->epoch;
535                $prog->{stop_epoch} = $t2->epoch;
536
537                # store plugin-specific stats
538                $plugin_data->{$plugin}->{programmes}++;
539                $plugin_data->{$plugin}->{total_duration} += ($t2->epoch - $t1->epoch);
540                $seen_progs_on_this_channel++;
541                $plugin_data->{$plugin}->{earliest_data_seen} = $t1->epoch if (!defined $plugin_data->{$plugin}->{earliest_data_seen});
542                $plugin_data->{$plugin}->{earliest_data_seen} = $t1->epoch if ($t1->epoch < $plugin_data->{$plugin}->{earliest_data_seen});
543                $plugin_data->{$plugin}->{latest_data_seen} = $t2->epoch if (!defined $plugin_data->{$plugin}->{latest_data_seen});
544                $plugin_data->{$plugin}->{latest_data_seen} = $t2->epoch if ($t2->epoch > $plugin_data->{$plugin}->{latest_data_seen});
545
546                # store channel-specific stats
547                $channel_data->{$ch}->{programmes}++;
548                $channel_data->{$ch}->{total_duration} += ($t2->epoch - $t1->epoch);
549
550                # store timeslot info
551                next if ($t1->epoch > $endtime);        # programme starts after timeslots we are interested .. nice that we have it ... but we really don't care about it!
552                next if ($t2->epoch < $starttime);      # programme ends  before timeslots we are interested .. nice that we have it ... but we really don't care about it!
553                my $start_slotnum;
554                if ($t1->epoch >= $starttime) {
555                    $start_slotnum = int(($t1->epoch - $starttime) / $timeslot_size);
556                } else {
557                    $start_slotnum = 0;
558                }
559                my $end_slotnum;
560                if ($t2->epoch < $endtime) {
561                    $end_slotnum = int(($t2->epoch - $starttime) / $timeslot_size);
562                } else {
563                    $end_slotnum = ($num_timeslots-1);
564                }
565
566                # add this programme into the global timeslots table for this channel
567                foreach my $slotnum ($start_slotnum..$end_slotnum) {
568                    $channel_data->{$ch}->{timeslots}[$slotnum]++;
569                }
570            }
571
572            $seen_channels_with_data++ if ($seen_progs_on_this_channel > 0);
573        }
574
575        # print some stats about what we saw!
576        printf "SHEPHERD: %s '%s' returned data for %d channels, %d programmes, %dd%02dh%02dm%02ds duration, %s%s\n",
577            ucfirst($plugintype), $plugin, $seen_channels_with_data, $plugin_data->{$plugin}->{programmes},
578            int($plugin_data->{$plugin}->{total_duration} / 86400),             # days
579            int(($plugin_data->{$plugin}->{total_duration} % 86400) / 3600),    # hours
580            int(($plugin_data->{$plugin}->{total_duration} % 3600) / 60),       # mins
581            int($plugin_data->{$plugin}->{total_duration} % 60),                # sec
582            (defined $plugin_data->{$plugin}->{earliest_data_seen} ? (strftime "%a %e %b %H:%M - ", localtime($plugin_data->{$plugin}->{earliest_data_seen})) : 'no data'),
583            (defined $plugin_data->{$plugin}->{latest_data_seen} ? (strftime "%a %e %b %H:%M", localtime($plugin_data->{$plugin}->{latest_data_seen})) : '');
584        $plugin_data->{$plugin}->{laststatus} = sprintf "%dch/%dpr/%dhrs %s-%s",
585            $seen_channels_with_data, $plugin_data->{$plugin}->{programmes},
586            int($plugin_data->{$plugin}->{total_duration} / 3600),
587            (defined $plugin_data->{$plugin}->{earliest_data_seen} ? (strftime "%a%d%b%H:%M", localtime($plugin_data->{$plugin}->{earliest_data_seen})) : 'no'),
588            (defined $plugin_data->{$plugin}->{latest_data_seen} ? (strftime "%a%d%b%H:%M", localtime($plugin_data->{$plugin}->{latest_data_seen})) : 'data');
589
590    } else {
591        printf "WARNING: Plugin %s didn't seem to return any valid XMLTV!\n",$plugin;
592        delete $plugin_data->{$plugin}->{valid};
593    }
594}
595
596
597# analyze grabber data - do we have all the data we want?
598# returns 1 if we need more data, 0 if we have all we want
599sub analyze_plugin_data
600{
601    my ($threshold,$analysistype) = @_;
602    my $retval = 0; # until proven otherwise
603    my $total_data_percent = 0, my $total_channels = 0;
604    my $statusstring = "";
605
606    # iterate across each channel
607    foreach my $ch (sort keys %{$channels}) {
608        $total_channels++;
609        if (defined $channel_data->{$ch}) {
610            my $data_in_channel = 0;
611            for my $slotnum (0..($num_timeslots-1)) {
612                $data_in_channel++ if ((defined $channel_data->{$ch}->{timeslots}[$slotnum]) && ($channel_data->{$ch}->{timeslots}[$slotnum] > 0));
613            }
614
615            # do we have enough data for this channel?
616            my $data_in_channel_percent = $data_in_channel / ($num_timeslots-1) * 100;
617            if ($data_in_channel_percent >= $threshold) {
618                $statusstring .= sprintf "%s: %0.1f%% [complete], ",$ch,$data_in_channel_percent;
619            } else {
620                $statusstring .= sprintf "%s: %0.1f%% [hungry], ",$ch,$data_in_channel_percent;
621                $retval = 1;
622            }
623            $total_data_percent += $data_in_channel_percent;
624        } else {
625            $statusstring .= sprintf "%s: 0%% [starving], ",$ch;
626            $retval = 1;
627        }
628    }
629
630    if ($total_channels > 0) {
631        $total_data_percent = $total_data_percent / $total_channels;
632    } else {
633        $total_data_percent = 0;
634    }
635
636    # print some stats about what our analysis says!
637    printf "SHEPHERD: %s ANALYSIS: %sTOTAL %0.2f%% %s %0.2f%%: %s\n",
638        uc($analysistype), $statusstring, $total_data_percent,
639        ($total_data_percent >= $channel_ok_threshold_percent ? ">" : "<"), $channel_ok_threshold_percent,
640        ($retval ? "WANT MORE DATA" : "COMPLETE");
641    return $retval;
642}
643
644
645# work out date range we are expecting data to be in
646sub calc_date_range
647{
648    # normalize starttime to beginning of hour
649    my $now = time;
650    my ($sec,$min,@rest) = localtime($now);
651
652    $starttime = $now - ((60 * $min) + $sec);
653
654    if ($days) {
655        $endtime = $starttime + ($days * 86400);
656    } else {
657        $endtime = $starttime + (7*86400);
658    }
659    $starttime += (86400 * $opt->{offset}) if ($opt->{offset});
660
661    $num_timeslots = ($endtime - $starttime) / $timeslot_size;
662}
663
664# -----------------------------------------
665# Subs: Reconciling data
666# -----------------------------------------
667
668# for all the data we have, try to pick the best bits!
669sub reconcile_data
670{
671    printf "\nReconciling data:\n\n";
672
673    my @proglist = [ ];
674    my @position_pointer = [ ];
675    my @grabber_order = [ ];
676    my $num_grabbers = 0;
677    my $pref_order;
678
679    printf "Preference for whose data we prefer as follows:\n";
680    foreach my $proggy (sort { $components->{$a}->{order} <=> $components->{$b}->{order} } query_grabbers()) {
681        if ((!$components->{$proggy}->{disabled}) && ($plugin_data->{$proggy}) && ($plugin_data->{$proggy}->{valid})) {
682            $grabber_order[$num_grabbers] = $proggy;
683            my $orig_prog = $plugin_data->{$proggy}->{xmltv}->[3];
684            my $prognum = 0;
685            foreach my $new_prog (sort order_channel_time @{$orig_prog}) {
686                $proglist[$num_grabbers]->[$prognum] = $new_prog;
687                $prognum++;
688            }
689
690            printf "  %d. %s (%d programmes)\n",($num_grabbers+1),$proggy,$prognum;
691            $num_grabbers++;
692        }
693    }
694
695    my %writer_args = ( encoding => 'ISO-8859-1' );
696    my $fh = new IO::File(">$input_postprocess_file") || die "can't open outputfile $input_postprocess_file: $!";
697    $writer_args{OUTPUT} = $fh;
698    my $writer = new XMLTV::Writer(%writer_args);
699
700    $writer->start({'source-info-url' => "about:blank", 'source-info-name' => "$progname $version", 'generator-info-name' => "$progname $version"} );
701
702    for my $ch (sort keys %$channels) {
703        $writer->write_channel({'display-name' => [[ $ch, "en" ]], 'id' => $channels->{$ch}} );
704    }
705
706    for my $ch (sort keys %$channels) {
707        printf "Reconciling channel: %s (%s)\n",$ch,$channels->{$ch};
708
709        #
710        # 1. position pointers to first piece of data for this channel
711        #
712
713        printf "REC#1: processing channel %s\n",$channels->{$ch} if $recdebug;
714        my $position_pointer;
715        for (my $i=0; $i < $num_grabbers; $i++) {
716            $position_pointer[$i] = 0;
717            while ((defined $proglist[$i]->[($position_pointer[$i])]) && ($proglist[$i]->[($position_pointer[$i])]->{'channel'} ne $channels->{$ch})) {
718                $position_pointer[$i]++;
719            }
720            if (!defined $proglist[$i]->[($position_pointer[$i])]) {
721                $position_pointer[$i] = -1;
722                printf "REC#1: no programmes found for channel %s from gradder %d\n",$channels->{$ch},$i if $recdebug;
723            } else {
724                printf "REC#1: advanced position_pointer to %d for grabber %d (first programme is \"%s\", start %d, stop %d)\n",
725                    $position_pointer[$i],$i,${XMLTV::best_name($langs,$proglist[$i]->[($position_pointer[$i])]->{title})}[0],
726                    $proglist[$i]->[($position_pointer[$i])]->{start_epoch},$proglist[$i]->[($position_pointer[$i])]->{stop_epoch} if $recdebug;
727            }
728        }
729
730        my $all_done_on_this_channel;
731        do {
732            #
733            # 2. find 'earliest' programme from all the choices
734            #
735
736            $all_done_on_this_channel = 1; # unless proven otherwise
737            my $earliest_programme_time = undef;
738            my $earliest_programme_slot = undef;
739            for (my $i=0; $i < $num_grabbers; $i++) {
740                next if ($position_pointer[$i] == -1); # skip if no programmes on this channel from this grabber
741                my $this_programme = $proglist[$i]->[($position_pointer[$i])];
742
743                if ((!defined $this_programme) || (!defined $this_programme->{title}) || ($this_programme->{channel} ne $channels->{$ch})) {
744                    # no more programmes on this channel for this grabber!
745                    printf "REC#2: no more programmes on grabber %d for this channel\n",$i if $recdebug;
746                    $position_pointer[$i] = -1;
747                } else {
748                    if ((!defined $earliest_programme_time) || ($earliest_programme_time > $this_programme->{'start_epoch'})) {
749                        $earliest_programme_time = $this_programme->{'start_epoch'};
750                        $earliest_programme_slot = $i;
751                        printf "REC#2: earliest programme (so far) on grabber %d, start %d (end %d) programme \"%s\"\n",
752                            $i,$this_programme->{start_epoch},$this_programme->{stop_epoch},${XMLTV::best_name($langs,$this_programme->{title})}[0] if $recdebug;
753                    } else {
754                        printf "REC#2:    programme on grabber %d was not earlier start %d (end %d) programme \"%s\"\n",
755                            $i,$this_programme->{start_epoch},$this_programme->{stop_epoch},${XMLTV::best_name($langs,$this_programme->{title})}[0] if $recdebug;
756                    }
757                }
758            }
759            if (!defined $earliest_programme_slot) {
760                # no programmes available on ANY grabber for this channel, skip this channel
761                printf "REC#2: no programmes on any grabbers for channel %s, skipping this channel\n",$channels->{$ch} if $recdebug;
762            } else {
763                #
764                # 3a. compare how many programmes on other grabbers overlap with it
765                # TODO (FUTURE): enhance this to do "majority voting" acrosa all grabbers based on identical start/stop
766                # TODO (FUTURE): where star/stop times match exactly but title names dont, record the mapping for known-as_to_most-preferred-title so we can use this in future transformations
767
768                my $preferred_programme = $proglist[$earliest_programme_slot]->[($position_pointer[$earliest_programme_slot])];
769                my $startpoint = $preferred_programme->{'start_epoch'};
770                my $stoppoint = $preferred_programme->{'stop_epoch'};
771                my $duration = $stoppoint - $startpoint;
772                my $extraduration = 0;
773                $extraduration = int($duration * ($reclogic{compare_overlapping_programmes_extra_overtime_duration_percent}/100)) if ($reclogic{compare_overlapping_programmes_extra_overtime_duration_percent} > 0);
774                $extraduration = $reclogic{compare_overlapping_programmes_extra_overtime_max} if ($extraduration > $reclogic{compare_overlapping_programmes_extra_overtime_max}); # upper limit
775                $stoppoint += $extraduration;
776                printf "REC#3: comparing other grabbers to see how many programmes fit in timeslot %d to %d (%d+%d)\n",$startpoint,$stoppoint,$duration,$extraduration if $recdebug;
777
778                my $max_progs_found = 0;
779                my $max_progs_slot = undef;
780                for (my $i=0; $i < $num_grabbers; $i++) {
781                    next if ($i == $earliest_programme_slot);
782                    next if ($position_pointer[$i] == -1);
783                    my $position_offset = 0;
784                    my $progs_found = 0;
785
786                    while ((defined $proglist[$i]->[($position_pointer[$i]+$position_offset)]) &&
787                           ($proglist[$i]->[($position_pointer[$i]+$position_offset)]->{'channel'} eq $channels->{$ch}) &&
788                           ($proglist[$i]->[($position_pointer[$i]+$position_offset)]->{'start_epoch'} >= $startpoint) &&
789                           ($proglist[$i]->[($position_pointer[$i]+$position_offset)]->{'stop_epoch'} <= $stoppoint)) {
790                        my $this_prog_duration = $proglist[$i]->[($position_pointer[$i]+$position_offset)]->{'stop_epoch'} - $proglist[$i]->[($position_pointer[$i]+$position_offset)]->{'start_epoch'};
791
792                        printf "REC#3a.  programme on grabber %d matched (start %d end %d), \"%s\"%s\n", $i,
793                            $proglist[$i]->[($position_pointer[$i]+$position_offset)]->{'start_epoch'},
794                            $proglist[$i]->[($position_pointer[$i]+$position_offset)]->{'stop_epoch'},
795                            ${XMLTV::best_name($langs,$proglist[$i]->[($position_pointer[$i]+$position_offset)]->{'title'})}[0],
796                            ($this_prog_duration <= $reclogic{min_time_override_for_duplicate} ? 
797                            ", but ignored because of min_time_override_for_duplicate ($reclogic{min_time_override_for_duplicate} sec)" : "") if $recdebug;
798
799                        $progs_found++ if ($this_prog_duration > $reclogic{min_time_override_for_duplicate});
800                        $position_offset++;
801                    }
802
803                    printf "REC#3a:  %d programmes on grabber %d within timeslot\n",$progs_found,$i if $recdebug;
804                    if ($progs_found > $max_progs_found) {
805                        $max_progs_found = $progs_found;
806                        $max_progs_slot = $i;
807                    }
808                }
809
810                # 3b. if there are 2 or more programmes on other channels, use those - otherwise use this one
811                if ((!defined $max_progs_slot) || ($max_progs_found <= 1)) {
812                    printf "REC#3b: no grabbers with more programmes in timeslot found\n" if $recdebug;
813                } else {
814                    printf "REC#3b: grabber %d has %d programmes between %d and %d - using THAT grabber as our preference now!\n",
815                        $max_progs_slot,$max_progs_found,$startpoint,$stoppoint if $recdebug;
816                    $earliest_programme_slot = $max_progs_slot;
817                }
818
819                #
820                # 4. populate our "reconciled" programme list with the programming chosen
821                #
822
823                my $chosen_prog = $proglist[$earliest_programme_slot]->[($position_pointer[$earliest_programme_slot])];
824                my $chosen_prog_name = ${XMLTV::best_name($langs,$chosen_prog->{'title'})}[0];
825                $startpoint = $chosen_prog->{'start_epoch'};
826                $stoppoint = $chosen_prog->{'stop_epoch'};
827                my $new_prog_entry = $chosen_prog;
828
829                printf "REC#4: chosen programme is from grabber %d: start %d, end %d, duration %d: \"%s\"\n",
830                    $earliest_programme_slot,$startpoint,$stoppoint,($stoppoint-$startpoint),$chosen_prog_name if $recdebug;
831
832                #
833                # 5a. see if we have it duplicated from multiple grabbers, first with exact match
834                # 5b. then with fuzz -/+ 5 mins max, -/+ 2.5mins for programmes <= 15 mins
835               
836                my $start1, my $start2, my $stop1, my $stop2;
837
838                if (($stoppoint - $startpoint) <= $reclogic{duplicate_programme_augment_data_short_cutoff}) {
839                    $start1 = $startpoint - $reclogic{duplicate_programme_augment_data_short_duration_threshold};
840                    $start2 = $startpoint + $reclogic{duplicate_programme_augment_data_short_duration_threshold};
841                    $stop1 = $stoppoint - $reclogic{duplicate_programme_augment_data_short_duration_threshold};
842                    $stop2 = $stoppoint + $reclogic{duplicate_programme_augment_data_short_duration_threshold};
843                } else {
844                    $start1 = $startpoint - $reclogic{duplicate_programme_augment_data_long_duration_threshold};
845                    $start2 = $startpoint + $reclogic{duplicate_programme_augment_data_long_duration_threshold};
846                    $stop1 = $stoppoint - $reclogic{duplicate_programme_augment_data_long_duration_threshold};
847                    $stop2 = $stoppoint + $reclogic{duplicate_programme_augment_data_long_duration_threshold};
848                }
849                if ($start2 >= $stop1) {
850                    $start2 = $startpoint;
851                    $stop1 = $stoppoint;
852                }
853
854                printf "REC#5a: looking in other grabbers for matching programmes within timeslot start %d and end %d (%d)\n",$startpoint,$stoppoint,($stoppoint-$startpoint) if $recdebug;
855                for (my $i=0; $i < $num_grabbers; $i++) {
856                    next if ($i == $earliest_programme_slot);
857                    next if ($position_pointer[$i] == -1);
858                    if ((defined $proglist[$i]->[($position_pointer[$i])]) &&
859                        ($proglist[$i]->[($position_pointer[$i])]->{'channel'} eq $channels->{$ch}) &&
860                        ($proglist[$i]->[($position_pointer[$i])]->{'start_epoch'} == $startpoint) &&
861                        ($proglist[$i]->[($position_pointer[$i])]->{'stop_epoch'} == $stoppoint)) {
862                        # winner .. matches our criteria ...
863                        my $match_prog = $proglist[$i]->[($position_pointer[$i])];
864                        my $match_prog_name = ${XMLTV::best_name($langs,$match_prog->{'title'})}[0];
865
866                        printf "REC#5a:   found programme on grabber %d: start %d, end %d: \"%s\"\n", $i,
867                            $match_prog->{'start_epoch'},$match_prog->{'stop_epoch'},$match_prog_name if $recdebug;
868
869                        if ($chosen_prog_name ne $match_prog_name) {
870                            # names differ - save it in our translation table for future reference!
871                            $title_translation_table->{$grabber_order[$i]}->{$match_prog_name} = $chosen_prog_name;
872                            printf "REC#5a: title-translation: higher-preference grabber '%s' called programme \"%s\" compared to '%s' calling it \"%s\"\n",
873                                $grabber_order[$earliest_programme_slot],$chosen_prog_name,$grabber_order[$i],$chosen_prog_name if $recdebug;
874                        }
875
876                        foreach my $field (keys %{$match_prog}) {
877                            next if ($field eq "start_epoch");
878                            next if ($field eq "stop_epoch");
879                            if (!defined $new_prog_entry->{$field}) {
880                                printf "REC#5a:      adding field \"%s\"\n",$field if $recdebug;
881                                $new_prog_entry->{$field} = $match_prog->{$field};
882                                # TODO (FUTURE): should we add to programme description to say where we got what data from?
883                            }
884                        }
885                    }
886                }
887
888                printf "REC#5b: looking in other grabbers for matching programmes within timeslot start %d-%d (%d) and end %d-%d (%d)\n",
889                    $start1,$start2,($start2-$start1),$stop1,$stop2,($stop2-$stop1) if $recdebug;
890
891                for (my $i=0; $i < $num_grabbers; $i++) {
892                    next if ($i == $earliest_programme_slot);
893                    next if ($position_pointer[$i] == -1);
894
895                    if ((defined $proglist[$i]->[($position_pointer[$i])]) &&
896                        ($proglist[$i]->[($position_pointer[$i])]->{'channel'} eq $channels->{$ch}) &&
897                        ($proglist[$i]->[($position_pointer[$i])]->{'start_epoch'} >= $start1) &&
898                        ($proglist[$i]->[($position_pointer[$i])]->{'start_epoch'} < $start2) &&
899                        ($proglist[$i]->[($position_pointer[$i])]->{'stop_epoch'} >= $stop1) &&
900                        ($proglist[$i]->[($position_pointer[$i])]->{'stop_epoch'} < $stop2)) {
901                        # winner .. matches our criteria ...
902                        my $match_prog = $proglist[$i]->[($position_pointer[$i])];
903
904                        printf "REC#5b:   found programme on grabber %d: start %d, end %d: \"%s\"\n", $i,
905                            $match_prog->{'start_epoch'},$match_prog->{'stop_epoch'},
906                            ${XMLTV::best_name($langs,$match_prog->{'title'})}[0] if $recdebug;
907
908                        foreach my $field (keys %{$match_prog}) {
909                            # 5b. pick fields from each one in order of our preferences
910                            next if ($field eq "start_epoch");
911                            next if ($field eq "stop_epoch");
912                            if (!defined $new_prog_entry->{$field}) {
913                                printf "REC#5b:      adding field \"%s\"\n",$field if $recdebug;
914                                $new_prog_entry->{$field} = $match_prog->{$field};
915                                # TODO (FUTURE): should we add to programme description to say where we got what data from?
916                            }
917                        }
918                    }
919                }
920
921                #
922                # 6.  write out new entry
923                #
924
925                printf "REC#6: writing out programme entry\n" if $recdebug;
926                &cleanup($new_prog_entry);
927
928                # scrub programme for known bogosities
929
930                # oztivo typically inserts blank 'director' details into 'credits' .. scrub them
931                if ((defined $new_prog_entry->{'credits'}) &&
932                    (defined $new_prog_entry->{'credits'}->{'director'}) &&
933                    (defined $new_prog_entry->{'credits'}->{'director'}->[0])) {
934                    my @director_list = $new_prog_entry->{'credits'}->{'director'}->[0];
935                    for my $i (0 .. $#director_list) {
936                        delete $new_prog_entry->{'credits'}->{'director'}->[$i] if ((defined $director_list[$i]) && ($director_list[$i] eq ""));
937                    }
938                }
939
940                # want to keep epoch start/stop for our own processing, but stop XMLTV whining about it in write_programme
941                # so temporarily remove them & reinsert them back afterwards
942                my ($orig_start_epoch,$orig_end_epoch) = ($new_prog_entry->{'start_epoch'},$new_prog_entry->{'stop_epoch'});
943                delete $new_prog_entry->{'start_epoch'};
944                delete $new_prog_entry->{'stop_epoch'};
945
946                # write out
947                $writer->write_programme($new_prog_entry);
948                ($new_prog_entry->{'start_epoch'},$new_prog_entry->{'stop_epoch'}) = ($orig_start_epoch,$orig_end_epoch);
949
950                # 7a. remove all programmes that end before this endtime
951                for (my $i=0; $i < $num_grabbers; $i++) {
952                    next if ($position_pointer[$i] == -1);
953                    while ((defined $proglist[$i]->[($position_pointer[$i])]) &&
954                           ($proglist[$i]->[($position_pointer[$i])]->{'channel'} eq $channels->{$ch}) &&
955                           ($proglist[$i]->[($position_pointer[$i])]->{'stop_epoch'} <= $stoppoint)) {
956                        printf "REC#7a: removing programme on grabber %d slot %d since it ends before inserted start: start %d end %s: \"%s\"\n",
957                            $i, $position_pointer[$i],
958                            $proglist[$i]->[($position_pointer[$i])]->{'start_epoch'},
959                            $proglist[$i]->[($position_pointer[$i])]->{'stop_epoch'},
960                            ${XMLTV::best_name($langs,$proglist[$i]->[($position_pointer[$i])]->{'title'})}[0] if $recdebug;
961                        delete $proglist[$i]->[($position_pointer[$i])];
962                        $position_pointer[$i]++;
963                    }
964                }
965
966                # 7b. adjust starttimes of any programmes to match endtime (with fuzz of +5 mins max)
967                for (my $i=0; $i < $num_grabbers; $i++) {
968                    next if ($position_pointer[$i] == -1);
969                    my $position_offset = 0;
970                    while ((defined $proglist[$i]->[($position_pointer[$i]+$position_offset)]) &&
971                           ($proglist[$i]->[($position_pointer[$i]+$position_offset)]->{'channel'} eq $channels->{$ch}) &&
972                           ($proglist[$i]->[($position_pointer[$i]+$position_offset)]->{'start_epoch'} < $stoppoint)) {
973                        my $this_prog = $proglist[$i]->[($position_pointer[$i]+$position_offset)];
974                        if (($this_prog->{'start_epoch'} + $reclogic{readjust_starttime_for_nonmatched_programmes}) >= $stoppoint) {
975                            printf "REC#7b: adjusting starttime on grabber %d slot %d since it starts <5mins before inserted end:\n",
976                                $i,($position_pointer[$i]+$position_offset) if $recdebug;
977                            printf "REC#7b:    orig: start %d end %d, now: start %d end %d: \"%s\"\n",
978                                $this_prog->{'start_epoch'}, $this_prog->{'stop_epoch'}, $stoppoint, $this_prog->{'stop_epoch'},
979                                ${XMLTV::best_name($langs,$this_prog->{'title'})}[0] if $recdebug;
980                            $proglist[$i]->[($position_pointer[$i]+$position_offset)]->{'start_epoch'} = $stoppoint;
981                            $proglist[$i]->[($position_pointer[$i]+$position_offset)]->{'start'} = sprintf "%s",(strftime "%Y%m%d%H%M", localtime($stoppoint));
982                            $position_offset++;
983                        } else {
984                            printf "REC#7b: removing grabber %d slot %d programme because it started too long before chosen programme: start %d end %d: \"%s\"\n",
985                                $i, ($position_pointer[$i]+$position_offset), $this_prog->{'start_epoch'}, $this_prog->{'stop_epoch'},
986                                ${XMLTV::best_name($langs,$this_prog->{'title'})}[0] if $recdebug;
987                            $position_pointer[$i]++;
988                        }
989                    }
990                }
991            }
992
993            # 8. check that we still have at least one pointer on current channel
994            for (my $i=0; $i < $num_grabbers; $i++) {
995                next if ($position_pointer[$i] == -1);
996                printf "REC#8: grabber %d is now at slot %d\n",$i,$position_pointer[$i] if $recdebug;
997                if ((defined $proglist[$i]->[($position_pointer[$i])]) &&
998                    ($proglist[$i]->[($position_pointer[$i])]->{'channel'} eq $channels->{$ch})) {
999                    $all_done_on_this_channel = 0;
1000                    printf "REC#8:   grabber %d is at slot %d is still on channel \"%s\"\n",$i,$position_pointer[$i],$channels->{$ch} if $recdebug;
1001                }
1002            }
1003            printf "REC#9:\n" if $recdebug;
1004        } until ($all_done_on_this_channel);
1005    }
1006    $writer->end();
1007}
1008
1009# sorting helper routine - sort by channel then by start-time
1010sub order_channel_time {
1011        my $chanresult = $a->{channel} cmp $b->{channel};
1012        return $chanresult if ($chanresult != 0);
1013        return ($a->{start_epoch} <=> $b->{start_epoch});
1014}
1015
1016# descend a structure and clean up various things, including stripping
1017# leading/trailing spaces in strings, translations of html stuff etc
1018#   -- taken & modified from Michael 'Immir' Smith's excellent tv_grab_au
1019my %amp;
1020BEGIN { %amp = ( nbsp => ' ', qw{ amp & lt < gt > apos ' quot " } ) }
1021
1022sub cleanup {
1023    my $x = shift;
1024    if    (ref $x eq "REF")   { cleanup($_) }
1025    elsif (ref $x eq "HASH")  { cleanup(\$_) for values %$x }
1026    elsif (ref $x eq "ARRAY") { cleanup(\$_) for @$x }
1027    elsif (defined $$x) {
1028        $$x =~ s/&(#(\d+)|(.*?));/ $2 ? chr($2) : $amp{$3}||' ' /eg; # scrub html
1029        # $$x =~ s/[^\x20-\x7f]/ /g;    # disabled (we want to keep non-std chars)
1030        $$x =~ s/(^\s+|\s+$)//g;        # strip leading/trailing spaces
1031    }
1032}
1033
1034# -----------------------------------------
1035# Subs: Postprocessing
1036# -----------------------------------------
1037
1038sub postprocess_data
1039{
1040    # for our first postprocessor, we feed it ALL of the XMLTV files we have
1041    # as each postprocessor runs, we feed in the output from the previous one
1042    # Shepherd checks the "completeness" of the data that comes out of a postprocessor & automatically
1043    # reverts back to the previous postprocessor if it was shown to be bad
1044
1045    # first time around: feed in reconciled data ($input_postprocess_file)
1046
1047    my $need_more_data;
1048
1049    printf "\nPostprocessing stage:\n";
1050
1051    foreach my $postprocessor (sort { $components->{$a} <=> $components->{$b} } query_postprocessors()) {
1052        next if ($components->{$postprocessor}->{disabled});
1053
1054        $components->{$postprocessor}->{lastdata} = time;
1055        $components->{$postprocessor}->{laststatus} = "unknown";
1056
1057        printf "\nSHEPHERD: Using postprocessor: %s\n",$postprocessor;
1058
1059        my $output = "$CWD/postprocessors/$postprocessor/output.xmltv";
1060        my $comm = "$CWD/postprocessors/$postprocessor/$postprocessor " .
1061                   "--region $region " .
1062                   "--channels_file $channels_file " .
1063                   "--output $output";
1064        $comm .= " --days $days" if ($days);
1065        $comm .= " --offset $opt->{offset}" if ($opt->{offset});
1066        $comm .= " --debug" if ($debug);
1067        $comm .= " @ARGV" if (@ARGV);
1068        $comm .= " $input_postprocess_file";
1069        print "SHEPHERD: Excuting command: $comm\n";
1070
1071        chdir "$CWD/postprocessors/$postprocessor/";
1072        system($comm);
1073        chdir $CWD;
1074
1075        #
1076        # soak up the data we just collected and check it
1077        # YES - these are the SAME routines we used in the previous 'grabber' phase
1078        # but the difference here is that we clear out our 'channel_data' beforehand
1079        # so we can independently analyze the impact of this postprocessor.
1080        # if it clearly returns bad data, don't use that data (go back one step) and
1081        # flag the postprocessor as having failed.  after 3 consecutive failures, disable it
1082        #
1083
1084        # clear out channel_data
1085        foreach my $ch (keys %{$channels}) {
1086            delete $channel_data->{$ch};
1087        }
1088
1089        # process and analyze it!
1090        &soak_up_data($postprocessor, $output, "postprocessor");
1091        $need_more_data = &analyze_plugin_data($postprocessor_ok_threshold_percent, "POSTPROCESSOR");
1092
1093        $components->{$postprocessor}->{laststatus} = $plugin_data->{$postprocessor}->{laststatus};
1094
1095        if (($need_more_data) && (!$insufficient_grabber_data)) {
1096            # urgh.  this postprocessor did a bad bad thing ...
1097            printf "SHEPHERD: XML data from postprocessor %s rejected, using XML from previous stage\n",$postprocessor;
1098
1099            if (defined $components->{$postprocessor}->{conescutive_failures}) {
1100                $components->{$postprocessor}->{conescutive_failures}++;
1101            } else {
1102                $components->{$postprocessor}->{conescutive_failures} = 1;
1103            }
1104            printf "SHEPHERD: Postprocessor \"%s\" has now failed %d times in a row.  %d more and it will be automatically disabled.\n",
1105                $postprocessor,
1106                $components->{$postprocessor}->{conescutive_failures},
1107                ($postprocessor_disable_failure_threshold - $components->{$postprocessor}->{conescutive_failures});
1108
1109            if ($components->{$postprocessor}->{conescutive_failures} >= $postprocessor_disable_failure_threshold) {
1110                printf "SHEPHERD: Disabling Postprocessor \"%s\".\n",$postprocessor;
1111                $components->{$postprocessor}->{disabled} = 1;
1112            }
1113        } else {
1114            # accept what this postprocessor did to our output ...
1115            printf "SHEPHERD: accepting output from postprocessor %s, feeding it into next stage\n",$postprocessor;
1116            $input_postprocess_file = $output;
1117            delete $components->{$postprocessor}->{conescutive_failures} if (defined $components->{$postprocessor}->{conescutive_failures});
1118        }
1119    }
1120}
1121
1122
1123sub output_data
1124{
1125    # $input_postprocess_file contains our final output
1126    # send it to whereever --output told us to!
1127
1128    my $output_filename = "$CWD/output.xmltv";
1129    $output_filename = $opt->{output} if ($opt->{output});
1130
1131    open(OUTFILE,">$output_filename") || die "could not open output file $output_filename for writing: $!\n";
1132
1133    if (!(open(INFILE,"<$input_postprocess_file"))) {
1134        printf "WARNING: could not open input file \"%s\": %s\n", $input_postprocess_file, $!;
1135        printf "Output XMLTV data may be damanged as a result!\n";
1136    } else {
1137        while (<INFILE>) {
1138            print OUTFILE $_;
1139        }
1140        close(INFILE);
1141        close(OUTFILE);
1142    }
1143
1144    printf "Final output stored in $output_filename.\n";
1145}
1146
1147# -----------------------------------------
1148# Subs: Updates & Installations
1149# -----------------------------------------
1150
1151sub update
1152{
1153    printf "\nChecking for updates:\n\n";
1154
1155    my $data;
1156    my $sites = "";
1157    $sites = "$mirror_site," if ($mirror_site);
1158    $sites .= $HOME;
1159
1160    foreach my $site (split(/,/,$sites)) {
1161        my $url = $site . "/status";
1162        print "Fetching status file: $url.\n";
1163        $data = LWP::Simple::get($url);
1164        last if $data;
1165
1166        print "Failed to retrieve status file from $url.\n";
1167    }
1168    return if (!$data);
1169
1170    my %clist = %$components;
1171    while ($data =~ /(.*):(.*):(.*)/g)
1172    {
1173        my ($proggy, $latestversion, $progtype) = ($1,$2,$3);
1174        update_component($proggy, $latestversion, $progtype);
1175        delete $clist{$proggy};
1176    }
1177
1178    # work out what components disappeared (if any)
1179    foreach (keys %clist) {
1180        unless ($components->{$_}->{disabled}) {
1181            print "\nDeleted component: $_.\n";
1182            disable($_);
1183            $made_changes = 1;
1184        }
1185    }
1186
1187}
1188
1189sub update_component
1190{
1191    my ($proggy, $latestversion, $progtype) = @_;
1192
1193    # handle new installs..
1194    if ($progtype eq "shepherd") {
1195        if(! -e "$CWD/$progname") {
1196            print "Missing: $CWD/$progname\n";
1197            install($progname, $latestversion, $progtype);
1198            return;
1199        }
1200    } else {
1201        if (!defined $components->{$proggy} or ! -e ($progtype . "s/$proggy/$proggy")) {
1202            print "New $progtype: $proggy.\n";
1203            install($proggy, $latestversion, $progtype);
1204            return;
1205        }
1206        if ($components->{$proggy}->{disabled}) {
1207            print "Warning: grabber $proggy disabled by config file.\n";
1208        }
1209    }
1210
1211    # upgrade/downgrades
1212    my $ver;
1213    if ($progtype eq "shepherd") {
1214        $ver = $version;
1215    }
1216    else {
1217        $ver = $components->{$proggy}->{ver};
1218    } 
1219
1220    my $result = versioncmp($ver, $latestversion);
1221    if ($result == -1) {
1222        print "Upgrading $proggy from v$ver to v$latestversion.\n";
1223    } elsif ($result == 1) {
1224        print "Downgrading $proggy from v$ver to v$latestversion.\n";
1225    } else {
1226        print "Already have latest version of $proggy: v$ver.\n";
1227        return;
1228    }
1229    install($proggy, $latestversion, $progtype);
1230}
1231
1232sub install
1233{
1234    my ($proggy, $latestversion, $progtype) = @_;
1235
1236    print "Downloading $proggy v$latestversion.\n";
1237
1238    my $sites = "";
1239    $sites = "$mirror_site," if ($mirror_site);
1240    $sites .= $HOME;
1241
1242    my $rdir = "";
1243    my $ldir = $CWD;
1244    my $ver = "unknown";
1245
1246    if ($progtype eq "shepherd") {
1247        $ver = $version;
1248    } else {
1249        $rdir = $progtype . "s";
1250        $ldir = "$CWD/$progtype" . "s/$proggy";
1251        $ver = $components->{$proggy}->{ver} if ((defined $components->{$proggy}) && $components->{$proggy}->{ver});
1252        -d ("$CWD/$progtype" . "s") or mkdir ("$CWD/$progtype" . "s") or die "Cannot create directory $CWD/$progtype" . "s: $!";
1253    }
1254    -d $ldir or mkdir $ldir or die "Cannot create directory $ldir: $!";
1255
1256    my $newfile = "$ldir/$proggy-$latestversion";
1257    my $rc;
1258
1259    foreach my $site (split(/,/,$sites)) {
1260        printf "Fetching $site/$rdir/$proggy v$latestversion.\n";
1261        $rc = LWP::Simple::getstore("$site/$rdir/$proggy", $newfile);
1262        last if (is_success($rc));
1263
1264        print "Failed to retrieve $site/$rdir/$proggy.\n";
1265    }
1266    return if (!is_success($rc));
1267
1268    # Make it executable
1269    system('chmod u+x ' . $newfile);
1270
1271    -d $ARCHIVE_DIR or mkdir $ARCHIVE_DIR or die "Cannot create directory $ARCHIVE_DIR: $!";
1272
1273    if (-e "$ldir/$proggy")
1274    {
1275        rename("$ldir/$proggy", "$ARCHIVE_DIR/$proggy-$ver");
1276    }
1277    rename($newfile, "$ldir/$proggy");
1278   
1279    print "Installed $proggy v$latestversion.\n" if ($debug);
1280
1281    # if the update was for shepherd itself, restart it
1282    if ($progtype eq "shepherd") {
1283        print "\n*** Restarting ***\n\n";
1284        exec("$ldir/$proggy");
1285        # This exits.
1286    }
1287
1288    print "Testing $proggy...\n" if ($debug);
1289    my $result = test_proggy($ldir,"$ldir/$proggy");
1290
1291    $components->{$proggy}->{ver} = $latestversion;
1292    $components->{$proggy}->{ready} = $result;
1293    $components->{$proggy}->{laststatus} = sprintf "updated to %s on %s", $latestversion, (strftime "%a%d%b%y",localtime(time));
1294
1295    $made_changes = 1;
1296}
1297
1298sub test_proggy
1299{
1300    my ($testdir,$proggyexec) = @_;
1301
1302    chdir($testdir);
1303    system("$proggyexec --ready");
1304    chdir ($CWD);
1305
1306    my $result = $?;
1307    print "Return value: $result\n" if ($debug);
1308
1309    print "\nprogramme $proggyexec did not exit cleanly!\n" .
1310         "It may require configuration.\n\n" if ($result);
1311    return !$result;
1312}
1313
1314sub enable
1315{
1316    my $proggy = shift;
1317
1318    # confirm it exists first
1319    if (!$components->{$proggy}) {
1320        printf "No such component: \"%s\".\n",$proggy;
1321        return;
1322    }
1323    print "Enabling $proggy.\n";
1324
1325    delete $components->{$proggy}->{disabled};
1326    $components->{$proggy}->{laststatus} = sprintf "enabled on %s, not run yet",(strftime "%a%d%b%y", localtime(time));
1327    $made_changes = 1;
1328}
1329
1330sub disable
1331{
1332    my $proggy = shift;
1333
1334    # confirm it exists first
1335    if (!$components->{$proggy}) {
1336        printf "No such component: \"%s\".\n",$proggy;
1337        return;
1338    }
1339    print "Disabling $proggy.\n";
1340
1341    $components->{$proggy}->{disabled} = 1;
1342    $components->{$proggy}->{laststatus} = sprintf "manually disabled on %s",(strftime "%a%d%b%y", localtime(time));
1343    $made_changes = 1;
1344}
1345
1346sub set_order
1347{
1348    my ($quiet,$order) = @_;
1349    $pref_order = $order if ($order);
1350
1351    # reset current order to zero
1352    foreach my $proggy (query_grabbers()) {
1353        $components->{$proggy}->{order} = 0;
1354    }
1355
1356    # and now set order
1357    my $order_num = 1;
1358    if ($pref_order) {
1359        foreach my $proggy (split(/,/,$pref_order)) {
1360            if (defined $components->{$proggy} and $components->{$proggy}->{type} eq 'grabber') {
1361                $components->{$proggy}->{order} = $order_num;
1362                $order_num++;
1363            }
1364        }
1365    }
1366
1367    # set order of any grabbers not specified in a random manner
1368    foreach my $proggy (sort query_grabbers()) {
1369        if ((!defined $components->{$proggy}->{order}) || ($components->{$proggy}->{order} == 0)) {
1370            $components->{$proggy}->{order} = $order_num+int(rand(1000));
1371        }
1372    }
1373
1374    # .. and finally normalize the order (& show the user the order we chose)
1375    print "Grabber order set as follows:\n" unless $quiet;
1376    $order_num = 0;
1377    foreach my $proggy (sort { $components->{$a}->{order} <=> $components->{$b}->{order} } query_grabbers()) {
1378        $order_num++;
1379        $components->{$proggy}->{order} = $order_num;
1380        printf " #%d. %s%s\n",$components->{$proggy}->{order},$proggy,($components->{$proggy}->{disabled} ? " [disabled]" : "") unless $quiet;
1381    }
1382
1383    $made_changes = 1;
1384}
1385
1386sub check
1387{
1388    my $result;
1389    foreach my $proggy (keys %$components) {
1390        my $progtype = $components->{$proggy}->{type};
1391        $result = test_proggy("$CWD/$progtype" . "s/$proggy", "$CWD/$progtype" . "s/$proggy/$proggy");
1392        printf "%s %s: %s\n",ucfirst($progtype), $proggy,($result ? "OK" : "Failed");
1393        if (!$result ne !$components->{$proggy}->{ready}) {
1394            $components->{$proggy}->{ready} = $result;
1395            $made_changes = 1;
1396        }
1397    }
1398}
1399# -----------------------------------------
1400# Subs: Utilities
1401# -----------------------------------------
1402#
1403
1404sub query_grabbers
1405{
1406    return query_component_type('grabber');
1407}
1408
1409sub query_postprocessors
1410{
1411    return query_component_type('postprocessor');
1412}
1413
1414sub query_component_type
1415{
1416    my $progtype = shift;
1417
1418    my @ret = ();
1419    foreach (keys %$components)
1420    {
1421        push (@ret, $_) if ($components->{$_}->{type} eq $progtype);
1422    }
1423    return @ret;
1424}
1425
1426# -----------------------------------------
1427# Subs: Setup
1428# -----------------------------------------
1429
1430sub read_config_file
1431{
1432    read_file($config_file, 'configuration');
1433
1434    # TEMPORARY! Convert old $grabbers/$postprocessors config file to
1435    # new $components format.
1436    if (defined $grabbers or defined $postprocessors)
1437    {
1438        foreach (keys %$grabbers) {
1439            $grabbers->{$_}->{type} = 'grabber';
1440        }
1441        foreach (keys %$postprocessors) {
1442            $postprocessors->{$_}->{type} = 'postprocessor';
1443        }
1444        $components = { %$grabbers, %$postprocessors };
1445        $grabbers = undef;
1446        $postprocessors = undef;
1447    }
1448
1449
1450    # if we are updating from a previous rev of shepherd.conf we may not
1451    # have any 'order' fields set .. check here
1452    my $found_order = 1;
1453    foreach (query_grabbers())
1454    {
1455        $found_order = 0 if (!defined $components->{$_}->{order});
1456    }
1457    if (($found_order == 0) && (!$opt->{setorder}))
1458    {
1459        # at least one 'order' was missing .. we need to put it in!
1460        printf "Legacy shepherd.conf file didn't contain any grabber order! Automatically updating using a random order, use --setorder to manually set this if you care.\n";
1461        &set_order(1);
1462    }
1463
1464    # if a mirror has been specified, add it into our config
1465    if ($opt->{mirror}) {
1466        $mirror_site = $opt->{mirror};
1467        $made_changes = 1;
1468        print "Adding mirror: $mirror_site\n";
1469    }
1470}
1471
1472sub read_channels_file
1473{
1474    read_file($channels_file, 'channels');
1475}
1476
1477sub read_file
1478{
1479    my $fn = shift;
1480    my $name = shift;
1481
1482    print "Reading $name file: $fn\n";
1483    unless (-r $fn)
1484    {
1485        unless ($opt->{configure})
1486        {
1487            print "\nNo $name file found.\n" .
1488                  ucfirst($progname) . " must be configured: " .
1489                  "configuring now.\n\n";
1490            $opt->{'configure'} = 1;
1491        }
1492        return;
1493    }
1494    local (@ARGV, $/) = ($fn);
1495    no warnings 'all';
1496    eval <>;
1497    if ($@ and !$opt->{configure})
1498    {
1499        die "\nError in $name file!\nDetails:\n$@";
1500    }
1501}
1502
1503sub write_config_file
1504{
1505    open(CONF, ">$config_file") or die "cannot write to $config_file: $!";
1506    print CONF Data::Dumper->Dump(
1507        [$region,  $pref_order,  $mirror_site,  $components, $title_translation_table  ],
1508        ["region", "pref_order", "mirror_site", "components", "title_translation_table" ]);
1509    close CONF;
1510    print "\nUpdated configuration file $config_file.\n" if ($debug);
1511}
1512
1513sub write_channels_file
1514{
1515    open(CHAN, ">$channels_file") or die "cannot write to $channels_file: $!";
1516    print CHAN Data::Dumper->Dump([$channels], ["channels"]);
1517    close CHAN;
1518    print "Updated channels file $channels_file.\n" if ($debug);
1519}
1520
1521sub get_initial_command_line_options
1522{
1523  GetOptions( 'config-file=s'   => \$opt->{configfile},
1524              'help'            => \$opt->{help},
1525              'configure'       => \$opt->{configure},
1526              'mirror=s'        => \$opt->{mirror},
1527              'dontcallgrabbers' => \$opt->{dontcallgrabbers},
1528              'debug'           => \$debug);
1529}
1530
1531sub get_remaining_command_line_options
1532{
1533    GetOptions(
1534              'version'         => \$opt->{status},
1535              'status'          => \$opt->{status},
1536              'list'            => \$opt->{list},
1537              'show-config'     => \$opt->{show_config},
1538
1539              'update'          => \$opt->{update},
1540              'noupdate'        => \$opt->{noupdate},
1541
1542              'disable=s'       => \$opt->{disable},
1543              'enable=s'        => \$opt->{enable},
1544              'setorder=s'      => \$opt->{setorder},
1545
1546              'days=i'          => \$days,
1547              'offset=i'        => \$opt->{offset},
1548              'show-channels'   => \$opt->{show_channels},
1549              'output=s'        => \$opt->{output},
1550              'check'           => \$opt->{check}
1551            );
1552}
1553
1554
1555# -----------------------------------------
1556# Subs: Configuration
1557# -----------------------------------------
1558
1559sub configure
1560{
1561    my $REGIONS = {
1562        "ACT" => 126,
1563        "NSW: Sydney" => 73,
1564        "NSW: Newcastle" => 184,
1565        "NSW: Central Coast" => 66,
1566        "NSW: Griffith" => 67,
1567        "NSW: Broken Hill" => 63,
1568        "NSW: Northern NSW" => 69,
1569        "NSW: Southern NSW" => 71,
1570        "NSW: Remote and Central" => 106,
1571        "NT: Darwin" => 74,
1572        "NT: Remote & Central" => 108,
1573        "QLD: Brisbane" => 75,
1574        "QLD: Gold Coast" => 78,
1575        "QLD: Regional" => 79,
1576        "QLD: Remote & Central" => 114,
1577        "SA: Adelaide" => 81,
1578        "SA: Renmark" => 82,
1579        "SA: Riverland" => 83,
1580        "SA: South East SA" => 85,
1581        "SA: Spencer Gulf" => 86,
1582        "SA: Remote & Central" => 107,
1583        "Tasmania" => 88,
1584        "VIC: Melbourne" => 94,
1585        "VIC: Geelong" => 93,
1586        "VIC: Eastern Victoria" => 90,
1587        "VIC: Mildura/Sunraysia" => 95,
1588        "VIC: Western Victoria" => 98,
1589        "WA: Perth" => 101,
1590        "WA: Regional" => 102
1591    };
1592
1593    print "\nConfiguring.\n\n" .
1594          "Select your region:\n";
1595    foreach (sort keys %$REGIONS)
1596    {
1597        printf(" (%3d) %s\n", $REGIONS->{$_}, $_);
1598    }
1599    $region = ask_choice("Enter region code:", "94", values %$REGIONS);
1600
1601    print "\nFetching channel information... ";
1602
1603    my @channellist = get_channels();
1604
1605    print "done.\n\n" .
1606          "For each channel you want guide data for, enter an XMLTV id\n" .
1607          "of your choice (e.g. \"seven.free.au\"). If you don't need\n" .
1608          "guide data for this channel, just press Enter.\n\n" .
1609          "Please don't subscribe to unneeded channels.\n\nChannels:\n";
1610    $channels = {};
1611    my $line;
1612    foreach (@channellist)
1613    {
1614        $line = ask(" \"$_\"? ");
1615        $channels->{$_} = $line if ($line);
1616    }
1617
1618
1619    print "\nRandomly selecting grabber order.\n\n";
1620    set_order(0);
1621
1622    show_channels();
1623    unless(ask_boolean("\nCreate configuration file?"))
1624    {
1625        print "Aborting configuration.\n";
1626        exit 0;
1627    }
1628
1629    write_config_file();
1630    write_channels_file();
1631
1632    print "Finished configuring.\n\n" .
1633          "Shepherd is installed into $CWD.\n\n";
1634   
1635    if ($invoked ne "$CWD/$progname" and $invoked =~ /$progname/)
1636    {
1637        print "Warning: you invoked this program as $invoked.\n" .
1638            "In the future, it should be run as $CWD/$progname,\n" .
1639            "to avoid constantly re-downloading the latest version.\n\n" .
1640            "MythTV users may wish to create the following symlink, by " .
1641            "doing this (as root):\n" .
1642            "\"ln -s $CWD/$progname /usr/bin/tv_grab_au\".\n\n" .
1643            "You may safely delete $invoked.\n\n";
1644    }
1645
1646    status();
1647
1648    unless (ask_boolean("\nGrab data now?"))
1649    {
1650        exit 0;
1651    }
1652}
1653
1654sub get_channels
1655{
1656    my @date = localtime;
1657    my $page = LWP::Simple::get(
1658        "http://au.tv.yahoo.com/results.html?rg=$region&dt=" .
1659        ($date[5] + 1900) . "-$date[4]-$date[3]");
1660    my @channellist;
1661    while ($page =~ /<tr class=rtb><td class=rth><a .*?>(.*?)<\/a>/g)
1662    {
1663        push @channellist, $1;
1664    }
1665    return @channellist;
1666}
1667
1668# -----------------------------------------
1669# Subs: Status & Help
1670# -----------------------------------------
1671
1672sub show_config
1673{
1674    print "\nConfiguration\n".
1675          "-------------\n" .
1676          "Config file: $config_file\n" .
1677          "Debug mode : " . is_set($debug) . "\n" .
1678          "Output file: " . ($opt->{output} ? $opt->{output} : "None") . "\n" .
1679          "Region ID  : $region\n";
1680  show_channels();
1681  print "\n";
1682  status();
1683  print "\n";
1684}
1685
1686sub show_channels
1687{
1688  print "Subscribed channels:\n";
1689  print "    $_ -> $channels->{$_}\n" for sort keys %$channels;
1690}
1691
1692sub is_set
1693{
1694    my $arg = shift;
1695    return $arg ? "Yes" : "No";
1696}
1697
1698sub status
1699{
1700    print " Grabber           Version Enabled Ready Last Run   Status\n" .
1701          " ----------------- ------- ------- ----- ---------- ---------------------------\n";
1702    foreach (sort { $components->{$a}->{order} <=> $components->{$b}->{order} } query_grabbers()) {
1703        my $h = $components->{$_};
1704        printf  " %-16s %8s %4s %6s  %11s %s\n",
1705                "$h->{order}. $_",
1706                ($h->{ver} ? $h->{ver} : "unknown"),
1707                $h->{disabled} ? '' : 'Y',
1708                $h->{ready} ? 'Y' : '',
1709                $h->{lastdata} ? (strftime "%a%d%b%y", localtime($h->{lastdata})) : 'never',
1710                $h->{laststatus} ? $h->{laststatus} : '';
1711    }
1712    printf "Grabbers shown in order of preference.\n\n";
1713
1714    print " Postprocessor     Version Enabled Ready Last Run   Status\n" .
1715          " ----------------- ------- ------- ----- ---------- ---------------------------\n";
1716    foreach (sort { $components->{$a} <=> $components->{$b} } query_postprocessors()) {
1717        my $h = $components->{$_};
1718        printf  " %-16s %8s %4s %6s  %11s %s\n",
1719                $_,
1720                ($h->{ver} ? $h->{ver} : "unknown"),
1721                $h->{disabled} ? '' : 'Y',
1722                $h->{ready} ? 'Y' : '',
1723                $h->{lastdata} ? (strftime "%a%d%b%y", localtime($h->{lastdata})) : 'never',
1724                $h->{laststatus} ? $h->{laststatus} : '';
1725    }
1726    printf "Postprocessors shown in order of execution.\n\n";
1727}
1728
1729sub help
1730{
1731    print q{
1732Command-line options:
1733    --help                Print this message
1734
1735    --status              Print a list of grabbers maintained
1736    --list                Print a detailed list of grabbers
1737    --mirror <s>          Set URL <s> as primary location to check for updates
1738
1739    --configure           Setup
1740    --show-config         Print setup details
1741
1742    --setorder <s>        Set order of grabbers to <s> (comma-seperated list of grabbers)
1743
1744    --disable <s>         Don't ever use grabber/postprocessor <s>
1745    --enable <s>          Okay, maybe use it again then
1746    --uninstall <s>       Remove a disabled grabber/postprocessor
1747
1748    --noupdate            Do not attempt to update before running
1749    --update              Update only; do not grab data
1750
1751    --check               Check status of all grabbers and postprocessors
1752};
1753    exit 0;
1754}
1755
1756# -----------------------------------------
1757# Subs: override handlers for standard perl.
1758# -----------------------------------------
1759
1760# ugly hack. please don't try this at home kids!
1761sub my_die {
1762    my ($arg,@rest) = @_;
1763    my ($pack,$file,$line,$sub) = caller(1);
1764
1765    # check if we are in an eval()
1766    if ($^S) {
1767        printf STDERR "  shepherd caught a die() within eval{} from file $file line $line\n";
1768    } else {
1769        if (!ref($arg)) {
1770            printf STDERR "DIE at line %d in file %s\n",$line,$file;
1771            CORE::die(join("",@rest));
1772        } else {
1773            CORE::die($arg,@rest);
1774        }
1775    }
1776}
Note: See TracBrowser for help on using the browser.