Last month I decided to jump on the last.fm (aka audioscrobbler) bandwagon. For those not in the know, this is a service where your music player program (using a special plugin) sends real-time updates about what music you're playing to a website, where they are entered into a database awaiting all sorts of statistical analysis you never thought possible (or at least not worth anybody's time). These range from simple things like a list of tracks you listened to recently, to weekly and monthly charts of your favorite artists and locating "neighbors" from among the other users who like the same things you do. And, as you would expect from a social-type website, you can establish friendships with other users.

Most of this information is available through web pages and also XML/RSS web services ready to be consumed by news aggregators or any other programs that people (like me) think up. One of the simplest web services lets you download an RSS feed with the last 10 tracks that any user played. Great, I can use Liferea to see what songs I've just listened to. This is of course sort of useless since by the time Quod Libet sends the song name to last.fm and Liferea pulls a fresh copy of the RSS, I'm probably onto another song already. More interesting is the fact that I can monitor one of my friend's listining habits in near real-time. But still, a single user isn't very interesting...

Another web service allows you to download an XML list of a user's friends. Combining this information with the feeds for individual users' recent tracks would actually provide some interesting information, but unfortunately they don't have a web service for this. HTTP requests, XML parsing, data sorting... Perl to the rescue! So I wrote the following script (it doesn't actually use the aforementioned RSS feeds, but an XML service that I figured would be easier to parse) to find out who my friends are, find out what they've been listening to, and write the results in text, RSS, or Data::Dumper format.

#!/usr/bin/perl use strict; use warnings; use XML::Parser; use Getopt::Lucid qw/:all/; use LWP::Simple; use Data::Dumper; use Heap::Simple; use POSIX qw/strftime/; my @friends; sub fparse_start { my $expat = shift; my $elt = shift; if ($elt eq 'user') { my %attr = @_; push @friends, $attr{username}; } } my @tracks; my $insertpoint = -1; sub tparse_start { my $expat = shift; my $elt = shift; my %attr = @_; if ($elt eq 'track') { push @tracks, []; } elsif ($elt eq 'artist') { $insertpoint = 0; } elsif ($elt eq 'name') { $insertpoint = 1; } elsif ($elt eq 'url') { $insertpoint = 2; } elsif ($elt eq 'date') { $tracks[$#tracks]->[3] = $attr{uts}; $insertpoint = 4; } } sub tparse_char { my $expat = shift; my $string = shift; if ($insertpoint > -1) { $tracks[$#tracks]->[$insertpoint] = $string; $insertpoint = -1; } } my @opts = ( Param('user|u')->required, Param('mode|m')->default('text') ); my $opt = Getopt::Lucid->getopt( \@opts ); my $user = $opt->get_user(); my $mode = $opt->get_mode(); my $xp = new XML::Parser(Handlers => {Start => \&fparse_start}); my $content = get("http://ws.audioscrobbler.com/1.0/user/${user}/frien +ds.xml"); if ($content) { $xp->parse($content); my $alltracks = new Heap::Simple(elements => [Array => 4], order => '>'); foreach my $fr(@friends) { my $content = get("http://ws.audioscrobbler.com/1.0/user/${fr} +/recenttracks.xml"); if ($content) { my $xp = new XML::Parser(Handlers => { Start => \&tparse_st +art, Char => \&tparse_cha +r }); $xp->parse($content); foreach my $t(@tracks) { $alltracks->insert([ $fr, @$t ]); } @tracks = (); } else { print "error getting tracks for $fr\n"; } } if ($mode eq 'dump') { for (1..$alltracks->count()) { my $tr = $alltracks->extract_top(); print Dumper(\$tr); } } elsif ($mode eq 'text') { for (1..$alltracks->count()) { my $tr = $alltracks->extract_top(); print "User: $tr->[0]\n"; print "Track: $tr->[1] - $tr->[2]\nDate: $tr->[5]\n"; } } elsif ($mode eq 'rss') { print "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"; print "<rss version=\"2.0\">\n"; print "<channel>\n"; print "<language>en</language>\n"; print "<pubDate>", scalar localtime, "</pubDate>\n"; print "<ttl>10</ttl>\n"; print "<title>${user}'s Friends' Recent Tracks</title>\n"; for (1..$alltracks->count()) { print "<item>\n"; my $tr = $alltracks->extract_top(); my $title = "$tr->[0] - $tr->[1] - $tr->[2]"; my $date = strftime '%a, %d %b %Y %H:%M:%S %z', localtime +$tr->[4]; print "<title>$title</title>\n"; print "<link>$tr->[3]</link>\n"; print "<pubDate>$date</pubDate>\n"; print "</item>\n"; } print "</channel>\n"; print "</rss>\n"; } else { print "Undefined output mode $mode\n"; } } else { die "Badness!"; }

Replies are listed 'Best First'.
Re: What are YOUR friends listening to?
by themage (Friar) on Oct 25, 2006 at 10:00 UTC
    Hi Alivac,

    It looks nice for me, and is for sure a very good use for perl. But, usually, at least in my case, I already know most of the music my friends listen.

    So, When I saw your code my first thought was... Can I use this to check what my neighbours are listening to? My neighbours are those people who listen to many of the musics I listen more, so probably I'll like most music they listen also. But as they are completly unknow to me, probably they know other things I still don't know.

    So, I made a small change in your script, to make it possible to pass another param to specify that I want friends or neighbours. Here is the diff:
    65c65,66 < Param('mode|m')->default('text') --- > Param('mode|m')->default('text'), > Param('info|i')->defau +lt('friends'), 71a73 > my $info = $opt->get_info(); 74c76,79 < my $content = get("http://ws.audioscrobbler.com/1.0/user/${user}/fri +ends.xml"); --- > my $feed=$info eq 'neighbours' > ?"http://ws.audioscrobbler.com/1.0/user/${user}/neighb +ours.xml" > :"http://ws.audioscrobbler.com/1.0/user/${user}/friend +s.xml"; > my $content = get($feed);
    Congratulation. A very nice use for perl.