package AmphetaDesk::AmphetaDesk::MyChannels; ############################################################################### # AmphetaDesk::AmphetaDesk (c) 2000-2002 Disobey # # morbus@disobey.com http://www.disobey.com/amphetadesk/ # ############################################################################### # ABOUT THIS PACKAGE: # # This package is important - it carries all of the various functions that # # are used to create and interact with the user's subscription list. By # # default, it comes built in with a number of standard entries, but will # # create a user specific file when the time comes (much like Settings.pm). # # # # LIST OF ROUTINES BELOW: # # add_url - adds a channel to myChannels.opml from passed url. # # check_for_duplicate_filename - checks for sub'd filenames existence. # # check_for_duplicate_url - checks for the existance of a sub'd url. # # count_my_channels - simply spits out the number of sub'd channels. # # create_channel_filename - creates a filename based off channel title. # # del_url - delete a single channel from the subscribed list. # # download_my_channels - read our myChannels.opml and get all channels. # # get_my_channels_data - get the value of the passed data from channel url. # # get_my_sorted_channels - get arrays of various sorted channel options. # # import_my_channels - imports the OPML data into the current list. # # load_my_channels - reads in the myChannels.opml and returns the data. # # remove_old_channel_files - deletes old and dejected files. # # save_my_channels - saves the relevant channel subscriptions to a file. # # set_my_channels_data - set the value of the passed data for channel url. # # update_my_channel_data - takes a URL and new data and updates our OPML. # ############################################################################### # "Get your hands off my damn remote, woman! Make me some pie! PIEEe!" # ############################################################################### use strict; $|++; use AmphetaDesk::AmphetaDesk::Settings; use AmphetaDesk::AmphetaDesk::Utilities; use AmphetaDesk::AmphetaDesk::WWW; use HTTP::Date qw/time2iso str2time/; use Time::Local; use File::Spec::Functions; use AmphetaDesk::XML::Simple; require Exporter; use vars qw( @ISA @EXPORT ); @ISA = qw( Exporter ); @EXPORT = qw( add_url count_my_channels del_url download_my_channels get_my_channels_data get_my_sorted_channels import_my_channels load_my_channels remove_old_channel_files save_my_channels set_my_channels_data update_my_channel_data ); # where we store our in # memory myChannels.opml. my %CHANNELS; ############################################################################### # add_url - adds a channel to myChannels.opml from passed url. # ############################################################################### # USAGE: # # add_url( $url ); # # # # NOTES: # # This routine downloads the passed $url and creates an entry for # # myChannels.opml based on the data it sees within. After saving the file, # # it saves the myChannels.opml list. # # # # RETURNS: # # 1; there's no reason why this routine should fail. # ############################################################################### sub add_url { my ($incoming_urls) = @_; return unless $incoming_urls; # first thing we do is split the URL on any newlines # or on ",http://". This allows us to accept mass # POSTS of newlined URLs, as well as comma spliced lists. my @urls = split(/\n|,http:\/\//, $incoming_urls); # now, loop through each one. foreach my $url (@urls) { # add the http:// back if missing. $url =~ s/^/http:\/\// unless $url =~ /^http:\/\//; # just some happy blurbage for the log file. note("Attempting to add a new channel from $url."); # does the url already exists in our subscriptions? if ( check_for_duplicate_url($url) ) { note("AmphetaDesk has determined that you are already " . "subscribed to '$CHANNELS{$url}{title}'.", 1, 1); next; # ya boOog! subscribing to the subscribed. sheesh! } # download the channel data. my $channel_raw_data = get($url); unless ($channel_raw_data) { note("AmphetaDesk could not connect to $url.", 1, 1); next; } # if this looks like a OPML file, load it in, and import all # the various channels within (Radio/AmphetaDesk compatible). # we check our return code for success (the import will fail # if "xmlurl" attributes don't exist in the OPML. if ($channel_raw_data =~ /{channel}->{title} ) ) { $htmlurl = $channel_data->{channel}->{"link"}; $title = $channel_data->{channel}->{title}; $description = $channel_data->{channel}->{description} if defined( $channel_data->{channel}->{description} ); } # else, maybe it's scriptingNews... elsif ( defined ( $channel_data->{header}->{channelTitle} ) ) { $htmlurl = $channel_data->{header}->{channelLink}; $title = $channel_data->{header}->{channelTitle}; $description = $channel_data->{header}->{channelDescription} if defined( $channel_data->{header}->{channelDescription} ); } # else... else { # we have no clue, so spit an error and return. note("AmphetaDesk could not determine the format of $url.", 1, 1); next; } # sanitize our data before processing. $title = strip_newlines_and_tabs($title); $description = strip_newlines_and_tabs($description); $title = $title || $url; $description = $description || ""; # now create a filename based off the title. my $filename = create_channel_filename( $title ); my $filepath = catfile( get_setting("dir_data_channels"), $filename ); # and save the channel data, so we don't have to redownload it. open(NEW_CHANNEL, ">$filepath") or note("AmphetaDesk could not save the new channel data. " . "Please report the following error to " . get_setting("app_email") . ": $!", 1, 1); print NEW_CHANNEL $channel_raw_data; close(NEW_CHANNEL); # now, add the new channel data to our %CHANNELS. $CHANNELS{$url}{date_added} = time2iso(time); $CHANNELS{$url}{date_downloaded} = $CHANNELS{$url}{date_added}; $CHANNELS{$url}{description} = $description; $CHANNELS{$url}{title} = $title; $CHANNELS{$url}{filename} = $filename; $CHANNELS{$url}{htmlurl} = $htmlurl; $CHANNELS{$url}{xmlurl} = $url; # and save subscriptions to disk. save_my_channels(); # and say hello to our log file. note("AmphetaDesk has added '$title' successfully.", 1, 1); } return 1; } ############################################################################### # check_for_duplicate_filename - checks for sub'd filenames existence. # # check_for_duplicate_url - checks for the existance of a sub'd url. # ############################################################################### # USAGE: # # check_for_duplicate_filename; # # check_for_duplicate_url; # # # # NOTES: # # These routines merely check to see if various filenames or URLs already # # exist in the currently subscribed channels. If so, we return happily. # # # # RETURNS: # # 1; a matching filename or url was found. # # 0; no matching filename or url was found. # ############################################################################### sub check_for_duplicate_url { my ($url) = @_; foreach (keys %CHANNELS) { return 1 if $CHANNELS{$_}{xmlurl} eq $url; } return 0; } sub check_for_duplicate_filename { my ($filename) = @_; foreach (keys %CHANNELS) { return 1 if $CHANNELS{$_}{filename} eq $filename; } return 0; } ############################################################################### # count_my_channels - simply spits out the number of sub'd channels. # ############################################################################### # USAGE: # # my $total_count = count_my_channels; # # # # NOTES: # # This routine counts the number of channels currently subscribed and # # returns the result. No muss, no fuss. Wheeeeee. My comments own j00. # # # # RETURNS: # # $count; the total number of subscribed channels. # ############################################################################### sub count_my_channels { my $total_count; # /me coughs like a sick dog. bah! foreach ( values %CHANNELS ) { $total_count++; } $total_count; # /me wanders about in a daze, moaning. } ############################################################################### # create_channel_filename - creates a filename based off the channel title. # ############################################################################### # USAGE: # # create_channel_filename( $channel_title ); # # # # NOTES: # # This routine looks at $channel_title and creates a semi-unique filename # # based off it. It chalks on ".xml" to the end, and returns the creation. # # # # RETURNS: # # $filename; the semi-unique filename created. # ############################################################################### sub create_channel_filename { my ($channel_title) = @_; # removing nonword characters, # lowercase, chop at 20, # and add extension. $channel_title =~ s/\W//gi; $channel_title = lc($channel_title); $channel_title = substr($channel_title, 0, 20); # check the %CHANNELS to see if the filename is already # in use. if so, chop the last four chars off the string, # and append a number. fixes moreover.com title bug. if ( check_for_duplicate_filename("$channel_title.xml") ) { $channel_title = substr($channel_title, -4); $channel_title .= int(rand 8999)+1000; } # my logfile is growing larger. note("Channel filename has been created: $channel_title.xml."); return "$channel_title.xml"; } ############################################################################### # del_url - delete a single channel from the subscribed list # ############################################################################### # USAGE: # # del_url( $channel_url ); # # # # NOTES: # # This routine takes a look at the passed channel url and sees if there is # # a matching one in our global %CHANNELS. If there is, then we delete that # # one out of there, and resave our channels file. Multiple urls can be # # specified if they are seperated by ",http://" or a newline. # # # # RETURNS: # # 1; there's no reason why this routine would fail. # ############################################################################### sub del_url { my ($incoming_urls) = @_; return unless $incoming_urls; # split the channel url on newline or http://. my @urls = split(/\n|,http:\/\//, $incoming_urls); # now, loop through each one. foreach my $url (@urls) { # add the http:// back if missing. $url =~ s/^/http:\/\// unless $url =~ /^http:\/\//; # go through each of the channel # and remove from our $CHANNELS # as well as the hard drive. if (exists $CHANNELS{$url}) { my $file_location = catfile(get_setting("dir_data_channels"), $CHANNELS{$url}{filename}); note("AmphetaDesk has removed '$CHANNELS{$url}{title}' successfully.", 1, 1); delete $CHANNELS{$url}; unlink $file_location; # listening to chemical brothers. } else { note("AmphetaDesk couldn't find $url. Did you already remove it?", 1, 1); } } # save the channels save_my_channels(); return 1; } ############################################################################### # download_my_channels - read our myChannels.opml and get all channels. # ############################################################################### # USAGE: # # download_my_channels(); # # # # NOTES: # # This routine takes a look at our loaded %CHANNELS and downloads each # # one to our local directory, assuming it meets a number of time-based # # prerequisities. It will sort the downloads by oldest modified channel. # # # # RETURNS: # # 0; if the subscriptions_file could not be loaded. # # 1; all channels were mirrored successfully. # ############################################################################### sub download_my_channels { # sort and process each one. the channels are sorted in reverse order, # based on the last time they were downloaded, so we loop through each ID. my @sorted_channels = get_my_sorted_channels( "date_downloaded", "reversed_urls" ); foreach my $url (@sorted_channels) { # if the downloaded date doesn't exist (because of an import, for example), # set yesterday's date (which will force an update). check date_added too. if (not $CHANNELS{$url}{date_downloaded} or $CHANNELS{$url}{date_downloaded} eq "") { $CHANNELS{$url}{date_downloaded} = time2iso( time-86400 ); } $CHANNELS{$url}{date_added} = $CHANNELS{$url}{date_downloaded} unless $CHANNELS{$url}{date_added}; # if the file exists, then check the timestamps and see if we should download. if ( -e catfile( get_setting("dir_data_channels"), $CHANNELS{$url}{filename} ) ) { # figure out the epoch seconds on the last time the channel was downloaded. my $channel_seconds = str2time($CHANNELS{$url}{date_downloaded}); # if the last time it was downloaded has been less # than an hour ago, then we skip the blasted thing. # via 'next'. if over an hour, we'll download it. # over an hour ago, then we download the file. if ((time - $channel_seconds) < 3600) { note("Skipping $CHANNELS{$url}{filename} - local copy is less than an hour old."); next; } else { note("Checking $CHANNELS{$url}{filename} - local copy is more than an hour old.", 1); } } # else, the file doesn't exist, so let ourselves know. else { note("Downloading $CHANNELS{$url}{filename} - local copy doesn't exist.", 1); } # download and store the file. my $result = mirror( $CHANNELS{$url}{xmlurl}, catfile( get_setting("dir_data_channels"), $CHANNELS{$url}{filename} ) ); # if we did indeed download something, then update our # date_downloaded in our %CHANNELS. we'll resave later. if ($result) { $CHANNELS{$url}{date_downloaded} = time2iso(time); } } # make a pretty divider for our log file and window. note("--------------------------------------------------------------------------------", 1); # count 'em. my $total_count = count_my_channels; if ($total_count) { note("There was a total of $total_count subscribed channels."); note("We dance a drunken jig of epic proportions! " . " ...") if $total_count > 100; } # resave the channels list # with the latest dates. save_my_channels(); return 1; } ############################################################################### # get_my_channels_data - get the value of the passed data from channel url. # # set_my_channels_data - set the value of the passed data for channel url. # ############################################################################### # USAGE: # # my $answer = get_my_channels_data( $url, "app_os" ); # # set_setting( $url, "title", "Gamegrene.com" ); # # # # NOTES: # # Returns the value of the passed setting in $url, from $CHANNELS. If # # the passed setting doesn't exist, we return a hash ref of all settings. # # Sets the passed variable to the passed value for $url, in $CHANNELS # # # # RETURNS: # # $value; the value of the passed or set. # # undef; if the setting doesn't exist or isn't defined. # ############################################################################### sub get_my_channels_data { my ($url, $setting) = @_; return undef unless $CHANNELS{$url}; return $CHANNELS{$url} unless $setting; $CHANNELS{$url}{$setting} || undef; } sub set_my_channels_data { my ($url, $setting, $value) = @_; $CHANNELS{$url}{$setting} = $value; $CHANNELS{$url}{$setting}; } ############################################################################### # get_my_sorted_channels - get arrays of various sorted channel options. # ############################################################################### # USAGE: # # # returns complete channel data, sorted by title. # # my @array = get_my_sorted_channels("title", "data"); # # # # # returns a list of arrays, sorted by last downloaded. # # my @array = get_my_sorted_channels("date_downloaded", "reversed_urls"); # # # # NOTES: # # This routine goes through our currently stored subscription list in # # %CHANNELS, and returns an array based on how the data should be sorted # # as well as how the data is returned. Sorting can be done on ANY # # attribute stored in %CHANNELS, and the returned array could be sorted # # normally or reversed, and could contain either URLs of the channels, # # or hashes of the data for the specific channel. # # # # RETURNS: # # @array; the sorted array of channel ids. # ############################################################################### sub get_my_sorted_channels { my ($sort_by, $return) = @_; # prepare our various arrays. my @urls = sort { uc( $CHANNELS{$a}{$sort_by} ) cmp uc ( $CHANNELS{$b}{$sort_by} ) } keys %CHANNELS; my @urls_r = sort { uc( $CHANNELS{$b}{$sort_by} ) cmp uc ( $CHANNELS{$a}{$sort_by} ) } keys %CHANNELS; my @data; foreach (@urls) { push(@data, $CHANNELS{$_}); } my @data_r; foreach (@urls_r) { push(@data_r, $CHANNELS{$_}); } # return the format requested. return @urls if $return eq "urls"; return @urls_r if $return eq "reversed_urls"; return @data if $return eq "data"; return @data_r if $return eq "reversed_data"; } ############################################################################### # import_my_channels - imports the OPML data into the current list. # # load_my_channels - reads in the myChannels.opml and returns the data. # ############################################################################### # USAGE: # # load_my_channels; # # load_my_channels("/path/to/file"); # # load_my_channels($string_with_opml); # # import_my_channels("/path/to/file"); # # import_my_channels($string_with_opml); # # # # NOTES: # # This routine reads the passed info into %CHANNELS. If this file does not # # exist, it will create one in memory based on the defaults. This fixes # # an earlier (pre v0.92) problem where upgrading became a hassle since # # the files could be overwritten. If any changes are made to the default # # channels, they are then saved to disk. The hash this routine creates is # # keyed to the channels xml (see the default hash layout below for an # # example of what it looks like). # # # # This routine can accept a local file path, or a string with OPML in it. # # # # RETURNS: # # undef; if the import routine found no applicable XML URLs. # # 1; this routine nearly always returns successfully. # ############################################################################### sub import_my_channels { my ($o) = @_; my $rc = load_my_channels($o); save_my_channels(); return $rc; } sub load_my_channels { # import_my_channels is just a wrapper around this. my ($opml) = @_; # if we've got data from a file or a string, import. if ($opml and ($opml =~ /\n|\f|\r|\t/ or -e $opml)) { # if this isn't a string, open. unless ($opml =~ /\n|\f|\r|\t/) { # Radio Userland or similar importing. we load the file into # a string first so that we can lowercase xmlUrl and htmlUrl. open (OPML, $opml) or note("There was an error opening the OPML file. " . "Please report the following error to " . get_setting("app_email") . " : $!", 1); local $/ = undef; $opml = ; close(OPML); } # does this OPML file have any xmlurl attributes? unless ($opml =~ / xmlurl=/i) { note("AmphetaDesk could not find any " . "XML URLs within the loaded OPML file.", 1, 1); return undef; } # our data is loaded in (either from the file, or as # passed), so we do a innerUppers conversion to lowercase. $opml =~ s/xmlUrl/xmlurl/g; $opml =~ s/htmlUrl/htmlurl/g; # load the data into a temporary data structure. note("Loading and parsing the channels from the OPML file."); my $temp_data; eval { $temp_data = XMLin( $opml, keyattr=>{ outline => "+xmlurl" } ) }; if ($@) { $@ =~ s/[\r\n\f]//g; note("There was an error loading the OPML data. " . "Please report the following error to " . get_setting("app_email") . " : $@.", 1); } # does the file only have one channel? if so, our structure # doesn't contain an array (which we assume below in our foreach), # so we've got to treat it specially here. ah well. silly code. if ($temp_data->{body}->{outline}->{xmlurl}) { # Radio Userland or similar importing. unless ( $temp_data->{body}->{outline}->{filename} ) { $temp_data->{body}->{outline}->{filename} = create_channel_filename($temp_data->{body}->{outline}->{title}); } unless ( $temp_data->{body}->{outline}->{date_added} ) { $temp_data->{body}->{outline}->{date_added} = time2iso( time ); } # set the date_added iso. date downloaded will be set on next dl. # and add the channel to our master structure, going # through each hash entry and adding them manually. # we do this so we don't clobber/delete existing attributes. my $smaller_hashkey = $temp_data->{body}->{outline}->{xmlurl}; foreach my $key ( keys %{ $temp_data->{body}->{outline} } ) { $CHANNELS{ $smaller_hashkey }->{$key} = $temp_data->{body}->{outline}->{$key}; } return 1; # we return early so we don't trip our foreach. } # take the temporary data structure and # copy it into the right %CHANNELS structure. foreach my $channel ( values %{ $temp_data->{body}->{outline} } ) { # Radio Userland or similar importing. unless ( $channel->{filename} ) { $channel->{filename} = create_channel_filename($channel->{title}); } unless ( $channel->{date_added} ) { $channel->{date_added} = time2iso( time ); } # and add the channel to our master structure, going # through each hash entry and adding them manually. # we do this so we don't clobber/delete existing attributes. foreach my $key ( keys %{ $channel } ) { $CHANNELS{ $channel->{xmlurl} }->{$key} = $channel->{$key}; } } return 1; } # if $opml wasn't passed, # then load our defaults. else { note("User channels configuration doesn't exist. Using defaults."); %CHANNELS = ( "http://www.disobey.com/amphetadesk/news.xml" => { date_added => "2002-03-01 00:00:00", date_downloaded => "2002-03-01 00:00:00", description => "The latest news, updates, and press mentions of Disobey.com's AmphetaDesk::AmphetaDesk.", filename => "amphetadeskdocumenta.xml", htmlurl => "http://www.disobey.com/amphetadesk/", title => "Disobey.com's AmphetaDesk::AmphetaDesk - News and Updates", xmlurl => "http://www.disobey.com/amphetadesk/news.xml" }, "http://www.freebsd.org/news/news.rdf" => { date_added => "2005-10-11 00:00:00", date_downloaded => "2005-10-11 00:00:00", description => "News from the FreeBSD Project", filename => "freebsdprojectnews.xml", htmlurl => "http://www.FreeBSD.org/news/", title => "FreeBSD Project News", xmlurl => "http://www.freebsd.org/news/news.rdf" }, "http://www.freshports.org/news.php" => { date_added => "2005-10-11 00:00:00", date_downloaded => "2005-10-11 00:00:00", description => "he easiest place to find ports", filename => "freshportstheplacefo.xml", htmlurl => "http://www.FreshPorts.org/", title => "FreshPorts -- The Place For Ports", xmlurl => "http://www.freshports.org/news.php" }, "http://www.oreillynet.com/feeds/author/?x-au=73&x-mimetype=application/rdf+xm" => { date_added => "2005-10-11 00:00:00", date_downloaded => "2005-10-11 00:00:00", description => "Dru Lavigne's O'Reilly Network Items: Weblogs, Articles, Hacks and Books", filename => "drulavigneoreillynet.xml", htmlurl => "http://www.oreillynet.com/pub/au/73", title => "Dru Lavigne, O'Reilly Network", xmlurl => "http://www.oreillynet.com/feeds/author/?x-au=73&x-mimetype=application/rdf+xm" }, "http://daily.daemonnews.org/ddn.rdf.php3" => { date_added => "2005-10-11 00:00:00", date_downloaded => "2005-10-11 00:00:00", description => "Daemon News PHP News System", filename => "bsdnews.xml", htmlurl => "http://bsdnews.com", title => "BSD News", xmlurl => "http://daily.daemonnews.org/ddn.rdf.php3" } ); # now, save the subscriptions to disk. save_my_channels(); } return 1; } ############################################################################### # remove_old_channel_files - deletes old and dejected files. # ############################################################################### # USAGE: # # remove_old_channel_files; # # # # NOTES: # # A clean up routine run at the beginning of each Ampheta start that will # # remove any extra files that are no longer needed (channels, etc.). This # # really shouldn't be necessary anymore, but can occur with third party # # modification of the myChannels.opml without regard for deleting # # orphaned/stored channel data. # # # # RETURNS: # # 0; if the channels directory could not be opened. # # 1; if everything happened successfully. # ############################################################################### sub remove_old_channel_files { # say hello and load myChannels.opml. note("Checking for orphaned channel files to delete..."); # open the directory and suck in the files. opendir(CHANNELS , get_setting("dir_data_channels")) or return 0; my @directory_files = grep !/^\.\.?$/, readdir(CHANNELS); closedir(CHANNELS); # now go through each file and each # subscription and make sure they match up. foreach my $directory_file ( @directory_files ) { # if the subscription matches the directory file, # then it should be there and we last; out of it. my $match; foreach my $channel_url ( keys %CHANNELS ) { if ( $directory_file eq $CHANNELS{$channel_url}{filename} ) { $match++; last; } } # if there was no match, however, it's a file # we don't think should be there, so we delete it. unless ( $match ) { note("Condemning \"$directory_file\" back to the grave."); unlink( catfile(get_setting("dir_data_channels"), $directory_file) ) or note("AmphetaDesk couldn't delete $directory_file. Please " . "report the following error to " . get_setting("app_email") . ": $!"); } $match = 0; } # make a pretty divider for our log file. note("--------------------------------------------------------------------------------"); return 1; } ############################################################################### # save_my_channels - saves the relevant channel subscriptions to a file. # ############################################################################### # USAGE: # # save_my_channels; # # save_my_channels("/path/to/file"); # # # # NOTES: # # This routine merely takes the current %CHANNELS, extracts the channel # # information that should be saved, and then dumps it to the user's # # data directory (or the passed file location). It will be read in on # # successive loads. # # # # RETURNS: # # 1; if the new channels were saved and activated. # # 0; if we couldn't save the settings to the passed file name. # ############################################################################### sub save_my_channels { my ($passed_path) = @_; # just some happy blurbage for the log file. note("Creating a brand new channels file from the current subscriptions."); # create an outputtable settings structure. my $temp_data; # used for temporary storage. foreach my $channel ( values %CHANNELS ) { push @{ $temp_data->{body}->{outline} }, $channel; } # write out the file. we set noattr for "no attributes", # as well as "opml" as the root name. xmldecl sets the xml # declaration to version 1.0... eval { XMLout( $temp_data, noattr => 0, rootname => 'opml', xmldecl => "", outputfile => $passed_path || get_setting("files_myChannels"), ) } or return 0; # die if there are any errors. if ($@) { $@ =~ s/[\r\n\f]//g; note("There was an error saving " . $passed_path || get_setting("names_file_myChannels") . ". Please report the following error to " . get_setting("app_email") . ": $@.", 1); } return 1; } ############################################################################### # update_my_channel_data - takes a URL and new data and updates our OPML. # ############################################################################### # USAGE: # # update_my_channels_data($url, $parsed_data); # # # # NOTES: # # This routine looks at the passed $data and uses it to update the OPML # # information for our passed $url. This routine is typically called with # # the data returned from a load_my_channel routine (which indicates a # # successful parse. Incidentally, this is called for every successful # # parse that we do, so that we'll always have the latest info. # # # # RETURNS: # # 0; if the passed $url could not be found in our OPML. # # 1; if everything in the channel was updated successfully. # ############################################################################### sub update_my_channel_data { my ($url, $data) = @_; # we're not subscribed to this. unless ($CHANNELS{$url}) { note("AmphetaDesk couldn't update $url because " . "we couldn't find a matching subscription. Please report this error to " . get_setting("app_email")); return 0; } # now update each relevant entry from our newly parsed data. $CHANNELS{$url}{description} = $data->{channel}{description} if $data->{channel}{description}; $CHANNELS{$url}{htmlurl} = $data->{channel}{"link"} if $data->{channel}{"link"}; $CHANNELS{$url}{title} = $data->{channel}{title} if $data->{channel}{title}; # for email, we try going from best to worst. $CHANNELS{$url}{email} = $data->{channel}{managingEditor} || $data->{channel}{webMaster} || $data->{header}{webMaster} || $data->{header}{managingEditor} || ""; return 1; } 1;