<html><head><meta name="color-scheme" content="light dark"></head><body><pre style="word-wrap: break-word; white-space: pre-wrap;">#!/usr/bin/perl

use warnings;
use strict;

use XML::RSS;
use LWP::Simple;
use List::Util qw(first);
use File::Save::Home qw/make_subhome_directory get_subhome_directory_status/;
use Cache::File;
use Getopt::Long;
use Pod::Usage;
use HTML::Entities;

my $VERSION = 0.2;

my ( $show_man, $show_help, $debug);
my ( $installed_only, $act_as_filter, $reload_changes );
my $feed_uri = 'http://search.cpan.org/uploads.rdf';
my $log = sub{};

GetOptions(
  'filter!' =&gt; \$act_as_filter, 'uri' =&gt; \$feed_uri,
  'installed' =&gt; \$installed_only,
  'debug' =&gt; \$debug, 'reload' =&gt; \$reload_changes,
  'help|?' =&gt; \$show_help, man =&gt; \$show_man,
);

pod2usage(0) if $show_help;
pod2usage(-exitstatus =&gt; 0, -verbose =&gt; 2) if $show_man;
$log = sub{ print STDERR @_, "\n" } if $debug;

if ( $installed_only ) {
  require Module::Locate;
  Module::Locate-&gt;import(qw/locate/);
};
sub dist2module {
  my ( $m, $v ) = split( qr/-(?=v?\d+\.\d+)/, $_[0] );
  $m =~ s/\W/::/g;
  return $m;
}


my $uploads = $act_as_filter ? join( '', &lt;&gt; ) : get( $feed_uri );
die( "Unable to obtain uploads from standard input or http" ) unless $uploads;

my $cache_dir = get_subhome_directory_status( '.cpan-changes' );
make_subhome_directory( $cache_dir );

my $cache = Cache::File-&gt;new
  ( cache_root =&gt; $cache_dir-&gt;{abs}, default_expires =&gt; '1 week' );

my $rss = XML::RSS-&gt;new();
$rss-&gt;parse( $uploads );

my @ignore;

# print the title and link of each RSS item
foreach my $item (@{$rss-&gt;{'items'}}) {

  if ( $installed_only ) {
	 my $mod = dist2module( $item-&gt;{'title'} );
	 my $mod_info = eval{ locate( $mod ) };
	 warn( sprintf "Invalid dist name '%s':\n%s", $item-&gt;{'title'}, $@ ) if $@;
	 if ( not $mod_info || $@ ) {
		push @ignore, $item;
		next;
	 }
  };

  $log-&gt;( "title: ", $item-&gt;{'title'} ,"\nlink: $item-&gt;{'link'}" );

  my $changes;
  $changes = $cache-&gt;get( $item-&gt;{link} ) unless $reload_changes;

  if ( not $changes ) {
	 ( my $src_dir = $item-&gt;{'link'} ) =~ s|/~([^/]+)/|/src/\U$1\E/|;
	 my $uri = first{ head( $_ ) } map{ $src_dir . $_ }
		map{( $_, uc )} qw/Changes ChangeLog/;
    $changes = $uri ? get( $ uri ) : 'No changelog found';
	 $cache-&gt;set( $item-&gt;{link}, $changes );
  };

  # for some reason we will need to double encode the changes here
  my $encoded = encode_entities(encode_entities( $changes ),'&amp;');
  $item-&gt;{description} .= sprintf( "&lt;hr /&gt;&lt;pre&gt;%s&lt;/pre&gt;", $encoded );
  $log-&gt;( "description: ", $item-&gt;{description} );
}


if ( $installed_only ) {
  # filter modules that are not installed locally
  $rss-&gt;{items} =
	 [ grep{ my $item=$_; not grep{ $_ == $item } @ignore } @{$rss-&gt;{items}} ];
  $log-&gt;( "Displaying:\n  ".join( "\n  ", map{$_-&gt;{title}} @{$rss-&gt;{items}}) );
};

print $rss-&gt;as_rss_1_0;
$log-&gt;( $rss-&gt;as_rss_1_0, "\n\nDONE!\n" );

__END__

=head1 NAME

cpan-changes.pl - Enrich CPAN recent module feed with change-log info

=head1 SYNOPSIS

cpan-changes.pl [options]

 Options:
   --uri        feed uri [default: http://search.cpan.org/uploads.rdf]
   --filter     use as filter, i.e. retrieve RDF from standard input
   --installed  discard modules that aren't installed locally [EXPERIMENTAL]
   --reload     omits cache look-up and reload all changelogs
   --debug      processed info is logged to standard error
   --help       brief help message
   --man        full documentation

=head1 README

This program loads the CPAN recent modules feed, either from your
local CPAN mirror or as a filter in your favorite feed reader.
Afterwards it scans it and tries to fetch the changelog for new module
releases and parses them back into the RSS feed. All retrieved changelogs
are stored in a cache to minimize the load put onto your local CPAN mirror.

=head1 INSTALLED MODULES ONLY

This is a fragile hack using custom regexes to guess the module name
from the distribution (suggestions how to do this in a standard way
would be very welcome) and then just pull the respective item from the
internals of XML::RSS. Use with caution!

=head1 BUGS

Probably many. Up until now this program has only be tested under Linux
and in combination with the Liferea feed reader (both as conversion filter
and as programmatic source). I'd love to get feedback about your experiences
with this module in other environments.

=head1 PREREQUISITES

This script requires C&lt;perl 5.6&gt;, C&lt;XML::RSS 1.00&gt;, C&lt;LWP::Simple&gt;,
C&lt;File::Save::Home 0.03&gt;, C&lt;Cache 2.00&gt;, C&lt;Getopt::Long 2.4&gt;,
C&lt;Pod::Usage 1.08&gt; and C&lt;HTML::Parser 3.26&gt;. I know these are a lot of
prerequisites for a simple script (especially L&lt;File::Save::Home&gt;), but as
the intended use case for this script in on the desktop of an active perl
developer, this should do.

=head1 COREQUISITES

C&lt;Module::Locate 1.2&gt; for the option to display only changes to
locally installed modules.

=head1 CPAN META INFO

=pod SCRIPT CATEGORIES

CPAN
Web
Web/RSS

=pod OSNAMES

any

=head1 AUTHOR

Sebastian Willert &lt;willert@cpan.org&gt;

=cut
</pre></body></html>