root/shepherd @ 3

Revision 3, 19.0 kB (checked in by max, 7 years ago)

Lincoln's first pass at XML analysis.

Line 
1#!/usr/bin/perl -w
2
3# "Shepherd"
4
5my $version = '0.2.5ng';
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.5ng : multi-grabber (potentially with partial data) & postprocessor support
22#
23# ToDo:
24# * --mirror <url> option, to let you get software from somewhere other
25#   than whuffy. Store this in shepherd.conf. Whuffy is merely the first
26#   mirror.
27# * Make it check compilation after installing by calling --version or
28#   --desc or --ready
29# * --ready  option that says whether config is required?
30# * "Last Data" column in status
31
32use strict;
33use LWP::Simple;
34use Sort::Versions;
35use Cwd;
36use Getopt::Long;
37use Data::Dumper;
38use XMLTV;
39use XMLTV::Ask;
40
41# ---------------------------------------------------------------------------
42# --- Global Variables
43# ---------------------------------------------------------------------------
44
45my $progname = 'shepherd';
46
47my $HOME = 'http://www.whuffy.com';
48my $STATUS = "$HOME/status";
49
50my $invoked = Cwd::realpath($0);
51
52# By default, Shepherd runs from ~/.shepherd/. If it's not run as a user,
53# it will try /opt/shepherd/ instead.
54my $CWD = ($ENV{HOME} ? $ENV{HOME} . "/." : "/opt/") . $progname;
55-d $CWD or mkdir $CWD or die "Cannot create directory $CWD: $!";
56chdir($CWD);
57
58my $GRABBER_DIR = "$CWD/grabbers";
59my $ARCHIVE_DIR = "$CWD/archive";
60
61my $opt;
62my $debug = 1;
63my $grabbers = { };
64my $preferred; # obsolete but may still exist in shepherd.conf
65my $region;
66my $channels;
67my $config_file =   "$CWD/$progname.conf";
68my $channels_file = "$CWD/channels.conf";
69my $days;
70my $grabber_data = { };
71my $master_starttime = time;
72
73# ---------------------------------------------------------------------------
74# --- Setup
75# ---------------------------------------------------------------------------
76
77print ucfirst($progname) . " v$version\n\n";
78#print "Cwd: $CWD.\n";
79
80# Any options Shepherd doesn't understand, we'll pass to the grabber(s)
81Getopt::Long::Configure(qw/pass_through/);
82
83get_initial_command_line_options();
84
85help() if ($opt->{help});
86
87unless ($opt->{configure})
88{
89    read_config_file();
90    read_channels_file();
91}
92
93get_remaining_command_line_options();
94
95if ($opt->{status})
96{
97    status();
98    exit;
99}
100
101if ($opt->{show_config})
102{
103    show_config();
104    exit;
105}
106
107if ($opt->{enable})
108{
109    enable($opt->{enable});
110}
111
112if ($opt->{disable})
113{
114    disable($opt->{disable});
115}
116
117&set_order($opt->{setorder}) if ($opt->{setorder});
118
119if ($opt->{check})
120{
121    check();
122}
123
124if ($opt->{enable} or $opt->{disable} or $opt->{setorder} or $opt->{check})
125{
126    write_config_file();
127    status();
128    exit;
129}
130
131# ---------------------------------------------------------------------------
132# --- Update
133# ---------------------------------------------------------------------------
134
135unless ($opt->{noupdate})
136{
137    update($progname, $version);
138    write_config_file() unless ($opt->{configure});
139}
140
141if ($opt->{configure})
142{
143    configure();
144}
145
146# ---------------------------------------------------------------------------
147# --- Go!
148# ---------------------------------------------------------------------------
149
150unless ($opt->{update})
151{
152    grab_data();
153}
154
155print "Done.\n";
156
157status();
158
159# ---------------------------------------------------------------------------
160# --- Subroutines
161# ---------------------------------------------------------------------------
162
163# -----------------------------------------
164# Subs: Grabbing
165# -----------------------------------------
166
167sub grab_data
168{
169    my $used_grabbers = 0;
170    my $need_more_data = 1;
171
172    # iterate across grabbers until we have all our data we want (or need)
173    foreach my $grabber (sort { $grabbers->{$a}->{order} <=> $grabbers->{$b}->{order} } keys %$grabbers) {
174        next if ($grabbers->{$grabber}->{disabled});
175        $used_grabbers++;
176
177        print "\nUsing grabber: $grabber.\n\n";
178
179        my $output = $opt->{output};
180        $output ||= "$CWD/output.xmltv";
181        my $comm = "$GRABBER_DIR/$grabber/$grabber " .
182                   "--region $region " .
183                   "--channels_file $channels_file " .
184                   "--output $output";
185
186        # NOTE: ideally a grabber could be instructed to fetch partial data through --channel, --starttime & --endtime
187        # we don't have that for now so instead whenever there is missing data, ALL 7 days for all channels will be collected
188        # FIXME FUTURE: call grabbers just with what we want...
189        $comm .= " --days $days" if ($days);
190        $comm .= " --offset $opt->{offset}" if ($opt->{offset});
191        $comm .= " --debug" if ($debug);
192        $comm .= " @ARGV" if (@ARGV);
193        print "Excuting command:\n$comm\n\n";
194
195        chdir "$GRABBER_DIR/$grabber/";
196        system($comm);
197        chdir $CWD;
198
199        # soak up the data we just collected
200        &soak_up_grabber_data($grabber, $output);
201
202        # check to see if we have all the data we want
203        $need_more_data = &analyze_grabber_data();
204
205        last if (!$need_more_data);
206    }
207
208
209    if ($used_grabbers == 0)
210    {
211        print "No valid grabbers installed/enabled!\n";
212        return;
213    }
214
215    if ($need_more_data)
216    {
217        print "Ran through all grabbers but still missing data!!! :(\n";
218        return;
219    }
220}
221
222
223# interpret xmltv data from this grabber
224sub soak_up_grabber_data
225{
226    my ($grabber, $output) = @_;
227    eval { $grabber_data->{$grabber} = XMLTV::parsefiles($output); };
228}
229
230
231# analyze grabber data - do we have all the data we want?
232# returns 1 if we need more data, 0 if we have all we want
233sub analyze_grabber_data
234{
235    # normalize starttime to beginning of hour
236    my ($sec,$min,@rest) = localtime($master_starttime);
237    my $starttime = $master_starttime - ((60 * $min) + $sec);
238    my $endtime;
239
240    if ($days) {
241        $endtime = $starttime + ($days * 86400);
242    } else {
243        $endtime = $starttime + (7*86400);
244    }
245    $starttime += (86400 * $opt->{offset};
246
247    # XXX TODO!  for now just return 0 - we got all our data!
248    return 0;
249
250    # iterate across channels
251    #foreach my $channel (sort keys %{$channels}) {
252        # iterate across days
253
254    return 0;
255}
256
257# -----------------------------------------
258# Subs: Updates & Installations
259# -----------------------------------------
260
261sub update
262{
263    print "\nFetching status file: $STATUS.\n";
264    my $data = LWP::Simple::get($STATUS);
265    unless ($data)
266    {
267        print "Failed to retrieve status file.\n";
268        return;
269    }
270
271    my %glist = %$grabbers;
272    while ($data =~ /(.*):(.*)/g)
273    {
274        update_component($1, $2);
275        delete $glist{$1};
276    }
277    foreach (keys %glist)
278    {
279        unless ($grabbers->{$_}->{disabled})
280        {
281            print "\nDeleted grabber: $_.\n";
282            disable($_);
283        }
284    }
285}
286
287sub update_component
288{
289    my ($proggy, $latestversion) = @_;
290
291    print "\n";
292
293    if ($proggy eq $progname)
294    {
295        if(! -e "$CWD/$progname")
296        {
297            print "Missing: $CWD/$progname\n";
298            install($progname, $latestversion);
299            return;
300        }
301    }
302    else
303    {
304        if (!$grabbers->{$proggy} or ! -e "$GRABBER_DIR/$proggy/$proggy")
305        {
306            print "New grabber: $proggy.\n";
307            install($proggy, $latestversion);
308            return;
309        }
310        if ($grabbers->{$proggy}->{disabled})
311        {
312            print "Warning: $proggy disabled by config file.\n";
313        }
314    }
315
316    # Compare versions
317    my $ver = ($proggy eq $progname ? $version : $grabbers->{$proggy}->{ver});
318
319    my $result = versioncmp($ver, $latestversion);
320    if ($result == -1)
321    {
322        print "Upgrading $proggy from v$ver to v$latestversion.\n";
323    }
324    elsif ($result == 1)
325    {
326        print "Downgrading $proggy from v$ver to v$latestversion.\n";
327    }
328    else
329    {
330        print "Already have latest version of $proggy: v$ver.\n";
331        return;
332    }
333    install($proggy, $latestversion);
334}
335
336sub install
337{
338    my ($proggy, $latestversion) = @_;
339
340    print "Downloading $proggy v$latestversion.\n";
341
342    my $rdir = $HOME;
343    my $ldir = $CWD;
344    my $ver = $version;
345
346    if ($proggy ne $progname)
347    {
348        -d $GRABBER_DIR or mkdir $GRABBER_DIR
349            or die "Cannot create directory $GRABBER_DIR: $!";
350
351        $ldir = "$GRABBER_DIR/$proggy";
352        -d $ldir or mkdir $ldir or die "Cannot create directory $ldir: $!";
353        $rdir = "$rdir/grabbers";
354        $ver = $grabbers->{$proggy}->{ver};
355    }
356
357    my $newfile = "$ldir/$proggy-$latestversion";
358   
359    my $rc = LWP::Simple::getstore("$rdir/$proggy-$latestversion", $newfile);
360
361    unless (is_success($rc))
362    {
363        print "Failed to retrieve $rdir/$proggy-$latestversion.\n";
364        return;
365    }
366
367    # Make it executable
368    system('chmod u+x ' . $newfile);
369
370    -d $ARCHIVE_DIR or mkdir $ARCHIVE_DIR
371      or die "Cannot create directory $ARCHIVE_DIR: $!";
372
373    if (-e "$ldir/$proggy")
374    {
375        rename("$ldir/$proggy", "$ARCHIVE_DIR/$proggy-$ver");
376    }
377    rename($newfile, "$ldir/$proggy");
378   
379    print "Installed $proggy v$latestversion.\n" if ($debug);
380
381    if ($proggy eq $progname)
382    {
383        print "\n*** Restarting ***\n\n";
384        exec("$ldir/$proggy");
385        # This exits.
386    }
387
388    print "Testing $proggy...\n" if ($debug);
389    my $result = test_grabber($proggy);
390
391    # Update grabbers list
392    my $disabled = $grabbers->{$proggy}->{disabled};
393    $grabbers->{$proggy} = 
394        { 
395            'ver' => $latestversion, 
396            'disabled' => $disabled,
397            'ready' => $result
398        };
399}
400
401sub test_grabber
402{
403    my $proggy = shift;
404
405    chdir("$GRABBER_DIR/$proggy");
406    system("$GRABBER_DIR/$proggy/$proggy --ready");
407    chdir ($CWD);
408
409    my $result = $?;
410    print "Return value: $result\n" if ($debug);
411
412    if ($result)
413    {
414        print "\nGrabber $proggy did not exit cleanly!\n" .
415              "It may require configuration.\n\n";
416    }
417
418    return !$result;
419}
420
421sub enable
422{
423    my $proggy = shift;
424
425    print "Enabling $proggy.\n";
426    if (!$grabbers->{$proggy})
427    {
428        print "No such grabber: \"$proggy\".\n";
429    }
430    else
431    {
432        delete $grabbers->{$proggy}->{disabled};
433    }
434}
435
436sub disable
437{
438    my $proggy = shift;
439
440    print "Disabling $proggy.\n";
441    $grabbers->{$proggy}->{disabled} = 1;
442}
443
444sub set_order
445{
446    my $order = shift;
447    my $all_ok = 1;
448
449    # first check that user supplied a valid list of grabbers
450    if ($order) {
451        foreach my $proggy (split(/,/,$order)) {
452            next if !$proggy;
453            if (!$grabbers->{$proggy}) {
454                $all_ok = 0;
455                print "Invalid grabber: \"$proggy\".\n";
456            }
457        }
458    }
459    if ($all_ok) {
460        # if list was ok then first reset current order to zero
461        foreach my $proggy (keys %$grabbers) {
462            $grabbers->{$proggy}->{order} = 0;
463        }
464
465        # and now set order
466        my $order_num = 1;
467        if ($order) {
468            foreach my $proggy (split(/,/,$order)) {
469                $grabbers->{$proggy}->{order} = $order_num;
470                $order_num++;
471            }
472        }
473
474        # set order of any grabbers not specified in a random manner
475        foreach my $proggy (sort keys %$grabbers) {
476            if ($grabbers->{$proggy}->{order} == 0) {
477                $grabbers->{$proggy}->{order} = $order_num+int(rand(100));
478            }
479        }
480
481        # .. and finally normalize the order (& show the user the order we chose)
482        print "Grabber order set as follows:\n";
483        $order_num = 0;
484        $opt->{setorder} = "";
485        foreach my $proggy (sort { $grabbers->{$a}->{order} <=> $grabbers->{$b}->{order} } keys %$grabbers) {
486            $order_num++;
487            $grabbers->{$proggy}->{order} = $order_num;
488            $opt->{setorder} .= "$proggy" . ",";
489            printf " #%d. %s%s\n",$grabbers->{$proggy}->{order},$proggy,($grabbers->{$proggy}->{disabled} ? " [disabled]" : "");
490        }
491    }
492}
493
494sub check
495{
496    my $result;
497    foreach (keys %$grabbers)
498    {
499        $result = test_grabber($_);
500        print "Grabber $_: " . 
501              ($result ? "OK" : "Failed") . "\n\n";
502        if (!$result ne !$grabbers->{$_}->{ready})
503        {
504            $grabbers->{$_}->{ready} = $result;
505        }
506    }
507}
508
509# -----------------------------------------
510# Subs: Setup
511# -----------------------------------------
512
513sub read_config_file
514{
515    read_file($config_file, 'configuration');
516
517    # if we are updating from a previous rev of shepherd.conf we may not
518    # have any 'order' fields set .. check here
519    my $found_order = 1;
520    foreach (keys %$grabbers)
521    {
522        $found_order = 0 if (!defined $grabbers->{$_}->{order});
523    }
524    if (($found_order == 0) && (!$opt->{setorder}))
525    {
526        # at least one 'order' was missing .. we need to put it in!
527        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";
528        &set_order();
529        $opt->{setorder} = "";
530    }
531}
532
533sub read_channels_file
534{
535    read_file($channels_file, 'channels');
536}
537
538sub read_file
539{
540    my $fn = shift;
541    my $name = shift;
542
543    print "Reading $name file: $fn\n";
544    unless (-r $fn)
545    {
546        unless ($opt->{configure})
547        {
548            print "\nNo $name file found.\n" .
549                  ucfirst($progname) . " must be configured: " .
550                  "configuring now.\n\n";
551            $opt->{'configure'} = 1;
552        }
553        return;
554    }
555    local (@ARGV, $/) = ($fn);
556    no warnings 'all';
557    eval <>;
558    if ($@ and !$opt->{configure})
559    {
560        die "\nError in $name file!\nDetails:\n$@";
561    }
562}
563
564sub write_config_file
565{
566    open(CONF, ">$config_file") or die "cannot write to $config_file: $!";
567    print CONF Data::Dumper->Dump(
568        [$region,  $grabbers ],
569        ["region", "grabbers" ]);
570    close CONF;
571    print "\nUpdated configuration file $config_file.\n" if ($debug);
572}
573
574sub write_channels_file
575{
576    open(CHAN, ">$channels_file") or die "cannot write to $channels_file: $!";
577    print CHAN Data::Dumper->Dump([$channels], ["channels"]);
578    close CHAN;
579    print "Updated channels file $channels_file.\n" if ($debug);
580}
581
582sub get_initial_command_line_options
583{
584  GetOptions( 'config-file=s'   => \$opt->{configfile},
585              'help'            => \$opt->{help},
586              'configure'       => \$opt->{configure},
587              'debug'           => \$debug);
588}
589
590sub get_remaining_command_line_options
591{
592    GetOptions(
593              'version'         => \$opt->{status},
594              'status'          => \$opt->{status},
595              'list'            => \$opt->{list},
596              'show-config'     => \$opt->{show_config},
597
598              'update'          => \$opt->{update},
599              'noupdate'        => \$opt->{noupdate},
600
601              'disable=s'       => \$opt->{disable},
602              'enable=s'        => \$opt->{enable},
603              'setorder=s'      => \$opt->{setorder},
604
605              'days=i'          => \$days,
606              'offset=i'        => \$opt->{offset},
607              'show-channels'   => \$opt->{show_channels},
608              'output=s'        => \$opt->{output},
609              'check'           => \$opt->{check}
610            );
611}
612
613
614# -----------------------------------------
615# Subs: Configuration
616# -----------------------------------------
617
618sub configure
619{
620    my $REGIONS = {
621        "ACT" => 126,
622        "NSW: Sydney" => 73,
623        "NSW: Newcastle" => 184,
624        "NSW: Central Coast" => 66,
625        "NSW: Griffith" => 67,
626        "NSW: Broken Hill" => 63,
627        "NSW: Northern NSW" => 69,
628        "NSW: Southern NSW" => 71,
629        "NSW: Remote and Central" => 106,
630        "NT: Darwin" => 74,
631        "NT: Remote & Central" => 108,
632        "QLD: Brisbane" => 75,
633        "QLD: Gold Coast" => 78,
634        "QLD: Regional" => 79,
635        "QLD: Remote & Central" => 114,
636        "SA: Adelaide" => 81,
637        "SA: Renmark" => 82,
638        "SA: Riverland" => 83,
639        "SA: South East SA" => 85,
640        "SA: Spencer Gulf" => 86,
641        "SA: Remote & Central" => 107,
642        "Tasmania" => 88,
643        "VIC: Melbourne" => 94,
644        "VIC: Geelong" => 93,
645        "VIC: Eastern Victoria" => 90,
646        "VIC: Mildura/Sunraysia" => 95,
647        "VIC: Western Victoria" => 98,
648        "WA: Perth" => 101,
649        "WA: Regional" => 102
650    };
651
652    print "\nConfiguring.\n\n" .
653          "Select your region:\n";
654    foreach (sort keys %$REGIONS)
655    {
656        printf(" (%3d) %s\n", $REGIONS->{$_}, $_);
657    }
658    $region = ask_choice("Enter region code:", "94", values %$REGIONS);
659
660    print "\nFetching channel information... ";
661
662    my @channellist = get_channels();
663
664    print "done.\n\n" .
665          "For each channel you want guide data for, enter an XMLTV id\n" .
666          "of your choice (e.g. \"seven.free.au\"). If you don't need\n" .
667          "guide data for this channel, just press Enter.\n\n" .
668          "Please don't subscribe to unneeded channels.\n\nChannels:\n";
669    $channels = {};
670    my $line;
671    foreach (@channellist)
672    {
673        $line = ask(" \"$_\"? ");
674        $channels->{$_} = $line if ($line);
675    }
676
677
678    print "\nRandomly selecting grabber order.\n\n";
679    set_order();
680
681    show_channels();
682    unless(ask_boolean("\nCreate configuration file?"))
683    {
684        print "Aborting configuration.\n";
685        exit 0;
686    }
687
688    write_config_file();
689    write_channels_file();
690
691    print "Finished configuring.\n\n" .
692          "Shepherd is installed into $CWD.\n\n";
693   
694    if ($invoked ne "$CWD/$progname" and $invoked =~ /$progname/)
695    {
696        print "Warning: you invoked this program as $invoked.\n" .
697            "In the future, it should be run as $CWD/$progname,\n" .
698            "to avoid constantly re-downloading the latest version.\n\n" .
699            "MythTV users may wish to create the following symlink, by " .
700            "doing this (as root):\n" .
701            "\"ln -s $CWD/$progname /usr/bin/tv_grab_au\".\n\n" .
702            "You may safely delete $invoked.\n\n";
703    }
704
705    status();
706
707    unless (ask_boolean("\nGrab data now?"))
708    {
709        exit 0;
710    }
711}
712
713sub get_channels
714{
715    my @date = localtime;
716    my $page = LWP::Simple::get(
717        "http://au.tv.yahoo.com/results.html?rg=$region&dt=" .
718        ($date[5] + 1900) . "-$date[4]-$date[3]");
719    my @channellist;
720    while ($page =~ /<tr class=rtb><td class=rth><a .*?>(.*?)<\/a>/g)
721    {
722        push @channellist, $1;
723    }
724    return @channellist;
725}
726
727# -----------------------------------------
728# Subs: Status & Help
729# -----------------------------------------
730
731sub show_config
732{
733    print "\nConfiguration\n".
734          "-------------\n" .
735          "Config file: $config_file\n" .
736          "Debug mode : " . is_set($debug) . "\n" .
737          "Output file: " . ($opt->{output} ? $opt->{output} : "None") . "\n" .
738          "Region ID  : $region\n";
739  show_channels();
740  print "\n";
741  status();
742  print "\n";
743}
744
745sub show_channels
746{
747  print "Subscribed channels:\n";
748  print "    $_ -> $channels->{$_}\n" for sort keys %$channels;
749}
750
751sub is_set
752{
753    my $arg = shift;
754    return $arg ? "Yes" : "No";
755}
756
757sub status
758{
759    print " Grabber                     Version  Enabled  Ready    Last Data\n" .
760          " -----------------------------------------------------------------\n";
761    my $star;
762    foreach (sort { $grabbers->{$a}->{order} <=> $grabbers->{$b}->{order} } keys %$grabbers)
763    {
764        my $h = $grabbers->{$_};
765        printf  " %-26s %8s %5s %7s %10s\n",
766                "$h->{order}. $_",
767                $h->{ver},
768                $h->{disabled} ? '' : 'Y',
769                $h->{ready} ? 'Y' : '',
770                $h->{lastdata} ? localtime($h->{lastdata}) : '';
771    }
772
773    printf "Grabbers shown in order of preference.\n";
774}
775
776sub help
777{
778    print q{
779Command-line options:
780    --help                Print this message
781
782    --status              Print a list of grabbers maintained
783    --list                Print a detailed list of grabbers
784
785    --configure           Setup
786    --show-config         Print setup details
787
788    --setorder <s>        Set order of grabbers to <s> (comma-seperated list of grabbers)
789    --disable <s>         Don't ever use grabber <s>
790    --enable <s>          Okay, maybe use it again then
791    --uninstall <s>       Remove a disabled grabber
792
793    --noupdate            Do not attempt to update before running
794    --update              Update only; do not grab data
795
796    --check               Check status of all grabbers
797};
798    exit 0;
799}
Note: See TracBrowser for help on using the browser.