/usr/local/CPAN/XML-RSS-TimingBot/XML/RSS/TimingBot.pm
require 5;
package XML::RSS::TimingBot; # Time-stamp: "2004-05-19 12:50:53 ADT"
use LWP::UserAgent::Determined ();
@ISA = ('LWP::UserAgent::Determined');
use strict;
use vars qw($VERSION);
$VERSION = '2.03';
use LWP::Debug ();
use XML::RSS::Timing ();
BEGIN { *DEBUG = sub () {0} unless defined &DEBUG }
die "Where's _elem?!!?" unless __PACKAGE__->can('_elem');
#--------------------------------------------------------------------------
# Some incidental accessors:
sub minAge { shift->_elem( 'minAge' , @_) }
sub maxAge { shift->_elem( 'maxAge' , @_) }
sub min_age { shift->minAge(@_) } #alias
sub max_age { shift->maxAge(@_) } #alias
sub rss_semaphore_file { shift->_elem( 'rss_semaphore_file' , @_) }
#==========================================================================
sub feed_get_last_modified {
# This is not an epochtime, it's a string that we probably got
# from the remote server.
# As says RFC2616 section 14.25 : <<
# Note: When handling an If-Modified-Since header field, some
# servers will use an exact date comparison function, rather than a
# less-than function, for deciding whether to send a 304 (Not
# Modified) response. To get best results when sending an If-
# Modified-Since header field for cache validation, clients are
# advised to use the exact date string received in a previous Last-
# Modified header field whenever possible.
# >> (among many other wise things)
# Example value: "Sun, 04 Apr 2004 11:58:04 GMT"
my($self, $url) = @_;
DEBUG and print "Getting lastmod value for $url ...\n";
return $self->get_datum($url, 'lastmodified');
}
sub feed_get_next_update { # this is an epochtime
my($self, $url) = @_;
DEBUG and print "Getting next-update value for $url ...\n";
return $self->get_datum($url, 'nextupdate');
}
sub feed_get_etag { # this is a string
# See RFC 2616, "3.11 Entity Tags"
my($self, $url) = @_;
DEBUG and print "Getting etag value for $url ...\n";
return $self->get_datum($url, 'etag');
}
sub feed_set_last_modified {
my($self, $url, $last_modified_time) = @_;
DEBUG and print "Setting lastmod for $url to $last_modified_time\n";
$self->set_datum($url, 'lastmodified', $last_modified_time);
}
sub feed_set_next_update {
my($self, $url, $next_update_time) = @_;
DEBUG and print "Setting next-update for $url to $next_update_time\n";
$self->set_datum($url, 'nextupdate', $next_update_time);
}
sub feed_set_etag {
my($self, $url, $etag) = @_;
DEBUG and print "Setting etag for $url to $etag\n";
$self->set_datum($url, 'etag', $etag);
}
#--------------------------------------------------------------------------
sub new {
my $self = shift->SUPER::new(@_);
$self->_rssagent_init();
return $self;
}
sub _rssagent_init {
my $self = shift;
$self->agent("XmlRssTimingBot/$VERSION (" . $self->agent . ")" );
$self->rss_semaphore_file(1) unless $^O =~ m/(?:Mac|MSWin)/;
# Whatever needs doing here
return;
}
#==========================================================================
sub commit { # save all our new data for these various feeds we've been seeing
my $self = shift;
return $self->_stupid_commit;
}
sub datum_from_db {
my($self, $url, $varname) = @_;
return $self->_stupid_datum_from_db($url, $varname);
}
#==========================================================================
sub rssagent_just_before_real_request { # Override if you like
#my($self, $args_ref) = @_;
}
sub rssagent_just_after_real_request { # Override if you like
#my($self, $response, $args_ref) = @_;
}
#==========================================================================
sub simple_request {
my($self, @args) = @_;
LWP::Debug::trace('simple_request()');
LWP::Debug::debug("Trying simple_request with args: ["
. join(',', map $_||"''", @args) . "]");
DEBUG and print( "Trying simple_request with args: ["
. join(',', map $_||"''", @args) . "]");
my $resp;
my $maybe_response = $self->_rssagent_maybe_null_response($args[0]);
if( $maybe_response ) {
LWP::Debug::debug("Returning cached response");
return $maybe_response;
}
$self-> _rssagent_add_header_conditions(@args);
$self-> rssagent_just_before_real_request(\@args);
$resp = $self->SUPER::simple_request(@args);
$self-> rssagent_just_after_real_request($resp, \@args);
$self-> _rssagent_response_consider($resp);
LWP::Debug::debug("Returning uncached response");
return $resp;
}
#==========================================================================
sub _rssagent_maybe_null_response {
# Return a (virtual) response object if you want to block this request.
# Otherwise, to allow this request to actually happen, return a false value.
my($self, $req) = @_;
LWP::Debug::trace('_rssagent_maybe_null_response()');
my $url = $req->uri;
my $not_until = $self->feed_get_next_update($url);
unless(defined $not_until) {
LWP::Debug::debug( "No restrictions on when to get $url");
DEBUG > 1 and print "No restrictions on when to get $url\n";
return undef;
}
my $now = $self->now;
if($not_until >= $now) {
LWP::Debug::debug( "It's now $now, but I shouldn't look at $url until $not_until.");
DEBUG > 1 and print "It's now $now, but I shouldn't look at $url until $not_until.\n";
return $self->_rss_agent_null_response($req, $not_until);
}
# Else give the all-clear
return undef;
}
#==========================================================================
sub _rssagent_response_consider {
my($self, $response) = @_;
# Possibly extract the RSS-timing content from this response.
LWP::Debug::trace('_maybe_null_response()');
my $code = $response->code;
unless($code eq '200') { # or $code eq '304' ?
LWP::Debug::debug('Not trying to find RSS content in this $code response');
return;
}
my $url = $self->_url_from_response( $response );
return unless $self->_looks_like_rss( $url, $response );
my $now = $self->now();
my $time_string_from_resp = $self->_time_string_from_resp($response, $now);
$self->feed_set_last_modified($url, $time_string_from_resp)
if defined $time_string_from_resp;
my $etag = $self->_etag_from_resp($response);
$self->feed_set_etag($url, $etag) if defined $etag;
$self->_ponder_next_update($url, $now, $response) unless $code eq '304';
return;
}
#==========================================================================
sub _looks_like_rss {
my($self, $url, $response) = @_;
LWP::Debug::trace('_ponder_next_update');
my $content;
# Look for rss/rdf in the first 2000 bytes
# TODO: support Atom here? Does anyone ever use sy:* stuff in Atom?
my $c = $response->content_ref;
unless( $c and $$c ) {
LWP::Debug::debug("Content from $url is apparently null.");
print "NULL!\n";
# so it's sure not RSS!
return 0;
}
if ( $$c =~ m{^[^\x00]{0,2000}?(?:<rss|<rdf)}s ) {
LWP::Debug::debug("Content from $url looks like RSS/RDF.");
return 1;
}
LWP::Debug::debug("Content from $url doesn't look like RSS/RDF.");
return 0;
}
#==========================================================================
sub _ponder_next_update {
my($self, $url, $now, $response) = @_;
LWP::Debug::trace('_ponder_next_update');
my $content = $response->content;
$content =~ s/<!--.*?-->//sg; # kill XML comments
unless( $content =~ m{^[^\x00]{0,2000}?<rss|<rdf}s ) {
# Make super-sure that the our apparent start-tag wasn't just in a comment!
LWP::Debug::debug("Content from $url doesn't look like RSS/RDF.");
return;
}
my $timing = $self->{"_rss_timing_obj"} || XML::RSS::Timing->new;
$timing->use_exceptions(0);
$timing->minAge( $self->minAge ) if defined $self->minAge;
$timing->maxAge( $self->maxAge ) if defined $self->maxAge;
# Note that we use our server-time, not the other server's time
$timing->last_polled($now);
$self->_scan_xml_timing(\$content, $timing);
if( $timing->complaints ) {
LWP::Debug::debug("Errors in this feed's timing fields:\n"
. map("* $_\n", $timing->complaints)
. "]... so ignoring it all.\n"
);
return;
}
# Now actually learn...
my $next_update = $timing->nextUpdate();
LWP::Debug::debug("Remembering not to poll $url until $next_update");
$self->feed_set_next_update( $url, $next_update );
# Now, we /could/ also slip this into the response as a faked-out
# "Expires" header value, except that 1) who the hell ever looks at
# those, and 2) "Expires" is the expiration time expressed against
# the REMOTE server's clock, whereas nextUpdate is expressed
# against OUR clock. So mixing these up would screw up all kinds of
# things in the unhappy event of clock skew combined with someone
# actually looking at a fake-o Expires value.
return;
}
#==========================================================================
sub _scan_xml_timing {
my($self, $contentref, $timingobj) = @_;
return unless $contentref and $$contentref;
DEBUG > 1 and print "# _scan_xml_timing << self <$self>; contentref <$contentref>; timingobj <$timingobj>\n";
$self->_scan_for_ttl( $contentref, $timingobj );
$self->_scan_for_skipDays( $contentref, $timingobj );
$self->_scan_for_skipHours( $contentref, $timingobj );
$self->_scan_for_updatePeriod( $contentref, $timingobj );
$self->_scan_for_updateFrequency( $contentref, $timingobj );
$self->_scan_for_updateBase( $contentref, $timingobj );
return;
}
sub _scan_for_updateFrequency {my($s,$c,$t)=@_;$s->_scan_xml('updateFrequency', $c, $t) }
sub _scan_for_updatePeriod {my($s,$c,$t)=@_;$s->_scan_xml('updatePeriod', $c, $t) }
sub _scan_for_updateBase {my($s,$c,$t)=@_;$s->_scan_xml('updateBase', $c, $t) }
sub _scan_for_ttl {my($s,$c,$t)=@_;$s->_scan_xml('ttl', $c, $t) }
sub _scan_for_skipDays {my($s,$c,$t)=@_;$s->_scan_xml('skipDays' , $c, $t, 'day' ) }
sub _scan_for_skipHours {my($s,$c,$t)=@_;$s->_scan_xml('skipHours', $c, $t, 'hour') }
#==========================================================================
sub _etag_from_resp {
my($self, $response) = @_;
my $etag = $response->header('ETag');
return undef unless defined $etag
and length($etag)
and length($etag) < 251 # A good sanity limit, I think
and $etag !~ m/[\n\r]/ # Enforce this minimal sanity on content
;
DEBUG and print "Using etag $etag for resp-obj $response\'s etag\n";
return $etag;
}
sub _time_string_from_resp {
my($self, $response, $now) = @_;
require HTTP::Date;
foreach my $time_string (
$response->header('Last-Modified'),
$response->header('Date'),
HTTP::Date::time2str( $now ),
) {
next unless # enforce minimal sanity on the value...
defined $time_string
and $time_string =~ m/^[- \,\.\:a-zA-Z0-9]{4,40}$/s
and $time_string =~ m/[0-9A-Za-z]/;
DEBUG and print "Using time-string \"$time_string\" for resp-obj $response\'s lastmod\n";
return $time_string;
}
return undef;
}
#==========================================================================
sub _url_from_response {
my($self, $response) = @_;
my $this_res = $response;
for(1 .. 30) { # get the original request's URL
$this_res = ($this_res->previous || last);
}
return $this_res->request->uri;
}
#==========================================================================
sub _rssagent_add_header_conditions {
my $self = shift;
$self->_rssagent_add_ifmod_header(@_);
$self->_rssagent_add_ifnonematch_header(@_);
return;
}
sub _rssagent_add_ifmod_header {
my($self, $req) = @_;
LWP::Debug::trace('_rssagent_add_ifmod_header()');
my $url = $req->uri;
my $lastmod = $self->feed_get_last_modified( $url );
if(defined $lastmod and length $lastmod) {
LWP::Debug::debug("Setting If-Modified-Since on get-$url to $lastmod");
DEBUG and print "Setting If-Modified-Since on get-$url to $lastmod\n";
$req->header('If-Modified-Since' => $lastmod);
} else {
LWP::Debug::debug("I see no last-polled time for $url");
DEBUG and print "I see no last-polled time for $url\n";
}
return;
}
sub _rssagent_add_ifnonematch_header {
my($self, $req) = @_;
LWP::Debug::trace('_rssagent_add_ifnonematch_header()');
my $url = $req->uri;
my $etag = $self->feed_get_etag( $url );
if(defined $etag and length $etag) {
LWP::Debug::debug("Setting If-None-Match on get-$url to $etag");
DEBUG and print "Setting If-None-Match on get-$url to $etag\n";
$req->header('If-None-Match' => $etag);
} else {
LWP::Debug::debug("I see no etag for $url");
DEBUG and print "I see no etag for $url\n";
}
return;
}
#==========================================================================
sub _rss_agent_null_response {
my($self, $request, $not_until) = @_;
require HTTP::Response;
require HTTP::Date;
require HTTP::Status;
my $now_str = HTTP::Date::time2str( $self->now );
my $not_until_str = HTTP::Date::time2str( $not_until - 1);
# The -1 is because "Expires" means the last moment when it's still
# good, and not_until is the first moment when we can check. Q.E.D.
my $response = HTTP::Response->new(
HTTP::Status::RC_NOT_MODIFIED() => "Not Modified (" . __PACKAGE__
. " says it won't change until after $not_until_str)"
);
my $h = $response->headers;
$h->header( "Client-Date" => $now_str);
$h->header( "Date" => $now_str);
$h->header( "Expires" => $not_until_str);
$response->request($request);
return $response;
}
#==========================================================================
sub now {
# This is here just so we can change what we mean by 'now', when we're
# running tests. Trust me, it's handy.
return $_[0]->{'_now_hack'} if ref $_[0] and defined $_[0]->{'_now_hack'};
return time();
}
###########################################################################
#
# get_datum and set_datum implement the caching that both speeds things up
# and allows a layer of indirection so that changes don't happen until
# we commit
sub get_datum {
my($self, $url, $varname) = @_;
$url =~ s{\s+}{}g;
return unless length $url;
# First look in our dirty cache
my $for_db = ($self->{'rsstimingbot_for_db'} ||= {});
if( $for_db->{$url} and exists $for_db->{$url}{$varname} ) {
DEBUG > 6 and print " Found $varname for $url in dirty cache\n";
return $for_db->{$url}{$varname};
}
# then look in our has-been-read-from-disk cache
my $from_db = ($self->{'rsstimingbot_from_db'} ||= {});
if( $from_db->{$url} and exists $from_db->{$url}{$varname} ) {
DEBUG > 6 and print " Found $varname for $url in clean cache\n";
return $from_db->{$url}{$varname};
}
# and finally, as a last resort, actually fetch from the real DB
return(
$from_db->{$url}{$varname} = $self->datum_from_db($url, $varname)
);
}
sub set_datum {
my($self, $url, $varname, $value) = @_;
$url =~ s{\s+}{}g;
return unless length $url;
$self->{'rsstimingbot_from_db' }{$url}{$varname}
= $self->{'rsstimingbot_for_db'}{$url}{$varname}
= $value;
# And upon commit, we'll save all of 'rsstimingbot_for_db' to database
}
###########################################################################
#
# Our lame default storage methods, in case you didn't override
# commit and datum_from_db
sub _stupid_datum_from_db {
my($self, $url, $varname) = @_;
my $dbfile = $self->_stupid___url2dbfile($url);
DEBUG > 1 and print " DB: Getting datum $varname for $url ...\n";
return undef unless -e $dbfile;
my $unlocker = $self->_stupid_lock();
open( STUPID_DB, $dbfile)
or die "Can't read-open $dbfile for $url : $!\nAborting";
my @f;
local $/ = "\n";
my $from_db = ($self->{'rsstimingbot_from_db'} ||= {});
DEBUG > 8 and print " Reading DB file $dbfile...\n";
while(<STUPID_DB>) {
chomp;
@f = split ' ', $_, 3;
# Yup, just three space-separated fields: "url varname value"
if( @f >= 2 ) {
DEBUG > 9 and print " Datum read {$f[0]} {$f[1]} {$f[2]}\n";
$from_db->{ $f[0] }->{ $f[1] } =
defined($f[2]) ? $f[2] : ""; # because of split's behavior
}
}
close(STUPID_DB);
$unlocker and $unlocker->();
DEBUG > 8 and print " Done reading DB file $dbfile\n";
return $from_db->{$url}{$varname}
if $from_db->{$url} and exists $from_db->{$url}{$varname};
return undef;
}
# See XML::RSS::TimingBotDBI for a better example of a commit method
sub _stupid_commit { # write all our dirty cache to DB files
my $self = shift;
my $for_db = $self->{'rsstimingbot_for_db'} || return;
DEBUG > 1 and print " I see ", scalar(keys %$for_db), " url-data in $self to save\n";
return unless %$for_db;
my %path2mods;
foreach my $url (sort keys %$for_db) {
my $dbfile = $self->_stupid___url2dbfile($url);
foreach my $varname (sort keys %{ $for_db->{$url} }) {
my $value = $for_db->{$url}{$varname};
$path2mods{$dbfile}{ "$url $varname" } = defined($value) ? $value : '';
}
}
DEBUG > 7 and print " Committing to ", scalar(keys %path2mods),
" database files...\n";
my $unlocker = $self->_stupid_lock();
foreach my $dbfile (keys %path2mods) {
$self->_stupid___mod_db( $dbfile, $path2mods{$dbfile} );
$path2mods{$dbfile} = undef; # potentially free up some memory
}
DEBUG > 7 and print " Done committing all database files\n";
$unlocker and $unlocker->();
%$for_db = ();
return;
}
#
# And now some very internal stuff:
#
sub _stupid_lock {
my $self = shift;
my $file = $self->rss_semaphore_file;
return unless $file;
$file = $self->_stupid___url2dbfile("http://lock.nowhere.int/lock")
if $file eq '1';
DEBUG > 2 and
print "About to request (maybe wait for!) an exclusive lock on $file\n";
return $self->_getsem($file);
}
sub _stupid___my_db_path {
my $self = shift;
return $self->{'_dbpath'}
|| $ENV{'TIMINGBOTPATH'}
|| $ENV{'APPDATA'}
|| $ENV{'HOME'}
|| do { require File::Spec; File::Spec->curdir; }
}
sub _stupid___url2dbfile {
my($self, $url) = @_;
require File::Spec;
DEBUG > 2 and print " Pondering filespec for url $url\n";
my $url_stem = lc $url;
$url_stem =~ s{^http://(?:www\.)?}{}s;
$url_stem =~ s{\.(?:xml|rss|rdf)$}{}s;
$url_stem =~ s/\W+//sg; # includes killing all whitespace
$url_stem = 'misc' unless length $url_stem;
$url_stem = substr($url_stem,0,30) if length($url) > 30; # truncate
my $rssdir = File::Spec->catfile( $self->_stupid___my_db_path, 'rssdata');
if( -e $rssdir ) {
DEBUG > 12 and print " RSSdir $rssdir exists already.\n";
} else {
if( mkdir($rssdir, 0777) ) {
DEBUG > 1 and print " Successfully created RSSdir $rssdir\n";
} else {
die "Can't mkdir $rssdir: $!\nAborting";
}
}
my $path = File::Spec->catfile( $rssdir, $url_stem );
DEBUG > 6 and print "# Path to stupid DB: $path\n";
return $path;
}
# . . . . . . . . . . . . . . . . . . . . . . . . .
sub _stupid___mod_db {
my($self, $dbfile, $to_write) = @_;
if( -e $dbfile and -s _ ) {
DEBUG > 9 and print "Reading db $dbfile ...\n";
open( STUPID_DB, $dbfile)
or die "Can't read-open $dbfile : $!\nAborting";
my @f;
local $/ = "\n";
while(<STUPID_DB>) {
chomp;
@f = split ' ', $_, 3; # yup, just three space-separated fields
$to_write->{ "$f[0] $f[1]" } = defined($f[2]) ? $f[2] : ""
if @f >= 2 and ! exists $to_write->{ "$f[0] $f[1]" };
}
close(STUPID_DB);
} else {
DEBUG > 9 and print "No db $dbfile to read, so just writing new.\n";
}
DEBUG > 8 and print " Saving DB file $dbfile (", scalar(keys %$to_write), " entries)\n";
open( STUPID_DB, ">$dbfile" ) or die "Can't write-open $dbfile: $!\nAborting";
my $value;
foreach my $key (sort keys %$to_write) {
next unless defined( $value = $to_write->{$key} );
$value =~ tr/\n\r//d; # Enforce sanity
print STUPID_DB "$key $value\n";
}
close(STUPID_DB);
DEBUG > 8 and print " Done saving DB file $dbfile\n";
return;
}
############################################################################
sub _getsem {
my($self, $file, $be_nonblocking) = @_;
# Lock this semaphore file. Returns the unlocker sub!!
#
# To have the lock be non-blocking, specify a true second parameter.
# In that case, returns false if can't get the lock. Unlocker
# sub otherwise.
unless(defined $file and length $file) {
require Carp;
Carp::confess("Filename argument to _getsem must be contentful!")
}
open(my $fh, ">$file") or Carp::croak("Can't write-open $file\: $!");
#chmod 0666, $file; # yes, make it world-writeable. or at least try.
if($be_nonblocking) { # non-blocking!
eval { flock($fh, 2 | 4) } # Exclusive + NONblocking
or return; # couldn't get a lock.
} else { # normal case: Exclusive, Blocking
eval { flock($fh, 2) } # Exclusive
or do { require Carp; Carp::confess("Can't exclusive-block lock $file: $!") };
# should never just fail -- should queue up forever
}
unless( print $fh "I am a lowly " , __PACKAGE__ , " semaphore file\cm\cj" ) {
require Carp;
Carp::confess("Can't write to $file\: $!");
}
return(
sub {
if($fh) { # So we can call multiple times
DEBUG > 1 and print "Releasing lock on $file\n";
close($fh); # Presumably will never fail!
# Will release the lock.
undef $fh;
return 1;
} else {
return '';
}
}
);
# Now, I /could/ just have this work by returning the globref --
# then when all the references to the glob go to 0, the FH
# closes, and the lock is released. However, this /relies/ on
# the timing of garbage collection.
}
###########################################################################
#
# XML HELL BEGINS HERE
#
sub _scan_xml {
my($self, $tag, $c, $timing, $subtag) = @_;
die "Crazy tag \"$tag\"!!"
unless $tag =~ m/^[a-zA-Z_][a-zA-Z_0-9]*$/s; # sanity
die "Contentref has to be a scalar ref" unless $c and ref($c) eq 'SCALAR';
die "Timing object has to be an object!" unless $timing and ref($timing)
and ref($timing) ne 'SCALAR';
my $method = $tag;
DEBUG > 5 and print "# _scan_xml << self <$self>; timingobj <$timing>\n# tag <$tag>; subtag ",
defined($subtag) ? "<$subtag>" : "(nil)", "\n",
"# Content {\n$$c\n# }\n",
;
unless(defined $subtag) { # common case: just <tag>someval</tag>
if( $$c =~
m{
<
(?: [a-zA-Z_][-_\.a-zA-Z0-9]* \: )? # optional namespace
$tag\b
.*? # optional attributes and whitespace and junk
>
\s*
([^<>\s"]+)
\s*
</
(?: [a-zA-Z_][-_\.a-zA-Z0-9]* \: )? # optional namespace
$tag
\s* # just the optional (and rare) whitespace
>
}sx
) {
my $it = $1;
LWP::Debug::debug("Content has $method value: \"$it\"");
DEBUG > 2 and print(" Content has $method value: \"$it\"!!\n");
$timing->$method( $it );
} else {
LWP::Debug::debug("Content has no $method value");
DEBUG > 2 and print(" Content has no $method value\n");
}
return;
}
# Else it's a tag and subtaggy thing
die "Crazy subtag \"$tag\"!!"
unless $subtag =~ m/^[a-zA-Z_][a-zA-Z_0-9]*$/s; # sanity
if( $$c =~
m{
<
(?: [a-zA-Z_][-_\.a-zA-Z0-9]* \: )? # optional namespace
$tag
\b.*?>
\s*
(
(?:
<
(?: [a-zA-Z_][-_\.a-zA-Z0-9]* \: )? # optional namespace
$subtag
\b.*?>
\s*
[^<>\s"]+
\s*
</
(?: [a-zA-Z_][-_\.a-zA-Z0-9]* \: )? # optional namespace
$subtag
\s*>
\s*
)+
)
</
(?: [a-zA-Z_][-_\.a-zA-Z0-9]* \: )? # optional namespace
$tag
\s*>
}sx
) {
my $there = $1;
DEBUG > 3 and print " $method+subtag valuecluster \"$there\"\n";
my(@them) = ( $there =~
# Our previous RE made sure that this is a very simply-structured
# area, so we can get away with just this simple regexp:
m{
>
\s*
([^<>\s"]+)
\s*
</
}xsg
);
LWP::Debug::debug("Content $method+$subtag values: @them");
DEBUG > 2 and print(" Content $method+$subtag values: (",
join(q<,>, map("\"$_\" ", @them)), ") !!\n");
$timing->$method( @them ); # yes, we call the method, not a submethod
} else {
LWP::Debug::debug("Content has no $method+$subtag values");
DEBUG > 2 and print(" Content has no $method+$subtag values\n");
}
# Qvia sicvt exaltantvr Caeli a Terra,
# sic exaltatae svnt viae Meae a viis vestris,
# et cogitationes Meae a cogitationibvs vestris!
# VGABVGA!
return;
}
# EndHell.
###########################################################################
1;
__END__
<skipHours><hour>0</hour><hour>2</hour><hour>4</hour><hour>6</hour>
<hour>8</hour><hour>10</hour><hour>12</hour><hour>14</hour>
<hour>16</hour><hour>18</hour><hour>20</hour><hour>22</hour></skipHours>
<sy:updateFrequency>12</sy:updateFrequency>
<sy:updatePeriod>daily</sy:updatePeriod>
<sy:updateBase>1970-01-01T01:30+00:00</sy:updateBase>
<ttl>120</ttl>
http://interglacial.com/rss/find_of_the_week.rss