#!/usr/bin/perl # last.fm-ripper - (c) Copyright 2006 Jochen Schneider # # a simple last.fm to mp3-file ripper # # This program is free software; you can redistribute it and/or modify # it under the same terms as Perl itself. $VERSION = 1.2; $version = '1.1.4'; # current version string of the offical last.fm player $platform = guess_platform(); $ws_host = 'ws.audioscrobbler.com'; # last.fm webservices hostname $output_directory = '.'; # where to store mp3 files $get_covers = 1 unless (system("wget --version > /dev/null")==-1); # retrieve covers if wget is available # check if all essential modules are in place map { my $module = $_; unless (eval "use $module; 1") { print "Module \"$module\" is not availabe on your Perl installation\nEnter \"perl -MCPAN -e \"install $module\" to install it\n\n"; exit(1); } } ('Getopt::Long','IO::Socket','FileHandle','Digest::MD5','IO::Select'); # can we tag the mp3 files? if(eval "use MP3::Tag; 1") { $tag_mp3s = 1; } STDOUT->autoflush; $SIG{INT} = sub { $bye_bye = 1 }; Getopt::Long::Configure("pass_through"); GetOptions( 'help|?' => \$help, 'debug|d' => \$debug, 'no_covers|n' => $no_covers, 'artist|a=s' => \$artist, 'username|u=s' => \$username, 'password|p=s' => \$password, 'output_dir|o=s' => \$output_directory, 'aws_token|w=s' => \$aws_token); # let's see if params are correct and what our mission goals are if($help) { usage(); exit; } if($no_covers) { undef $get_covers; } if($username eq '') { print "missing username\n"; exit(1); } if(!-d $output_directory) { print "output directory does not exist.\n"; exit(1); } # will we listen to a supplied lastfm-url or will we roll our own for a similar-artist scheme if ($ARGV[0]=~/lastfm\:\/\//) { $url=$ARGV[0] } elsif ($artist) { $artist=~s/\s/\%20/g; $url="lastfm://artist/".$artist."/similarartists"; } else { print "please supply an artist name or last.fm url\n"; exit(1); } if($password eq '') { # we have to ask for a password # and let Term::ReadPassword do the job if available if (eval ("use Term::ReadPassword; 1")) { $password = read_password('password: '); } else { print "password: "; $password = ; chomp($password); } } if($aws_token) { # we have a amazon-webservices-token. Is Net::Amazon working? map { my $module = $_; if(!eval ("use $module; 1")) { die "could not initialize $module\n"; } } ('Net::Amazon', 'Net::Amazon::Request::Artist'); } # last.fm uses a high-security password-obfuscation ;-) $password_md5 = md5_password($password); debug("Now trying to play URL $url for user $username with pass $password - md5hash: $password_md5\n"); $sockets = IO::Select->new(); while (1) { $buffer = ''; if (!$handshake) { debug("trying to log in\n"); $handshake = IO::Socket::INET->new(PeerAddr => $ws_host, PeerPort => 80, Proto => "tcp") || die "could not initialize webservice socket\n"; $handshake_url = "/radio/handshake.php?version=$version&platform=$platform&username=$username&passwordmd5=$password_md5&debug=0"; $request = "GET $handshake_url HTTP/1.1\r\nhost: $ws_host\r\n\r\n"; print $handshake $request; $sockets->add($handshake); } elsif (!$tuned) { debug("trying to tune station\n"); $tune = IO::Socket::INET->new(PeerAddr => $ws_host, PeerPort => 80, Proto => "tcp") || die "could not initialize webservice socket\n"; $tune_url = "/radio/adjust.php?session=$session&url=$url&debug=0"; $request = "GET $tune_url HTTP/1.1\r\nhost: $ws_host\r\n\r\n"; print $tune $request; $sockets->add($tune); } elsif (!$streaming) { debug("requesting streaming data from $mp3_host\n"); $mp3 = IO::Socket::INET->new(PeerAddr => $mp3_host, PeerPort => 80, Proto => "tcp") || die "could not initialize mp3 socket\n"; $mp3_url = "/last.mp3?Session=$session"; $request = "GET $mp3_url HTTP/1.1\r\nhost: $mp3_host\r\n\r\n"; print $mp3 $request; $sockets->add($mp3); $new_track = 1; $streaming = 1; } elsif ($new_track) { # are we receiving mp3 data? - then ask for track info! if (length($mp3_data) >= 4096) { debug("trying to get new track data\n"); sleep 3; $track_info = IO::Socket::INET->new(PeerAddr => $ws_host, PeerPort => 80, Proto => "tcp") || die "could not initialize webservice socket\n"; $track_data_url = "/radio/np.php?session=$session"; $request = "GET $track_data_url HTTP/1.1\r\nhost: $ws_host\r\n\r\n"; print $track_info $request; $sockets->add($track_info); undef $new_track; } } elsif ($track_info_data) { debug("parsing track_info_data\n"); map { chomp(); debug("$_\n"); /(.+)=(.+)/; $track_info{$1} = $2; } split /\n/,$track_info_data; undef $track_info_data; $sockets->remove(track_info); $track_info->shutdown(2); if ($get_covers) { # try to retrieve the largest available cover jpeg with wget map { if ($_ ne '') { $cover = $_; } } ($track_info{'albumcover_small'},$track_info{'albumcover_medium'},$track_info{'albumcover_large'}); if ($cover ne '') { $cover_file = "$track_info{'artist'}-$track_info{'album'}-$track_info{'track'}.jpg"; if(system("wget -O \"$cover_file\" $cover > /dev/null 2>&1 &")>0){print "failed to retrieve cover art\n"}; $cover = ''; } } if ($aws_token) { my $aws_ua = Net::Amazon->new( token => $aws_token); my $aws_request = Net::Amazon::Request::Artist->new(artist => $track_info{'artist'}); my $aws_response = $aws_ua->request($aws_request); if ($aws_response->is_success()) { my %albums; map { my $album = $_; if ($album->album() eq $track_info{'album'}) { $track_info{'year'} = $album->year(); $track_info{'label'} = $album->label(); my $track_index = 0; map { my $title = $_; chomp($title); $track_index++; if ($title eq $track_info{'track'}) { $track_info{'track_no'} = $track_index; } } $album->tracks(); $track_info{'track_count'} = $track_index; } } $aws_response->properties(); } else { print "could not ask Amazon webservices for track-info\n"; print $aws_response->message(); exit(1); } } } # handle sockets map { my $sock = $_; debug("reading from socket\n"); if ($sock eq $handshake) { debug("reading login response\n"); $sock->recv($buffer,1024,0) || $sockets->remove($sock); if($buffer =~ /session=([\w\d]+)/) { $session = $1; } if($buffer =~ /stream_url=(.+)\n/) { $stream_url = $1; $stream_url =~ /http:\/\/([\.\w\d]+)\//; $mp3_host = $1; } if($session eq 'FAILED') { die "could not login\n"; } } elsif ($sock eq $tune) { debug("reading tune response\n"); $sock->recv($buffer,1024,0) || $sockets->remove($sock); if($buffer =~ /HTTP\/1\.. 503/) { print "\nlast.fm service is temoprarily unavailable\n"; exit(1); } if($buffer =~ /response=OK/) { $tuned = 1; } else { print "sorry, could not tune last.fm to play $url.\n"; if ($artist) { print "maybe artist \"$artist\" is unkown to last.fm?\n"; } else { print "maybe url $url ist not valid?\n"; } exit(1); } } elsif ($sock eq $mp3) { debug("reading mp3 data\n"); $sock->recv($buffer,262144,0); if ($length = index($buffer,"SYNC",0) != -1 ) { debug("found new track\n"); print "\n"; if ($track_info{'artist'} ne '') { my $mp3_file_name = "$track_info{'artist'}-$track_info{'album'}-$track_info{'track'}.mp3"; my $mp3_path = $output_directory."/".$mp3_file_name; open(MP3,">$mp3_path"); print MP3 $mp3_data,substr($buffer,0,$length); close MP3; if ($tag_mp3s) { my $mp3 = MP3::Tag->new($mp3_path); my $mp3_tag = $mp3->new_tag("ID3v1"); $mp3_tag->title($track_info{'track'}); $mp3_tag->artist($track_info{'artist'}); $mp3_tag->album($track_info{'album'}); $mp3_tag->year($track_info{'year'}); $mp3_tag->track($track_info{'track_no'}); $mp3_tag->write_tag(); my $mp3v2_tag = $mp3->new_tag("ID3v2"); $mp3v2_tag->add_frame("TALB",$track_info{'album'}); $mp3v2_tag->add_frame("TIT2",$track_info{'track'}); $mp3v2_tag->add_frame("TPE1",$track_info{'artist'}); $mp3v2_tag->add_frame("TLEN",$track_info{'trackduration'}); $mp3v2_tag->add_frame("TRSN","last.fm"); $mp3v2_tag->add_frame("TRCK",$track_info{'track_no'}); $mp3v2_tag->add_frame("TYER",$track_info{'year'}); if ($get_covers) { open(COVER,"$cover_file"); my $cover_data; while(){$cover_data.=$_}; $mp3v2_tag->add_frame("APIC", chr(0x0), "image/jpeg", chr(0x0), "Cover Image", $cover_data); } $mp3v2_tag->write_tag; $mp3->close(); } } $new_track = 1; $mp3_data=substr($buffer,$length); } else { $mp3_data.=$buffer; } } elsif ($sock eq $track_info) { debug("reading track info data\n"); $sock->recv($buffer,1024,0) || $sockets->remove($sock); $track_info_data .= $buffer; } } $sockets->can_read(100); map { my $sock = $_; debug("writing to sockets\n"); } $sockets->can_write(100); print "\rDATA_LENGTH: ".length($mp3_data)." TRACK: $track_info{'track'} ARTIST: $track_info{'artist'} ALBUM: $track_info{'album'}"; if ($bye_bye){print "\nbye!\n";exit(0);} } sub debug { if ($debug) { print @_; } } sub usage { print < usage: last.fm-ripper -u [-p ] [-d] -[c] [-o ] [-a ] -u, --username last.fm username -p, --password last.fm password -a, --artist artist name (to find similar titles), obsolets last.fm url -d, --debug enable debugging output -o, --output_directory where to save mp3-files -n, --nocovers disable cover download -w, --aws_token amazon webservices developer token for advanced tagging (http://amazon.com/soap) EOF print "\n"; } sub guess_platform { my $platform = lc(`uname -s`); chomp($platform); if($platform eq ''){$platform="windows"}; #guess we are on win if uname does not work return $platform; } sub md5_password { my $password = shift @_; my $md5 = Digest::MD5->new(); $md5->reset; $md5->add($password); return $md5->hexdigest; } =pod =head1 NAME last.fm-ripper - save last.fm radio to mp3 files =head1 SYNOPSIS last.fm-ripper -u [-p ] [-d] -[c] [-o ] [-a ] -u, --username last.fm username -p, --password last.fm password -a, --artist artist name (to find similar titles), obsolets last.fm url -d, --debug enable debugging output -o, --output_directory where to save mp3-files -n, --no_covers disable cover download -w, --aws_token amazon webservices developer token for advanced tagging (http://amazon.com/soap) =head1 AUTHOR Jochen Schneider, =head1 DESCRIPTION last.fm-ripper is a small utility to save the last.fm program to individual mp3-files including all availabel id3-tags and the cover art (requires MP3::Tag module). Requires a valid last.fm login and password (http://www.last.fm/signup.php). If you don't like to enter your password on the commandline last.fm-ripper asks for it (requires Term::ReadPassword not to be echoed to the terminal). For advanced tagging (track-no., year) an amazon webservices developer token is required (http://amazon.com/soap). =head1 COPYRIGHT Copyright (c) 2006 Jochen Schneider. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the Artistic License, distributed with Perl. =cut