The Perl and Raku Conference 2025: Greenville, South Carolina - June 27-29 Learn more

#!/usr/bin/perl
use strict;
use YAML qw<DumpFile LoadFile>;
use Parallel::Iterator qw( iterate );
$| = 1;
use constant OUTPUT => 'cpan-faces';
use constant STATE => File::Spec->catfile( OUTPUT, 'work.yml' );
use constant SIZE => 80;
my $UPDATE = 0;
GetOptions( 'update' => \$UPDATE ) or die "cpan-gravatar.pl [--update]\n";
my $ua = LWP::UserAgent->new;
mkpath( OUTPUT );
my $icons = -f STATE ? LoadFile( STATE ) : {};
$SIG{INT} = sub {
print "Got SIGINT, stopping\n";
exit;
};
my $pid = $$;
END {
if ( $$ == $pid ) {
print "Saving ", STATE, "\n";
DumpFile( STATE, $icons );
my $index = File::Spec->catfile( OUTPUT, 'index.html' );
open my $ih, '>', $index or die "Can't write $index ($!)\n";
print $ih build_page( $icons );
close $ih;
}
}
update(
$icons,
$UPDATE
? sub {
my ( $icons, $id ) = @_;
return 0;
}
: sub {
my ( $icons, $id ) = @_;
return exists $icons->{$id}
&& $icons->{$id}->{state} eq 'done';
}
);
sub update {
my ( $icons, $skip_if ) = @_;
print "Getting ", MAIL_RC, "\n";
my $authors = get_authors( MAIL_RC );
open my $ah, '<:gzip', $authors or die "Can't read $authors ($!)\n";
my $iter = iterate(
{ workers => 20 },
sub {
my ( $id, undef ) = @_;
print "Checking $id\n";
return save_icon( lc( $id ) );
},
sub {
while ( defined( my $line = <$ah> ) ) {
next unless $line =~ /^alias\s+(\S+)/;
return $1;
}
return;
}
);
while ( my ( $id, $icon ) = $iter->() ) {
$icons->{$id} = $icon;
print "Icon saved as ", $icon->{name}, "\n"
if $icon && $icon->{name};
}
}
sub build_page {
my $icons = shift;
my $h = HTML::Tiny->new;
my @pic = ();
for my $id ( sort keys %$icons ) {
my $icon = $icons->{$id};
if ( my $img = $icon->{name} ) {
push @pic,
(
$h->div(
{ class => 'icon' },
$h->a(
{ href => user_home( $id ) },
$h->img(
{
src => File::Spec->abs2rel( $img, OUTPUT ),
width => SIZE,
height => SIZE,
alt => $id
}
),
),
)
);
}
}
return $h->html(
[
$h->head(
[
$h->title( 'The Faces of CPAN' ),
$h->link(
{
rel => 'stylesheet',
href => 'style.css',
type => 'text/css',
media => 'screen'
}
)
]
),
$h->body( [@pic] )
]
);
}
sub get_authors {
my $url = shift;
my $resp = $ua->get( $url );
if ( $resp->is_success ) {
my $name = File::Spec->catfile( OUTPUT, '01mailrc.txt.gz' );
open my $ah, '>', $name or die "Can't write $name ($!)\n";
binmode $ah;
print $ah $resp->content;
close $ah;
return $name;
}
else {
die $resp->status_line;
}
}
sub user_home {
my $id = shift;
return AUTHOR . lc( $id );
}
sub save_icon {
my $id = shift;
my %ext_map = ( jpeg => 'jpg' );
my ( $data, $type ) = eval { get_icon( $id ) };
if ( $@ ) {
return {
error => $@,
state => 'error'
};
}
# if ( $data && $data ne $default_image && $type =~ m{ ^image/(\S+) }x ) {
if ( $data && $type =~ m{ ^image/(\S+) }x ) {
my $ext = $ext_map{$1} || $1;
my $name = make_name( $id, $ext );
open my $ih, '>', $name
or die "Can't write $name ($!)\n";
binmode $ih;
print $ih $data;
close $ih;
return {
name => $name,
state => 'done'
};
}
return { state => 'done' };
}
sub make_name {
my ( $email, $ext ) = @_;
my %enc = (
'@' => '-AT-',
'.' => '-DOT-'
);
$email =~ s/([@.])/$enc{$1}||$1/eg;
return File::Spec->catfile( OUTPUT, "$email.$ext" );
}
sub get_default_icon {
if ( my ( $data, $type ) = get_icon( 'some.made.up@name.hexten.net' ) ) {
return $data;
}
die "Can't fetch default icon\n";
}
sub get_icon {
my $id = shift;
$id =~ s{^(((.).).*)$}{$3/$2/$1};
TRY: for my $ext ( qw( jpg png ) ) {
my $url = ICON_BASE . '/' . $id . '.' . $ext;
my $resp = $ua->get( $url );
if ( $resp->is_success ) {
return ( $resp->content, $resp->header( 'Content-Type' ) );
}
elsif ( $resp->code == 404 ) {
next TRY;
}
else {
die join ' ', $resp->code, $resp->message;
}
}
return;
}