File Coverage

blib/lib/Audio/Scrobbler.pm
Criterion Covered Total %
statement 15 142 10.5
branch 0 62 0.0
condition 0 26 0.0
subroutine 5 12 41.6
pod 7 7 100.0
total 27 249 10.8


line stmt bran cond sub pod time code
1             package Audio::Scrobbler;
2              
3 1     1   10464 use 5.006;
  1         4  
  1         39  
4 1     1   5 use strict;
  1         2  
  1         33  
5 1     1   1033 use bytes;
  1         14  
  1         5  
6              
7             =head1 NAME
8              
9             Audio::Scrobbler - Perl interface to audioscrobbler.com/last.fm
10              
11             =head1 SYNOPSIS
12              
13             use Audio::Scrobbler;
14              
15             $scrob = new Audio::Scrobbler(cfg => { ... });
16              
17             $scrob->handshake();
18             $scrob->submit(artist => "foo", album => "hello", track => "world",
19             length => 180);
20              
21             =head1 DESCRIPTION
22              
23             The C module provides a Perl interface to the track
24             submission API of Last.fm's AudioScrobbler -
25             http://www.audioscrobbler.com/. So far, only track submissions are
26             handled; the future plans include access to the various statistics.
27              
28             =cut
29              
30 1     1   32 use Digest::MD5 qw/md5_hex/;
  1         2  
  1         61  
31 1     1   1457 use LWP::UserAgent;
  1         49561  
  1         7693  
