#!/usr/bin/perl -w # "Shepherd" my $version = '0.2.6'; # A wrapper for various Aussie TV guide data grabbers # # Use --help for command-line options. # See shepherd.txt for details. # # A current version of this script, plus a README file, might be here: # http://www.whuffy.com/tv_grab_au/ # # Changelog: # 0.1.0 : Basic self-updating and grabber management # 0.2.0 : --configure # 0.2.1 : Has a home in ~/.shepherd/ # 0.2.2 : --check # 0.2.3 : Bugfix: archives correctly # 0.2.5 : multi-grabber (potentially with partial data) # 0.2.6 : postprocessor support # # ToDo: # * Make it check compilation after installing by calling --version or # --desc or --ready # * --ready option that says whether config is required? BEGIN { *CORE::GLOBAL::die = \&my_die; } use strict; use LWP::Simple; use Sort::Versions; use Cwd; use Getopt::Long; use Data::Dumper; use XMLTV; use XMLTV::Ask; use DateTime::Format::Strptime; use POSIX qw(strftime); use Time::HiRes qw(gettimeofday tv_interval); # --------------------------------------------------------------------------- # --- Global Variables # --------------------------------------------------------------------------- my $progname = 'shepherd'; my $HOME = 'http://www.whuffy.com'; my $invoked = Cwd::realpath($0); # By default, Shepherd runs from ~/.shepherd/. If it's not run as a user, # it will try /opt/shepherd/ instead. my $CWD = ($ENV{HOME} ? $ENV{HOME} . "/." : "/opt/") . $progname; -d $CWD or mkdir $CWD or die "Cannot create directory $CWD: $!"; chdir($CWD); my $GRABBER_DIR = "$CWD/grabbers"; my $POSTPROCESSOR_DIR = "$CWD/postprocessors"; my $ARCHIVE_DIR = "$CWD/archive"; my $timeslot_size = (15 * 60); # 15 minute slots my $channel_ok_threshold_percent = 90; # 90% these may need to be tweaked but look ok for now my $postprocessor_ok_threshold_percent = 80; # 80% these may need to be tweaked but look ok for now my $postprocessor_disable_failure_threshold = 5; # number of times a postprocessor has to fail in a row before it is automatically disabled my $opt; my $pref_order; my $mirror_site; my $made_changes = 0; my $debug = 1; my $grabbers = { }; my $postprocessors = { }; my $preferred; # obsolete but may still exist in shepherd.conf my $region; my $channels; my $config_file = "$CWD/$progname.conf"; my $channels_file = "$CWD/channels.conf"; my $days; # postprocessing my $langs = [ 'en' ]; my $num_timeslots; my $plugin_data = { }; my $channel_data = { }; my $starttime, my $endtime; my $input_postprocess_files = ""; my $insufficient_grabber_data = 0; # --------------------------------------------------------------------------- # --- Setup # --------------------------------------------------------------------------- print ucfirst($progname) . " v$version\n\n"; #print "Cwd: $CWD.\n"; # Any options Shepherd doesn't understand, we'll pass to the grabber(s) Getopt::Long::Configure(qw/pass_through/); get_initial_command_line_options(); help() if ($opt->{help}); unless ($opt->{configure}) { read_config_file(); read_channels_file(); } get_remaining_command_line_options(); if ($opt->{status}) { status(); exit; } if ($opt->{show_config}) { show_config(); exit; } if ($opt->{enable}) { enable($opt->{enable}); } if ($opt->{disable}) { disable($opt->{disable}); } &set_order(0,$opt->{setorder}) if ($opt->{setorder}); &check() if ($opt->{check}); if ($opt->{enable} or $opt->{disable} or $opt->{setorder} or $opt->{check} or $opt->{mirror}) { set_order(1) if $made_changes; write_config_file() if $made_changes; status(); exit; } # --------------------------------------------------------------------------- # --- Update # --------------------------------------------------------------------------- unless ($opt->{noupdate}) { update($progname, $version); set_order(1) if $made_changes; write_config_file() if (($made_changes) && (! $opt->{configure})) } if ($opt->{configure}) { configure(); } # --------------------------------------------------------------------------- # --- Go! # --------------------------------------------------------------------------- unless ($opt->{update}) { calc_date_range(); grab_data(); postprocess_data(); output_data(); } print "Done.\n"; status(); write_config_file(); # --------------------------------------------------------------------------- # --- Subroutines # --------------------------------------------------------------------------- # ----------------------------------------- # Subs: Grabbing # ----------------------------------------- sub grab_data { my $used_grabbers = 0; my $need_more_data = 1; printf "\nGrabber stage:\n"; # iterate across grabbers until we have all our data we want (or need) foreach my $grabber (sort { $grabbers->{$a}->{order} <=> $grabbers->{$b}->{order} } keys %$grabbers) { next if ($grabbers->{$grabber}->{disabled}); $used_grabbers++; $grabbers->{$grabber}->{lastdata} = time; $grabbers->{$grabber}->{laststatus} = "unknown"; printf "\nSHEPHERD: Using grabber: (%d) %s\n",$grabbers->{$grabber}->{order},$grabber; my $output = "$GRABBER_DIR/$grabber/output.xmltv"; $input_postprocess_files .= "$output "; my $comm = "$GRABBER_DIR/$grabber/$grabber " . "--region $region " . "--channels_file $channels_file " . "--output $output"; # NOTE: ideally a grabber could be instructed to fetch partial data through --channel, --starttime & --endtime # we don't have that for now so instead whenever there is missing data, ALL 7 days for all channels will be collected # FIXME FUTURE: call grabbers just with what we want... $comm .= " --days $days" if ($days); $comm .= " --offset $opt->{offset}" if ($opt->{offset}); $comm .= " --debug" if ($debug); $comm .= " @ARGV" if (@ARGV); print "SHEPHERD: Excuting command: $comm\n"; chdir "$GRABBER_DIR/$grabber/"; system($comm); chdir $CWD; # soak up the data we just collected &soak_up_data($grabber, $output, "grabber"); $grabbers->{$grabber}->{laststatus} = $plugin_data->{$grabber}->{laststatus}; # check to see if we have all the data we want $need_more_data = &analyze_plugin_data($channel_ok_threshold_percent, "AGGREGATE GRABBER"); last if (!$need_more_data); } if ($used_grabbers == 0) { print "No valid grabbers installed/enabled!\n"; return; } if ($need_more_data) { print "SHEPHERD: Ran through ALL grabbers but still missing data!!! :(\n"; $insufficient_grabber_data = 1; return; } } # interpret xmltv data from this grabber/postprocessor sub soak_up_data { my ($plugin, $output, $plugintype) = @_; if (! -r $output) { printf "SHEPHERD: Warning: plugin '%s' output file '%s' does not exist\n",$plugin,$output; return; } my $parse_start_time = [gettimeofday]; printf STDERR "SHEPHERD: Started parsing XMLTV from '%s' in '%s' .. any errors below are from parser:\n",$plugin,$output; eval { $plugin_data->{$plugin}->{xmltv} = XMLTV::parsefiles($output); }; printf STDERR "SHEPHERD: Completed XMLTV parsing from '%s' in %0.2f seconds\n",$plugin,tv_interval($parse_start_time); if (defined $plugin_data->{$plugin}->{xmltv}) { $plugin_data->{$plugin}->{valid} = 1; my $xmltv = $plugin_data->{$plugin}->{xmltv}; my ($encoding, $credits, $chan, $progs) = @$xmltv; $plugin_data->{$plugin}->{total_duration} = 0; $plugin_data->{$plugin}->{programmes} = 0; my $strptime = new DateTime::Format::Strptime( pattern => "%Y%m%d%H%M %z"); my $seen_channels_with_data = 0; # iterate thru channels foreach my $ch (sort keys %{$channels}) { my $seen_progs_on_this_channel = 0; # iterate thru programmes per channel foreach my $prog (@$progs) { next if ($prog->{channel} ne $channels->{$ch}); my $t1 = $strptime->parse_datetime($prog->{start}); my $t2 = $strptime->parse_datetime($prog->{stop}); next if (!$t1 || !$t2); # if we can't parse stop/start then clearly THIS data is bunk! # store plugin-specific stats $plugin_data->{$plugin}->{programmes}++; $plugin_data->{$plugin}->{total_duration} += ($t2->epoch - $t1->epoch); $seen_progs_on_this_channel++; $plugin_data->{$plugin}->{earliest_data_seen} = $t1->epoch if (!defined $plugin_data->{$plugin}->{earliest_data_seen}); $plugin_data->{$plugin}->{earliest_data_seen} = $t1->epoch if ($t1->epoch < $plugin_data->{$plugin}->{earliest_data_seen}); $plugin_data->{$plugin}->{latest_data_seen} = $t2->epoch if (!defined $plugin_data->{$plugin}->{latest_data_seen}); $plugin_data->{$plugin}->{latest_data_seen} = $t2->epoch if ($t2->epoch > $plugin_data->{$plugin}->{latest_data_seen}); # store channel-specific stats $channel_data->{$ch}->{programmes}++; $channel_data->{$ch}->{total_duration} += ($t2->epoch - $t1->epoch); # store timeslot info 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! 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! my $start_slotnum; if ($t1->epoch >= $starttime) { $start_slotnum = int(($t1->epoch - $starttime) / $timeslot_size); } else { $start_slotnum = 0; } my $end_slotnum; if ($t2->epoch < $endtime) { $end_slotnum = int(($t2->epoch - $starttime) / $timeslot_size); } else { $end_slotnum = ($num_timeslots-1); } # add this programme into the global timeslots table for this channel foreach my $slotnum ($start_slotnum..$end_slotnum) { $channel_data->{$ch}->{timeslots}[$slotnum]++; } } $seen_channels_with_data++ if ($seen_progs_on_this_channel > 0); } # print some stats about what we saw! printf "SHEPHERD: %s '%s' returned data for %d channels, %d programmes, %dd%02dh%02dm%02ds duration, %s%s\n", ucfirst($plugintype), $plugin, $seen_channels_with_data, $plugin_data->{$plugin}->{programmes}, int($plugin_data->{$plugin}->{total_duration} / 86400), # days int(($plugin_data->{$plugin}->{total_duration} % 86400) / 3600), # hours int(($plugin_data->{$plugin}->{total_duration} % 3600) / 60), # mins int($plugin_data->{$plugin}->{total_duration} % 60), # sec (defined $plugin_data->{$plugin}->{earliest_data_seen} ? (strftime "%a %e %b %H:%M - ", localtime($plugin_data->{$plugin}->{earliest_data_seen})) : 'no data'), (defined $plugin_data->{$plugin}->{latest_data_seen} ? (strftime "%a %e %b %H:%M", localtime($plugin_data->{$plugin}->{latest_data_seen})) : ''); $plugin_data->{$plugin}->{laststatus} = sprintf "%dch/%dpr/%dhrs %s-%s", $seen_channels_with_data, $plugin_data->{$plugin}->{programmes}, int($plugin_data->{$plugin}->{total_duration} / 3600), (defined $plugin_data->{$plugin}->{earliest_data_seen} ? (strftime "%a%d%b%H:%M", localtime($plugin_data->{$plugin}->{earliest_data_seen})) : 'no'), (defined $plugin_data->{$plugin}->{latest_data_seen} ? (strftime "%a%d%b%H:%M", localtime($plugin_data->{$plugin}->{latest_data_seen})) : 'data'); } else { printf "WARNING: Plugin %s didn't seem to return any valid XMLTV!\n",$plugin; delete $plugin_data->{$plugin}->{valid}; } } # analyze grabber data - do we have all the data we want? # returns 1 if we need more data, 0 if we have all we want sub analyze_plugin_data { my ($threshold,$analysistype) = @_; my $retval = 0; # until proven otherwise my $total_data_percent = 0, my $total_channels = 0; my $statusstring = ""; # iterate across each channel foreach my $ch (sort keys %{$channels}) { $total_channels++; if (defined $channel_data->{$ch}) { my $data_in_channel = 0; for my $slotnum (0..($num_timeslots-1)) { $data_in_channel++ if ((defined $channel_data->{$ch}->{timeslots}[$slotnum]) && ($channel_data->{$ch}->{timeslots}[$slotnum] > 0)); } # do we have enough data for this channel? my $data_in_channel_percent = $data_in_channel / ($num_timeslots-1) * 100; if ($data_in_channel_percent >= $threshold) { $statusstring .= sprintf "%s: %0.1f%% [complete], ",$ch,$data_in_channel_percent; } else { $statusstring .= sprintf "%s: %0.1f%% [hungry], ",$ch,$data_in_channel_percent; $retval = 1; } $total_data_percent += $data_in_channel_percent; } else { $statusstring .= sprintf "%s: 0%% [starving], ",$ch; $retval = 1; } } if ($total_channels > 0) { $total_data_percent = $total_data_percent / $total_channels; } else { $total_data_percent = 0; } # print some stats about what our analysis says! printf "SHEPHERD: %s ANALYSIS: %sTOTAL %0.2f%% %s %0.2f%%: %s\n", uc($analysistype), $statusstring, $total_data_percent, ($total_data_percent >= $channel_ok_threshold_percent ? ">" : "<"), $channel_ok_threshold_percent, ($retval ? "WANT MORE DATA" : "COMPLETE"); return $retval; } # work out date range we are expecting data to be in sub calc_date_range { # normalize starttime to beginning of hour my $now = time; my ($sec,$min,@rest) = localtime($now); $starttime = $now - ((60 * $min) + $sec); if ($days) { $endtime = $starttime + ($days * 86400); } else { $endtime = $starttime + (7*86400); } $starttime += (86400 * $opt->{offset}) if ($opt->{offset}); $num_timeslots = ($endtime - $starttime) / $timeslot_size; } # ----------------------------------------- # Subs: Postprocessing # ----------------------------------------- sub postprocess_data { # for our first postprocessor, we feed it ALL of the XMLTV files we have # as each postprocessor runs, we feed in the output from the previous one # Shepherd checks the "completeness" of the data that comes out of a postprocessor & automatically # reverts back to the previous postprocessor if it was shown to be bad # first time around: feed in $input_postprocess_files my $need_more_data; printf "\nPostprocessing stage:\n"; foreach my $postprocessor (sort { $postprocessors->{$a} <=> $postprocessors->{$b} } keys %$postprocessors) { next if ($postprocessors->{$postprocessor}->{disabled}); $postprocessors->{$postprocessor}->{lastdata} = time; $postprocessors->{$postprocessor}->{laststatus} = "unknown"; printf "\nSHEPHERD: Using postprocessor: %s\n",$postprocessor; my $output = "$POSTPROCESSOR_DIR/$postprocessor/output.xmltv"; my $comm = "$POSTPROCESSOR_DIR/$postprocessor/$postprocessor " . "--region $region " . "--channels_file $channels_file " . "--output $output"; $comm .= " --days $days" if ($days); $comm .= " --offset $opt->{offset}" if ($opt->{offset}); $comm .= " --debug" if ($debug); $comm .= " @ARGV" if (@ARGV); $comm .= " $input_postprocess_files"; print "SHEPHERD: Excuting command: $comm\n"; chdir "$POSTPROCESSOR_DIR/$postprocessor/"; system($comm); chdir $CWD; # # soak up the data we just collected and check it # YES - these are the SAME routines we used in the previous 'grabber' phase # but the difference here is that we clear out our 'channel_data' beforehand # so we can independently analyze the impact of this postprocessor. # if it clearly returns bad data, don't use that data (go back one step) and # flag the postprocessor as having failed. after 3 consecutive failures, disable it # # clear out channel_data foreach my $ch (keys %{$channels}) { delete $channel_data->{$ch}; } # process and analyze it! &soak_up_data($postprocessor, $output, "postprocessor"); $need_more_data = &analyze_plugin_data($postprocessor_ok_threshold_percent, "POSTPROCESSOR"); $postprocessors->{$postprocessor}->{laststatus} = $plugin_data->{$postprocessor}->{laststatus}; if (($need_more_data) && (!$insufficient_grabber_data)) { # urgh. this postprocessor did a bad bad thing ... printf "SHEPHERD: XML data from postprocessor %s rejected, using XML from previous stage\n",$postprocessor; if (defined $postprocessors->{$postprocessor}->{conescutive_failures}) { $postprocessors->{$postprocessor}->{conescutive_failures}++; } else { $postprocessors->{$postprocessor}->{conescutive_failures} = 1; } printf "SHEPHERD: Postprocessor \"%s\" has now failed %d times in a row. %d more and it will be automatically disabled.\n", $postprocessor, $postprocessors->{$postprocessor}->{conescutive_failures}, ($postprocessor_disable_failure_threshold - $postprocessors->{$postprocessor}->{conescutive_failures}); if ($postprocessors->{$postprocessor}->{conescutive_failures} >= $postprocessor_disable_failure_threshold) { printf "SHEPHERD: Disabling Postprocessor \"%s\".\n",$postprocessor; $postprocessors->{$postprocessor}->{disabled} = 1; } } else { # accept what this postprocessor did to our output ... printf "SHEPHERD: accepting output from postprocessor %s, feeding it into next stage\n",$postprocessor; $input_postprocess_files = $output; delete $postprocessors->{$postprocessor}->{conescutive_failures} if (defined $postprocessors->{$postprocessor}->{conescutive_failures}); } } } sub output_data { # $input_postprocess_files (hopefully just one file now) contains our final output # send it to whereever --output told us to! my $output_filename = "$CWD/output.xmltv"; $output_filename = $opt->{output} if ($opt->{output}); open(F,">$output_filename") || die "could not open output file $output_filename for writing: $!\n"; foreach my $infile (split(/ /,$input_postprocess_files)) { if (!(open(INFILE,"<$infile"))) { printf "WARNING: could not open input file \"%s\": %s\n", $infile, $!; printf "Output XMLTV data may be damanged as a result!\n"; } else { while () { print F $_ if ($opt->{output}); } close(INFILE); } } close(F); printf "Final output stored in $output_filename.\n"; } # ----------------------------------------- # Subs: Updates & Installations # ----------------------------------------- sub update { printf "\nChecking for updates:\n\n"; my $data; my $sites = ""; $sites = "$mirror_site," if ($mirror_site); $sites .= $HOME; foreach my $site (split(/,/,$sites)) { my $url = $site . "/status"; print "Fetching status file: $url.\n"; $data = LWP::Simple::get($url); last if $data; print "Failed to retrieve status file from $url.\n"; } return if (!$data); my %glist = %$grabbers; my %plist = %$postprocessors; while ($data =~ /(.*):(.*):(.*)/g) { my ($proggy, $latestversion, $progtype) = ($1,$2,$3); update_component($proggy, $latestversion, $progtype); delete $glist{$proggy} if ($progtype eq "grabber"); delete $plist{$proggy} if ($progtype eq "postprocessor"); } # work out what grabbers disappeared (if any) foreach (keys %glist) { unless ($grabbers->{$_}->{disabled}) { print "\nDeleted grabber: $_.\n"; disable($_,"grabber"); $made_changes = 1; } } # work out what postprocessors disappeared (if any) foreach (keys %plist) { unless ($postprocessors->{$_}->{disabled}) { print "\nDeleted Postprocessor: $_.\n"; disable($_,"postprocessor"); $made_changes = 1; } } } sub update_component { my ($proggy, $latestversion, $progtype) = @_; # handle new installs.. if (($proggy eq $progname) && ($progtype eq "shepherd")) { # shepherd itself.. if(! -e "$CWD/$progname") { print "Missing: $CWD/$progname\n"; install($progname, $latestversion, $progtype); return; } } elsif ($progtype eq "grabber") { if (!defined $grabbers->{$proggy} or ! -e "$GRABBER_DIR/$proggy/$proggy") { print "New grabber: $proggy.\n"; install($proggy, $latestversion, $progtype); return; } print "Warning: grabber $proggy disabled by config file.\n" if ($grabbers->{$proggy}->{disabled}); } elsif ($progtype eq "postprocessor") { if (!defined $postprocessors->{$proggy} or ! -e "$POSTPROCESSOR_DIR/$proggy/$proggy") { print "New postprocessor: $proggy.\n"; install($proggy, $latestversion, $progtype); return; } print "Warning: postprocessor $proggy disabled by config file.\n" if ($postprocessors->{$proggy}->{disabled}); } # upgrade/downgrades my $ver; if ($progtype eq "grabber") { $ver = ($proggy eq $progname ? $version : $grabbers->{$proggy}->{ver}); } elsif ($progtype eq "postprocessor") { $ver = ($proggy eq $progname ? $version : $postprocessors->{$proggy}->{ver}); } elsif (($proggy eq $progname) && ($progtype eq "shepherd")) { $ver = $version; } else { print "Warning: unknown type of programme: prog '$proggy' progtype '$progtype' not installed.\n"; return; } my $result = versioncmp($ver, $latestversion); if ($result == -1) { print "Upgrading $proggy from v$ver to v$latestversion.\n"; } elsif ($result == 1) { print "Downgrading $proggy from v$ver to v$latestversion.\n"; } else { print "Already have latest version of $proggy: v$ver.\n"; return; } install($proggy, $latestversion, $progtype); } sub install { my ($proggy, $latestversion, $progtype) = @_; print "Downloading $proggy v$latestversion.\n"; my $sites = ""; $sites = "$mirror_site," if ($mirror_site); $sites .= $HOME; my $rdir = ""; my $ldir = $CWD; my $ver = "unknown"; if (($proggy eq $progname) && ($progtype eq "shepherd")) { $ver = $version; } elsif ($progtype eq "grabber") { $rdir = "grabbers"; $ldir = "$GRABBER_DIR/$proggy"; $ver = $grabbers->{$proggy}->{ver} if ((defined $grabbers->{$proggy}) && $grabbers->{$proggy}->{ver}); -d $GRABBER_DIR or mkdir $GRABBER_DIR or die "Cannot create directory $GRABBER_DIR: $!"; } elsif ($progtype eq "postprocessor") { $rdir = "postprocessors"; $ldir = "$POSTPROCESSOR_DIR/$proggy"; $ver = $postprocessors->{$proggy}->{ver} if ((defined $postprocessors->{$proggy}) && $postprocessors->{$proggy}->{ver}); -d $POSTPROCESSOR_DIR or mkdir $POSTPROCESSOR_DIR or die "Cannot create directory $POSTPROCESSOR_DIR: $!"; } else { print "Warning: unknown type of programme: prog '$proggy' progtype '$progtype' not installed.\n"; return; } -d $ldir or mkdir $ldir or die "Cannot create directory $ldir: $!"; my $newfile = "$ldir/$proggy-$latestversion"; my $rc; foreach my $site (split(/,/,$sites)) { printf "Fetching $site/$rdir/$proggy-$latestversion.\n"; $rc = LWP::Simple::getstore("$site/$rdir/$proggy-$latestversion", $newfile); last if (is_success($rc)); print "Failed to retrieve $site/$rdir/$proggy-$latestversion.\n"; } return if (!is_success($rc)); # Make it executable system('chmod u+x ' . $newfile); -d $ARCHIVE_DIR or mkdir $ARCHIVE_DIR or die "Cannot create directory $ARCHIVE_DIR: $!"; if (-e "$ldir/$proggy") { rename("$ldir/$proggy", "$ARCHIVE_DIR/$proggy-$ver"); } rename($newfile, "$ldir/$proggy"); print "Installed $proggy v$latestversion.\n" if ($debug); # if the update was for shepherd itself, restart it if (($proggy eq $progname) && ($progtype eq "shepherd")) { print "\n*** Restarting ***\n\n"; exec("$ldir/$proggy"); # This exits. } print "Testing $proggy...\n" if ($debug); my $result = test_proggy($ldir,"$ldir/$proggy"); if ($progtype eq "grabber") { $grabbers->{$proggy}->{ver} = $latestversion; $grabbers->{$proggy}->{ready} = $result; $grabbers->{$proggy}->{laststatus} = sprintf "updated to %s on %s", $latestversion, (strftime "%a%d%b%y",localtime(time)); } elsif ($progtype eq "postprocessor") { $postprocessors->{$proggy}->{ver} = $latestversion; $postprocessors->{$proggy}->{ready} = $result; $postprocessors->{$proggy}->{laststatus} = sprintf "updated to %s on %s", $latestversion, (strftime "%a%d%b%y",localtime(time)); } $made_changes = 1; } sub test_proggy { my ($testdir,$proggyexec) = @_; chdir($testdir); system("$proggyexec --ready"); chdir ($CWD); my $result = $?; print "Return value: $result\n" if ($debug); print "\nprogramme $proggyexec did not exit cleanly!\n" . "It may require configuration.\n\n" if ($result); return !$result; } sub enable { my $proggy = shift; # confirm it exists first if ((!$grabbers->{$proggy}) && (!$postprocessors->{$proggy})) { printf "No such grabber/postprocessor: \"%s\".\n",$proggy; return; } print "Enabling $proggy.\n"; if ($grabbers->{$proggy}) { delete $grabbers->{$proggy}->{disabled}; $grabbers->{$proggy}->{laststatus} = sprintf "enabled on %s, not run yet",(strftime "%a%d%b%y", localtime(time)); } elsif ($postprocessors->{$proggy}) { delete $postprocessors->{$proggy}->{disabled}; $postprocessors->{$proggy}->{laststatus} = sprintf "enabled on %s, not run yet",(strftime "%a%d%b%y", localtime(time)); } $made_changes = 1; } sub disable { my $proggy = shift; # confirm it exists first if ((!$grabbers->{$proggy}) && (!$postprocessors->{$proggy})) { printf "No such grabber/postprocessor: \"%s\".\n",$proggy; return; } print "Disabling $proggy.\n"; if ($grabbers->{$proggy}) { $grabbers->{$proggy}->{disabled} = 1; $grabbers->{$proggy}->{laststatus} = sprintf "manually disabled on %s",(strftime "%a%d%b%y", localtime(time)); } elsif ($postprocessors->{$proggy}) { $postprocessors->{$proggy}->{disabled} = 1; $postprocessors->{$proggy}->{laststatus} = sprintf "manually disabled on %s",(strftime "%a%d%b%y", localtime(time)); } $made_changes = 1; } sub set_order { my ($quiet,$order) = @_; $pref_order = $order if ($order); # reset current order to zero foreach my $proggy (keys %$grabbers) { $grabbers->{$proggy}->{order} = 0; } # and now set order my $order_num = 1; if ($pref_order) { foreach my $proggy (split(/,/,$pref_order)) { if (defined $grabbers->{$proggy}) { $grabbers->{$proggy}->{order} = $order_num; $order_num++; } } } # set order of any grabbers not specified in a random manner foreach my $proggy (sort keys %$grabbers) { if ((!defined $grabbers->{$proggy}->{order}) || ($grabbers->{$proggy}->{order} == 0)) { $grabbers->{$proggy}->{order} = $order_num+int(rand(1000)); } } # .. and finally normalize the order (& show the user the order we chose) print "Grabber order set as follows:\n" unless $quiet; $order_num = 0; foreach my $proggy (sort { $grabbers->{$a}->{order} <=> $grabbers->{$b}->{order} } keys %$grabbers) { $order_num++; $grabbers->{$proggy}->{order} = $order_num; printf " #%d. %s%s\n",$grabbers->{$proggy}->{order},$proggy,($grabbers->{$proggy}->{disabled} ? " [disabled]" : "") unless $quiet; } $made_changes = 1; } sub check { my $result; foreach my $proggy (keys %$grabbers) { $result = test_proggy("$GRABBER_DIR/$proggy","$GRABBER_DIR/$proggy/$proggy"); printf "Grabber %s: %s\n",$proggy,($result ? "OK" : "Failed"); if (!$result ne !$grabbers->{$proggy}->{ready}) { $grabbers->{$proggy}->{ready} = $result; $made_changes = 1; } } foreach my $proggy (keys %$postprocessors) { $result = test_proggy("$POSTPROCESSOR_DIR/$proggy","$POSTPROCESSOR_DIR/$proggy/$proggy"); printf "Postprocessor %s: %s\n",$proggy,($result ? "OK" : "Failed"); if (!$result ne !$postprocessors->{$proggy}->{ready}) { $postprocessors->{$proggy}->{ready} = $result; $made_changes = 1; } } } # ----------------------------------------- # Subs: Setup # ----------------------------------------- sub read_config_file { read_file($config_file, 'configuration'); # if we are updating from a previous rev of shepherd.conf we may not # have any 'order' fields set .. check here my $found_order = 1; foreach (keys %$grabbers) { $found_order = 0 if (!defined $grabbers->{$_}->{order}); } if (($found_order == 0) && (!$opt->{setorder})) { # at least one 'order' was missing .. we need to put it in! 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"; &set_order(1); } # if a mirror has been specified, add it into our config if ($opt->{mirror}) { $mirror_site = $opt->{mirror}; $made_changes = 1; print "Adding mirror: $mirror_site\n"; } } sub read_channels_file { read_file($channels_file, 'channels'); } sub read_file { my $fn = shift; my $name = shift; print "Reading $name file: $fn\n"; unless (-r $fn) { unless ($opt->{configure}) { print "\nNo $name file found.\n" . ucfirst($progname) . " must be configured: " . "configuring now.\n\n"; $opt->{'configure'} = 1; } return; } local (@ARGV, $/) = ($fn); no warnings 'all'; eval <>; if ($@ and !$opt->{configure}) { die "\nError in $name file!\nDetails:\n$@"; } } sub write_config_file { open(CONF, ">$config_file") or die "cannot write to $config_file: $!"; print CONF Data::Dumper->Dump( [$region, $pref_order, $mirror_site, $grabbers, $postprocessors ], ["region", "pref_order", "mirror_site", "grabbers", "postprocessors" ]); close CONF; print "\nUpdated configuration file $config_file.\n" if ($debug); } sub write_channels_file { open(CHAN, ">$channels_file") or die "cannot write to $channels_file: $!"; print CHAN Data::Dumper->Dump([$channels], ["channels"]); close CHAN; print "Updated channels file $channels_file.\n" if ($debug); } sub get_initial_command_line_options { GetOptions( 'config-file=s' => \$opt->{configfile}, 'help' => \$opt->{help}, 'configure' => \$opt->{configure}, 'mirror=s' => \$opt->{mirror}, 'debug' => \$debug); } sub get_remaining_command_line_options { GetOptions( 'version' => \$opt->{status}, 'status' => \$opt->{status}, 'list' => \$opt->{list}, 'show-config' => \$opt->{show_config}, 'update' => \$opt->{update}, 'noupdate' => \$opt->{noupdate}, 'disable=s' => \$opt->{disable}, 'enable=s' => \$opt->{enable}, 'setorder=s' => \$opt->{setorder}, 'days=i' => \$days, 'offset=i' => \$opt->{offset}, 'show-channels' => \$opt->{show_channels}, 'output=s' => \$opt->{output}, 'check' => \$opt->{check} ); } # ----------------------------------------- # Subs: Configuration # ----------------------------------------- sub configure { my $REGIONS = { "ACT" => 126, "NSW: Sydney" => 73, "NSW: Newcastle" => 184, "NSW: Central Coast" => 66, "NSW: Griffith" => 67, "NSW: Broken Hill" => 63, "NSW: Northern NSW" => 69, "NSW: Southern NSW" => 71, "NSW: Remote and Central" => 106, "NT: Darwin" => 74, "NT: Remote & Central" => 108, "QLD: Brisbane" => 75, "QLD: Gold Coast" => 78, "QLD: Regional" => 79, "QLD: Remote & Central" => 114, "SA: Adelaide" => 81, "SA: Renmark" => 82, "SA: Riverland" => 83, "SA: South East SA" => 85, "SA: Spencer Gulf" => 86, "SA: Remote & Central" => 107, "Tasmania" => 88, "VIC: Melbourne" => 94, "VIC: Geelong" => 93, "VIC: Eastern Victoria" => 90, "VIC: Mildura/Sunraysia" => 95, "VIC: Western Victoria" => 98, "WA: Perth" => 101, "WA: Regional" => 102 }; print "\nConfiguring.\n\n" . "Select your region:\n"; foreach (sort keys %$REGIONS) { printf(" (%3d) %s\n", $REGIONS->{$_}, $_); } $region = ask_choice("Enter region code:", "94", values %$REGIONS); print "\nFetching channel information... "; my @channellist = get_channels(); print "done.\n\n" . "For each channel you want guide data for, enter an XMLTV id\n" . "of your choice (e.g. \"seven.free.au\"). If you don't need\n" . "guide data for this channel, just press Enter.\n\n" . "Please don't subscribe to unneeded channels.\n\nChannels:\n"; $channels = {}; my $line; foreach (@channellist) { $line = ask(" \"$_\"? "); $channels->{$_} = $line if ($line); } print "\nRandomly selecting grabber order.\n\n"; set_order(0); show_channels(); unless(ask_boolean("\nCreate configuration file?")) { print "Aborting configuration.\n"; exit 0; } write_config_file(); write_channels_file(); print "Finished configuring.\n\n" . "Shepherd is installed into $CWD.\n\n"; if ($invoked ne "$CWD/$progname" and $invoked =~ /$progname/) { print "Warning: you invoked this program as $invoked.\n" . "In the future, it should be run as $CWD/$progname,\n" . "to avoid constantly re-downloading the latest version.\n\n" . "MythTV users may wish to create the following symlink, by " . "doing this (as root):\n" . "\"ln -s $CWD/$progname /usr/bin/tv_grab_au\".\n\n" . "You may safely delete $invoked.\n\n"; } status(); unless (ask_boolean("\nGrab data now?")) { exit 0; } } sub get_channels { my @date = localtime; my $page = LWP::Simple::get( "http://au.tv.yahoo.com/results.html?rg=$region&dt=" . ($date[5] + 1900) . "-$date[4]-$date[3]"); my @channellist; while ($page =~ /(.*?)<\/a>/g) { push @channellist, $1; } return @channellist; } # ----------------------------------------- # Subs: Status & Help # ----------------------------------------- sub show_config { print "\nConfiguration\n". "-------------\n" . "Config file: $config_file\n" . "Debug mode : " . is_set($debug) . "\n" . "Output file: " . ($opt->{output} ? $opt->{output} : "None") . "\n" . "Region ID : $region\n"; show_channels(); print "\n"; status(); print "\n"; } sub show_channels { print "Subscribed channels:\n"; print " $_ -> $channels->{$_}\n" for sort keys %$channels; } sub is_set { my $arg = shift; return $arg ? "Yes" : "No"; } sub status { print " Grabber Version Enabled Ready Last Run Status\n" . " ----------------- ------- ------- ----- ---------- ---------------------------\n"; foreach (sort { $grabbers->{$a}->{order} <=> $grabbers->{$b}->{order} } keys %$grabbers) { my $h = $grabbers->{$_}; printf " %-16s %8s %4s %6s %11s %s\n", "$h->{order}. $_", ($h->{ver} ? $h->{ver} : "unknown"), $h->{disabled} ? '' : 'Y', $h->{ready} ? 'Y' : '', $h->{lastdata} ? (strftime "%a%d%b%y", localtime($h->{lastdata})) : 'never', $h->{laststatus} ? $h->{laststatus} : ''; } printf "Grabbers shown in order of preference.\n\n"; print " Postprocessor Version Enabled Ready Last Run Status\n" . " ----------------- ------- ------- ----- ---------- ---------------------------\n"; foreach (sort { $postprocessors->{$a} <=> $postprocessors->{$b} } keys %$postprocessors) { my $h = $postprocessors->{$_}; printf " %-16s %8s %4s %6s %11s %s\n", $_, ($h->{ver} ? $h->{ver} : "unknown"), $h->{disabled} ? '' : 'Y', $h->{ready} ? 'Y' : '', $h->{lastdata} ? (strftime "%a%d%b%y", localtime($h->{lastdata})) : 'never', $h->{laststatus} ? $h->{laststatus} : ''; } printf "Postprocessors shown in order of execution.\n\n"; } sub help { print q{ Command-line options: --help Print this message --status Print a list of grabbers maintained --list Print a detailed list of grabbers --mirror Set URL as primary location to check for updates --configure Setup --show-config Print setup details --setorder Set order of grabbers to (comma-seperated list of grabbers) --disable Don't ever use grabber/postprocessor --enable Okay, maybe use it again then --uninstall Remove a disabled grabber/postprocessor --noupdate Do not attempt to update before running --update Update only; do not grab data --check Check status of all grabbers and postprocessors }; exit 0; } # ----------------------------------------- # Subs: override handlers for standard perl. # ----------------------------------------- # ugly hack. please don't try this at home kids! sub my_die { my ($arg,@rest) = @_; my ($pack,$file,$line,$sub) = caller(1); # check if we are in an eval() if ($^S) { printf STDERR " shepherd caught a die() within eval{} from file $file line $line\n"; } else { if (!ref($arg)) { CORE::die((sprintf "DIE at line %d in file %s: %s\n",$line,$file,(join("",($arg,@rest))))); } else { CORE::die($arg,@rest); } } }