A few days ago, Ben Hammersley threw out an idea for weather podcasting. I had made a previous contribution to some of his work and, as Ben apparently knows, flattery will get you everywhere with an engineer. So I took up his latest challenge. I was able to get the bulk of the script done in just a few hours one evening. There is still some work that could be done to improve it, but it's stable enough that I wanted to go ahead and share it now.
How To Use
The URL accepts 3 parameters, a locid, a dayf and a unit. The locid is a weather.com location identifier, such as a zip code or city code (e.g., 92126 for San Diego, CA, or ITXX0067 for Rome, Italy). The dayf parameter indicates how many days of weather forecast you wish to receive (one RSS entry for each day, up to a maximum of 10, defaults to 2). The unit parameter is optional and can be either m (metric) or s (standard), and defaults to standard.
Anyway, if you want to give it a spin, the first thing you'll need to do is go to weather.com and get an id for the location in which you are interested. This id can be a zip code (if you're in the states) or some other kind of code which you can extract from the URL. Look up the weather for a location and you can get the location id from the URL. For example, if you look up weather in Rome, Italy, the URL will look like this:
http://www.weather.com/outlook/travel/local/ITXX0067?from=search_city
In this example, ITXX0067 is the location id. Using that, here is a sample URL you could use for my weather podcast:
http://www.jorgev.com/cgi-bin/weather.cgi?locid=ITXX0067&dayf=5&unit=m
This will create a podcast for a 5-day weather forecast for Rome, Italy in metric units. Or...
http://www.jorgev.com/cgi-bin/weather.cgi?locid=92126
This will create a podcast for a 2-day weather forecast for San Diego, CA, reported in standard units.
Note that the podcast currently creates (dayf + 1) RSS items...one with current conditions and the remainder for the number of forecast days requested.
Keep in mind this is still a work in progress. Maybe I should just call it a perpetual beta, like GMail or ICQ. Give it a try, feel free to leave comments or suggestions.
A brief explanation of how the script works, it's actually quite simple: I first call into weather.com's XML Data Feed. Then I use XPath to extract the data in which I am interested. I format this information into a text file, which I then pass onto the text2wave utility which performs the text-to-speech conversion. Finally, since wav files are so huge, I convert it to MP3 using the LAME encoder. Piece of cake, eh?
Here's the source code to my script (some items removed, such as key info):
#!/usr/bin/perl -w
# 2004-12-13 Created, Jorge Velázquez
# 2004-12-22 added dayf parameter
# 2004-12-28 replaced some text with phrases more suitable for text-to-speech conversion
# 2005-03-29 added contact info to the feed, fixed date to conform to RFC822
use strict;
use CGI qw(:standard);
use XML::RSS;
use XML::XPath;
use LWP::Simple;
use File::Temp;
use File::Basename;
use POSIX qw(strftime);
# partner and key information
my $par = 'partnerid';
my $key = 'key';
# get parameters
my $cgi = CGI::new();
my $locid = $cgi->param('locid');
my $unit = $cgi->param('unit');
if (not $unit) {
$unit = 's';
}
my $dayf = $cgi->param('dayf');
if (not $dayf) {
$dayf = 2;
}
if ($dayf > 10) {
$dayf = 10;
}
# wind direction hash
my %direction = (
'N' => 'north',
'NNE' => 'north northeast',
'NE' => 'northeast',
'ENE' => 'east northeast',
'E' => 'east',
'ESE' => 'east southeast',
'SE' => 'southeast',
'SSE' => 'south southeast',
'S' => 'south',
'SSW' => 'south southwest',
'SW' => 'southwest',
'WSW' => 'west southwest',
'W' => 'west',
'WNW' => 'west northwest',
'NW' => 'northwest',
'NNW' => 'north northwest'
);
# query weather info, current conditions with 2 day forecast
my $xml = get("http://xoap.weather.com/weather/local/$locid?cc=*&dayf=$dayf&prod=xoap&par=$par&key=$key&unit=$unit");
#load it into XPath object
my $xp = XML::XPath->new($xml);
# extract unit information from feed
my $ut = $xp->findvalue('/weather/head/ut');
my $ud = $xp->findvalue('/weather/head/ud');
my $us = $xp->findvalue('/weather/head/us');
# format current date/time according to RFC822
my $pubDate = strftime("%a, %d %b %Y %T PST", localtime());
# create rss 2.0 feed
my $rss = new XML::RSS(version => '2.0');
# channel information
$rss->channel(title => 'Weather Podcast',
link => 'http://www.weather.com/',
description => 'Local weather podcasting feed',
pubDate => $pubDate,
language => 'en-us',
webMaster => 'jorgev@jorgev.com (Jorge Velazquez)',
ttl => '720');
# get current weather conditions
my $dnam = $xp->findvalue('/weather/loc/dnam');
my $ccobst = $xp->findvalue('/weather/cc/obst');
my $cclsup = $xp->findvalue('/weather/cc/lsup');
my $cctemp = $xp->findvalue('/weather/cc/tmp');
my $ccwinds = $xp->findvalue('/weather/cc/wind/s');
my $ccwindt = $xp->findvalue('/weather/cc/wind/t');
my $cct = $xp->findvalue('/weather/cc/t');
my $ccbarr = $xp->findvalue('/weather/cc/bar/r');
my $ccbard = $xp->findvalue('/weather/cc/bar/d');
my $cchmid = $xp->findvalue('/weather/cc/hmid');
# convert the units to words
my $utword = $ut eq 'F' ? 'fahrenheit' : 'celsius';
my $udword = $ud eq 'km' ? 'kilometers' : 'miles';
my $usword = $us eq 'mph' ? 'miles per hour' : 'kilometers per hour';
# build the text file for converting to speech
my $text = "Current conditions for $dnam.";
my $desc = $text;
if ($cct ne 'N/A') {
$text .= " $cct.";
}
if ($cctemp ne 'N/A') {
$text .= " The temperature is $cctemp $utword.";
}
if ($ccwinds ne 'N/A') {
$text .= " The wind is from the $direction{$ccwindt} at $ccwinds $usword.";
}
if ($cchmid ne 'N/A') {
$text .= " The humidity is $cchmid percent.";
}
# write the file and get the stats
my $fileinfo = writemp3($text);
# add rss item for current weather
$rss->add_item(title => "Current weather conditions for $ccobst",
link => "http://www.weather.com/weather/local/$locid",
description => $desc,
pubDate => $pubDate,
enclosure => {
url => $fileinfo->{'url'},
length => $fileinfo->{'length'},
type => 'audio/mpeg'
});
# get forecast timestamp
my $lsup = $xp->findvalue('/weather/dayf/lsup');
# iterate through forecast days
my $nodeset = $xp->find('/weather/dayf/day');
foreach my $node ($nodeset->get_nodelist) {
# get the items of interest to our script
my $t = $node->findvalue('@t');
my $dt = $node->findvalue('@dt');
my $hi = $node->findvalue('hi');
my $low = $node->findvalue('low');
my $d = $node->findvalue('part[@p="d"]/t');
my $n = $node->findvalue('part[@p="n"]/t');
my $dwinds = $node->findvalue('part[@p="d"]/wind/s');
my $dwindt = $node->findvalue('part[@p="d"]/wind/t');
my $nwinds = $node->findvalue('part[@p="n"]/wind/s');
my $nwindt = $node->findvalue('part[@p="n"]/wind/t');
my $dhmid = $node->findvalue('part[@p="d"]/hmid');
my $nhmid = $node->findvalue('part[@p="n"]/hmid');
# build the text file for converting to speech
$text = "Weather forecast for $t, $dt in $dnam.";
$desc = $text;
if ($hi ne 'N/A') {
$text .= " The high is $hi $utword.";
}
if ($low ne 'N/A') {
$text .= " The low is $low $utword.";
}
$text .= " Daytime";
if ($d ne 'N/A') {
$d =~ s/T-Showers/thundershowers/i;
$text .= ", $d";
} else {
$text .= " conditions unavailable";
}
if ($dwinds ne 'N/A') {
$text .= ", wind from the $direction{$dwindt} at $dwinds $usword";
}
if ($dhmid ne 'N/A') {
$text .= ", humidity $dhmid percent";
}
$text .= ".";
$text .= " Nighttime";
if ($n ne 'N/A') {
$n =~ s/T-Showers/thundershowers/i;
$text .= ", $n";
} else {
$text .= " conditions unavailable";
}
if ($nwinds ne 'N/A') {
$text .= ", wind from the $direction{$nwindt} at $nwinds $usword";
}
if ($nhmid ne 'N/A') {
$text .= ", humidity $nhmid percent";
}
$text .= ".";
# write the file and get the stats
$fileinfo = writemp3($text);
# add this forecast
$rss->add_item(title => "Weather forecast for $t, $dt at $dnam",
link => "http://www.weather.com/weather/local/$locid",
description => $desc,
pubDate => $pubDate,
enclosure => {
url => $fileinfo->{'url'},
length => $fileinfo->{'length'},
type => 'audio/mpeg'
});
}
print header('application/rss+xml');
print $rss->as_string;
sub writemp3 {
# write the text file
my $fhtxt = new File::Temp(suffix => '.txt');
print $fhtxt @_;
close $fhtxt;
# write the wav file
my $fhwav = new File::Temp(suffix => '.wav');
system "text2wave", "-o", $fhwav->filename, $fhtxt->filename;
# convert it to mp3
my $fhmp3 = new File::Temp(unlink => 0, dir => '../mp3', suffix => '.mp3');
system "lame", "-h", $fhwav->filename, $fhmp3->filename;
# make it world-readable
chmod 0644, $fhmp3->filename;
# caller needs this information
my $basename = basename $fhmp3->filename;
my %fi = ('url' => "http://www.jorgev.com/mp3/$basename", 'length' => (stat $fhmp3->filename)[7]);
return \%fi;
}