32              
33             our @ISA = qw();
34              
35             our $VERSION = '0.01';
36              
37             sub err($ $);
38             sub handshake($);
39              
40             sub get_ua($);
41              
42             sub URLEncode($);
43             sub URLDecode($);
44              
45             =head1 METHODS
46              
47             The C class defines the following methods:
48              
49             =over 4
50              
51             =item * new ( cfg => { ... } )
52              
53             Create a new C object and initialize it with
54             the provided configuration parameters. The parameters themselves
55             are discussed in the description of the L and L
56             methods below.
57              
58             =cut
59              
60             sub new
61             {
62 0     0 1   my $proto = shift;
63 0   0       my $class = ref $proto || $proto;
64 0           my $self = { };
65 0           my %args = @_;
66              
67 0 0 0       if (exists($args{'cfg'}) && ref $args{'cfg'} eq 'HASH') {
68 0           $self->{'cfg'} = $args{'cfg'};
69             } else {
70 0           $self->{'cfg'} = { };
71             }
72 0   0       $self->{'cfg'} = $args{'cfg'} || { };
73 0           $self->{'ua'} = undef;
74 0           $self->{'req'} = { };
75 0           $self->{'err'} = undef;
76 0           bless $self, $class;
77 0           return $self;
78             }
79              
80             =item * err (message)
81              
82             Retrieves or sets the description of the last error encountered in
83             the operation of this C object.
84              
85             =cut
86              
87             sub err($ $)
88             {
89 0     0 1   my ($self, $err) = @_;
90              
91 0 0         $self->{'err'} = $err if $err;
92 0           return $self->{'err'};
93             }
94              
95             =item * handshake ()
96              
97             Perfors a handshake with the AudioScrobbler API via a request to
98             http://post.audioscrobbler.com/.
99              
100             This method requires that the following configuration parameters be set:
101              
102             =over 4
103              
104             =item * progname
105              
106             The name of the program (or plug-in) performing the AudioScrobbler handshake.
107              
108             =item * progver
109              
110             The version of the program (or plug-in).
111              
112             =item * username
113              
114             The username of the user's AudioScrobbler registration.
115              
116             =back
117              
118             If the handshake is successful, the method returns a true value, and
119             the L method may be invoked. Otherwise, an appropriate error
120             message may be retrieved via the L method.
121              
122             If the B configuration parameter is set, the L method
123             does not actually perform the handshake with the AudioScrobbler API,
124             just simulates a successful handshake and returns a true value.
125              
126             If the B configuration parameter is set, the L
127             method reports its progress with diagnostic messages to the standard output.
128              
129             =cut
130              
131             sub handshake($)
132             {
133 0     0 1   my ($self) = @_;
134 0           my ($ua, $req, $resp, $c, $s);
135 0           my (@lines);
136              
137 0           delete $self->{'nexturl'};
138 0           delete $self->{'md5ch'};
139              
140 0 0         $ua = $self->get_ua() or return undef;
141 0           $s = 'hs=true&p=1.1&c='.
142             URLEncode($self->{'cfg'}{'progname'}).'&v='.
143             URLEncode($self->{'cfg'}{'progver'}).'&u='.
144             URLEncode($self->{'cfg'}{'username'});
145 0 0         print "RDBG about to send the handshake request: $s\n"
146             if $self->{'cfg'}{'verbose'};
147 0 0         if ($self->{'cfg'}{'fake'}) {
148 0 0         print "RDBG faking it...\n" if $self->{'cfg'}{'verbose'};
149 0           $self->{'md5ch'} = 'furrfu';
150 0           $self->{'nexturl'} = 'http://furrfu.furrblah/furrquux';
151 0           return 1;
152             }
153 0           $req = new HTTP::Request('GET', "http://post.audioscrobbler.com/?$s");
154 0 0         if (!$req) {
155 0           $self->err('Could not create the handshake request object');
156 0           return undef;
157             }
158 0           $resp = $ua->request($req);
159 0 0         print "RDBG resp is $resp, success is ".$resp->is_success()."\n"
160             if $self->{'cfg'}{'verbose'};
161 0 0         if (!$resp) {
    0          
162 0           $self->err('Could not get a handshake response');
163 0           return undef;
164             } elsif (!$resp->is_success()) {
165 0           $self->err('Could not complete the handshake: '.
166             $resp->status_line());
167 0           return undef;
168             }
169 0           $c = $resp->content();
170 0 0         print "RDBG resp content is:\n$c\nRDBG ====\n"
171             if $self->{'cfg'}{'verbose'};
172 0           @lines = split /[\r\n]+/, $c;
173 0           $_ = $lines[0];
174             SWITCH:
175             {
176 0 0         /^FAILED\s+(.*)/ && do {
  0            
177 0           $self->err("Could not complete the handshake: $1");
178 0           return undef;
179             };
180 0 0         /^BADUSER\b/ && do {
181 0           $self->err('Could not complete the handshake: invalid username');
182 0           return undef;
183             };
184 0 0         /^UPTODATE\b/ && do {
185 0           $self->{'md5ch'} = $lines[1];
186 0           $self->{'nexturl'} = $lines[2];
187 0           last SWITCH;
188             };
189 0 0         /^UPDATE\s+(.*)/ && do {
190             # See if we care. (FIXME)
191 0           $self->{'md5ch'} = $lines[1];
192 0           $self->{'nexturl'} = $lines[2];
193 0           last SWITCH;
194             };
195 0           $self->err("Unrecognized handshake response: $_");
196 0           return undef;
197             }
198 0 0         print "RDBG MD5 challenge '$self->{md5ch}', nexturl '$self->{nexturl}'\n"
199             if $self->{'cfg'}{'verbose'};
200 0           return 1;
201             }
202              
203             =item * submit ( info )
204              
205             Submits a single track to the AudioScrobbler API. This method may only
206             be invoked after a successful L. The track information is
207             contained in the hash referenced by the B parameter; the following
208             elements are used:
209              
210             =over 4
211              
212             =item * title
213              
214             The track's title.
215              
216             =item * artist
217              
218             The name of the artist performing the track.
219              
220             =item * length
221              
222             The duration of the track in seconds.
223              
224             =item * album
225              
226             The name of the album (optional).
227              
228             =back
229              
230             Also, the L method requires that the following configuration
231             parameters be set for this C object:
232              
233             =over 4
234              
235             =item * username
236              
237             The username of the user's AudioScrobbler registration.
238              
239             =item * password
240              
241             The password for the AudioScrobbler registration.
242              
243             =back
244              
245             If the submission is successful, the method returns a true value.
246             Otherwise, an appropriate error message may be retrieved via the L
247             method.
248              
249             If the B configuration parameter is set, the L method
250             does not actually submit the track information to the AudioScrobbler API,
251             just simulates a successful submission and returns a true value.
252              
253             If the B configuration parameter is set, the L
254             method reports its progress with diagnostic messages to the standard output.
255              
256             =cut
257              
258             sub submit($ \%)
259             {
260 0     0 1   my ($self, $info) = @_;
261 0           my ($ua, $req, $resp, $s, $c, $datestr, $md5resp);
262 0           my (@t, @lines);
263              
264             # A couple of sanity checks - those never hurt
265 0 0 0       if (!defined($self->{'nexturl'}) || !defined($self->{'md5ch'})) {
266 0           $self->err('Cannot submit without a successful handshake');
267 0           return undef;
268             }
269 0 0 0       if (!defined($info->{'title'}) || !defined($info->{'album'}) ||
      0        
      0        
      0        
270             !defined($info->{'artist'}) || !defined($info->{'length'}) ||
271             $info->{'length'} !~ /^\d+$/) {
272 0           $self->err('Missing or incorrect submission info fields');
273 0           return undef;
274             }
275              
276             # Init...
277 0           @t = gmtime();
278 0           $datestr = sprintf('%04d-%02d-%02d %02d:%02d:%02d',
279             $t[5] + 1900, $t[4] + 1, @t[3, 2, 1, 0]);
280             # Let's hope md5_hex() always returns lowercase hex stuff
281 0           $md5resp = md5_hex(
282             md5_hex($self->{'cfg'}{'password'}).$self->{'md5ch'});
283              
284             # Let's roll?
285 0           $req = HTTP::Request->new('POST', $self->{'nexturl'});
286 0 0         if (!$req) {
287 0           $self->err('Could not create the submission request object');
288 0           return undef;
289             }
290 0           $req->content_type('application/x-www-form-urlencoded; charset="UTF-8"');
291 0           $s = 'u='.URLEncode($self->{'cfg'}{'username'}).
292             "&s=$md5resp&a[0]=".URLEncode($info->{'artist'}).
293             '&t[0]='.URLEncode($info->{'title'}).
294             '&b[0]='.URLEncode($info->{'album'}).
295             '&m[0]='.
296             '&l[0]='.$info->{'length'}.
297             '&i[0]='.URLEncode($datestr).
298             "\r\n";
299 0           $req->content($s);
300 0 0         print "RDBG about to send a submission request:\n".$req->content().
301             "\n===\n" if $self->{'cfg'}{'verbose'};
302 0 0         if ($self->{'cfg'}{'fake'}) {
303 0 0         print "RDBG faking it...\n" if $self->{'cfg'}{'verbose'};
304 0           return 1;
305             }
306              
307 0 0         $ua = $self->get_ua() or return undef;
308 0           $resp = $ua->request($req);
309 0 0         if (!$resp) {
    0          
310 0           $self->err('Could not get a submission response object');
311 0           return undef;
312             } elsif (!$resp->is_success()) {
313 0           $self->err('Could not complete the submission: '.
314             $resp->status_line());
315 0           return undef;
316             }
317 0           $c = $resp->content();
318 0 0         print "RDBG response:\n$c\n===\n" if $self->{'cfg'}{'verbose'};
319 0           @lines = split /[\r\n]+/, $c;
320 0           $_ = $lines[0];
321 0 0         SWITCH:
322             {
323 0           /^OK\b/ && last SWITCH;
324 0 0         /^FAILED\s+(.*)/ && do {
325 0           $self->err("Submission failed: $1");
326 0           return undef;
327             };
328 0 0         /^BADUSER\b/ && do {
329 0           $self->err('Incorrest username or password');
330 0           return undef;
331             };
332 0           $self->err('Unrecognized submission response: '.$_);
333 0           return undef;
334             }
335 0 0         print "RDBG submit() just fine and dandy!\n"
336             if $self->{'cfg'}{'verbose'};
337 0           return 1;
338             }
339              
340             =back
341              
342             There are also several methods and functions for the module's internal
343             use:
344              
345             =over 4
346              
347             =item * get_ua ()
348              
349             Creates or returns the cached C object used by
350             the C class for access to the AudioScrobbler API.
351              
352             =cut
353              
354             sub get_ua($)
355             {
356 0     0 1   my ($self) = @_;
357 0           my ($ua);
358              
359 0   0       $self->{'ua'} ||= new LWP::UserAgent();
360 0 0         if (!$self->{'ua'}) {
361 0           $self->err('Could not create a LWP UserAgent object');
362 0           return undef;
363             }
364 0           $self->{'ua'}->agent('scrobbler-helper/1.0pre1 '.
365             $self->{'ua'}->_agent());
366 0           return $self->{'ua'};
367             }
368              
369             =item * URLDecode (string)
370              
371             Decode a URL-encoded string.
372              
373             Obtained from http://glennf.com/writing/hexadecimal.url.encoding.html
374              
375             =cut
376              
377             sub URLDecode($) {
378 0     0 1   my $theURL = $_[0];
379 0           $theURL =~ tr/+/ /;
380 0           $theURL =~ s/%([a-fA-F0-9]{2,2})/chr(hex($1))/eg;
  0            
381 0           $theURL =~ s///g;
382 0           return $theURL;
383             }
384              
385             =item * URLEncode (string)
386              
387             Return the URL-encoded representation of a string.
388              
389             Obtained from http://glennf.com/writing/hexadecimal.url.encoding.html
390              
391             =cut
392              
393             sub URLEncode($) {
394 0     0 1   my $theURL = $_[0];
395 0           $theURL =~ s/([^a-zA-Z0-9_])/'%' . uc(sprintf("%2.2x",ord($1)));/eg;
  0            
396 0           return $theURL;
397             }
398              
399             =back
400              
401             =head1 TODO
402              
403             =over 4
404              
405             =item *
406              
407             Do something with UPDATE responses to the handshake.
408              
409             =item *
410              
411             Honor INTERVAL in some way.
412              
413             =item *
414              
415             Figure out a way to cache unsuccesful submissions for later retrying.
416              
417             =item *
418              
419             Web services - stats!
420              
421             =back
422              
423             =head1 SEE ALSO
424              
425             B
426              
427             =over 4
428              
429             =item * http://www.last.fm/
430              
431             =item * http://www.audioscrobbler.com/
432              
433             =item * http://www.audioscrobbler.net/
434              
435             =back
436              
437             The home site of the C module is
438             http://devel.ringlet.net/audio/Audio-Scrobbler/
439              
440             =head1 AUTHOR
441              
442             Peter Pentchev, Eroam@ringlet.netE
443              
444             =head1 COPYRIGHT AND LICENSE
445              
446             Copyright (C) 2005, 2006 by Peter Pentchev.
447              
448             This library is free software; you can redistribute it and/or modify
449             it under the same terms as Perl itself, either Perl version 5.8.7 or,
450             at your option, any later version of Perl 5 you may have available.
451              
452             $Id: Scrobbler.pm 88 2006-01-02 09:16:32Z roam $
453              
454             =cut
455              
456             1;