File Coverage

blib/lib/WWW/TinySong.pm
Criterion Covered Total %
statement 59 113 52.2
branch 14 64 21.8
condition 7 47 14.8
subroutine 16 22 72.7
pod 11 11 100.0
total 107 257 41.6


line stmt bran cond sub pod time code
1             package WWW::TinySong;
2              
3             =head1 NAME
4              
5             WWW::TinySong - Get free music links from tinysong.com
6              
7             =head1 SYNOPSIS
8              
9             # basic use
10              
11             use WWW::TinySong qw(search);
12              
13             for(search("we are the champions")) {
14             printf("%s", $_->{songName});
15             printf(" by %s", $_->{artistName});
16             printf(" on %s", $_->{albumName}) if $_->{albumName};
17             printf(" <%s>\n", $_->{tinysongLink});
18             }
19              
20             # customize the user agent
21              
22             use LWP::UserAgent;
23              
24             my $ua = new LWP::UserAgent;
25             $ua->timeout(10);
26             $ua->env_proxy;
27              
28             WWW::TinySong->ua($ua);
29              
30             # customize the service
31              
32             WWW::TinySong->service('http://tinysong.com/');
33              
34             # tolerate some server errors
35              
36             WWW::TinySong->retries(5);
37              
38             =head1 DESCRIPTION
39              
40             tinysong.com is a web app that can be queried for a song and returns a tiny
41             URL, allowing you to listen to the song for free online and share it with
42             friends. L is a Perl interface to this service, allowing you
43             to programmatically search its underlying database.
44              
45             =cut
46              
47 1     1   37193 use 5.006;
  1         4  
  1         51  
48 1     1   6 use strict;
  1         2  
  1         34  
49 1     1   11 use warnings;
  1         2  
  1         27  
50              
51 1     1   5 use Carp;
  1         1  
  1         121  
52 1     1   5 use Exporter;
  1         2  
  1         31  
53 1     1   29009 use CGI;
  1         23089  
  1         8  
54 1     1   1165 use HTML::Parser;
  1         10223  
  1         1957  
55              
56             our @EXPORT_OK = qw(link search);
57             our @ISA = qw(Exporter);
58             our $VERSION = '1.01';
59              
60             my($ua, $service, $retries);
61              
62             =head1 FUNCTIONS
63              
64             The do-it-all function is C. If you just want a tiny URL, use C.
65             These two functions may be Ced and used like any other function.
66             C and C are provided so that you can (hopefully) continue to use
67             this module if the tinysong.com API is extended and I'm too lazy or busy to
68             update, but you will probably not need to use them otherwise. The other public
69             functions are either aliases for one of the above or created to allow the
70             customization of requests issued by this module.
71              
72             =over 4
73              
74             =item link( $SEARCH_TERMS )
75              
76             =item WWW::TinySong->link( $SEARCH_TERMS )
77              
78             =cut
79              
80             sub link {
81 2 50 33 2 1 10566 unshift @_, __PACKAGE__ # add the package name unless already there
82             unless defined($_[0]) && UNIVERSAL::isa($_[0], __PACKAGE__);
83 2         14 return shift->a(@_);
84             }
85              
86             =item WWW::TinySong->a( $SEARCH_TERMS )
87              
88             Returns the short URL corresponding to the top result of searching with the
89             specified song and artist name terms or C if no song was found.
90              
91             =cut
92              
93             sub a {
94 2     2 1 6 my($pkg, $search_terms) = @_;
95 2         9 my $ret = $pkg->call('a', $search_terms);
96 2         9329 $ret =~ s/\s+//g;
97 2 50       19 return $ret =~ /^NSF;?$/ ? undef : $ret;
98             }
99              
100             =item search( $SEARCH_TERMS [, $LIMIT ] )
101              
102             =item WWW::TinySong->search( $SEARCH_TERMS [, $LIMIT ] )
103              
104             =cut
105              
106             sub search {
107 0 0 0 0 1 0 unshift @_, __PACKAGE__ # add the package name unless already there
108             unless defined($_[0]) && UNIVERSAL::isa($_[0], __PACKAGE__);
109 0         0 return shift->s(@_);
110             }
111              
112             =item WWW::TinySong->s( $SEARCH_TERMS [, $LIMIT ] )
113              
114             Searches for the specified song and artist name terms, giving up to $LIMIT
115             results. $LIMIT defaults to 10 if not C. Returns an array in list
116             context or the top result in scalar context. Return elements are hashrefs with
117             keys C
118             groovesharkLink)> as given by C. Here's a quick script to demonstrate:
119              
120             #!/usr/bin/perl
121              
122             use WWW::TinySong qw(search);
123             use Data::Dumper;
124              
125             print Dumper search("three little birds", 3);
126              
127             ...and its output on my system at the time of this writing:
128              
129             $VAR1 = {
130             'artistName' => 'Bob Marley',
131             'albumName' => 'Legend',
132             'songName' => 'Three Little Birds',
133             'artistID' => '139',
134             'tinysongLink' => 'http://tinysong.com/eg9',
135             'songID' => '1302',
136             'albumID' => '97291',
137             'groovesharkLink' => 'http://listen.grooveshark.com/song/Three_Little_Birds/1302'
138             };
139             $VAR2 = {
140             'artistName' => 'Bob Marley',
141             'albumName' => 'One Love: The Very Best Of Bob Marley & The Wailers',
142             'songName' => 'Three Little Birds',
143             'artistID' => '139',
144             'tinysongLink' => 'http://tinysong.com/lf2',
145             'songID' => '3928811',
146             'albumID' => '221021',
147             'groovesharkLink' => 'http://listen.grooveshark.com/song/Three_Little_Birds/3928811'
148             };
149             $VAR3 = {
150             'artistName' => 'Bob Marley & The Wailers',
151             'albumName' => 'Exodus',
152             'songName' => 'Three Little Birds',
153             'artistID' => '848',
154             'tinysongLink' => 'http://tinysong.com/egc',
155             'songID' => '3700',
156             'albumID' => '2397306',
157             'groovesharkLink' => 'http://listen.grooveshark.com/song/Three_Little_Birds/3700'
158             };
159              
160             =cut
161              
162             sub s {
163 0     0 1 0 my($pkg, $search_terms, $limit) = @_;
164              
165 0 0       0 if(wantarray) {
166 0 0       0 $limit = 10 unless defined $limit;
167             }
168             else {
169 0         0 $limit = 1; # no point in searching for more if only one is needed
170             }
171            
172 0         0 my @ret = $pkg->parse($pkg->call('s', $search_terms,
173             {limit => $limit}));
174              
175 0 0       0 return wantarray ? @ret : $ret[0];
176             }
177              
178             =item WWW::TinySong->b( $SEARCH_TERMS )
179              
180             Searches for the specified song and artist name terms, giving the top result.
181             I'm not really sure why this is part of the API because the same result can be
182             obtained by limiting a C to one result, but it's included here for
183             completeness.
184              
185             =cut
186              
187             sub b {
188 1     1 1 928 my($pkg, $search_terms) = @_;
189 1         7 return ($pkg->parse($pkg->call('b', $search_terms)))[0];
190             }
191              
192             =item WWW::TinySong->call( $METHOD , $SEARCH_TERMS [, \%EXTRA_PARAMS ] )
193              
194             Calls API "method" $METHOD using the specified $SEARCH_TERMS and optional
195             hashref of extra parameters. Whitespace sequences in $SEARCH_TERMS will be
196             converted to pluses. Returns the entire response as a string. Unless you're
197             just grabbing a link, you will probably want to pass the result through
198             C.
199              
200             =cut
201              
202             sub call {
203 3     3 1 8 my($pkg, $method, $search_terms, $param) = @_;
204 3 50       14 croak 'Empty method not allowed' unless length($method);
205              
206 3         21 $search_terms =~ s/[\s\+]+/+/g;
207 3         9 $search_terms =~ s/^\+//;
208 3         7 $search_terms =~ s/\+$//;
209 3 50       10 croak 'Empty search terms not allowed' unless length($search_terms);
210 3         16 my $url = join('/', $pkg->service, CGI::escape($method), $search_terms);
211              
212 3   50     56 $param ||= {};
213 0         0 $param = join('&', map
214 3         13 { sprintf('%s=%s', CGI::escape($_), CGI::escape($param->{$_})) }
215             keys %$param);
216 3 50       33 $url .= "?$param" if $param;
217              
218 3         12 return $pkg->_get($url);
219             }
220              
221             =item WWW::TinySong->parse( [ @RESULTS ] )
222              
223             Parses all the lines in the given list of results according to the specs,
224             building and returning a (possibly empty) list of hashrefs with the keys
225             C
226             groovesharkLink)>, whose meanings are hopefully self-explanatory.
227              
228             =cut
229              
230             sub parse {
231 1     1 1 512 my $pkg = shift;
232 1 50       773 return map {
233 1         5 /^(http:\/\/.*); (\d*); (.*); (\d*); (.*); (\d*); (.*); (http:\/\/.*)$/
234             or croak 'Result in unexpected format';
235             {
236 0         0 tinysongLink => $1,
237             songID => $2,
238             songName => $3,
239             artistID => $4,
240             artistName => $5,
241             albumID => $6,
242             albumName => $7,
243             groovesharkLink => $8,
244             }
245 1         3 } grep { !/^NSF;\s*$/ } map {chomp; split(/\n/, $_)} @_;
  1         4  
  1         6  
246             }
247              
248             =item WWW::TinySong->scrape( $QUERY_STRING [, $LIMIT ] )
249              
250             Searches for $QUERY_STRING by scraping, giving up to $LIMIT results. $LIMIT
251             defaults to 10 if not C. Returns an array in list context or the
252             top result in scalar context. Return elements are hashrefs with keys
253             C. Their values will be the
254             empty string if not given by the website. As an example, executing:
255              
256             #!/usr/bin/perl
257            
258             use WWW::TinySong;
259             use Data::Dumper;
260            
261             print Dumper(WWW::TinySong->scrape("we can work it out", 3));
262              
263             ...prints something like:
264              
265             $VAR1 = {
266             'artistName' => 'The Beatles',
267             'tinysongLink' => 'http://tinysong.com/5Ym',
268             'songName' => 'We Can Work It Out',
269             'albumName' => 'The Beatles 1'
270             };
271             $VAR2 = {
272             'artistName' => 'The Beatles',
273             'tinysongLink' => 'http://tinysong.com/uLd',
274             'songName' => 'We Can Work It Out',
275             'albumName' => 'We Can Work It Out / Day Tripper'
276             };
277             $VAR3 = {
278             'artistName' => 'The Beatles',
279             'tinysongLink' => 'http://tinysong.com/2EaX',
280             'songName' => 'We Can Work It Out',
281             'albumName' => 'The Beatles 1967-70'
282             };
283              
284             This function is how the primary functionality of the module was implemented in
285             the 0.0x series. It remains here as a tribute to the past, but should be
286             avoided because scraping depends on the details of the response HTML, which may
287             change at any time (and in fact did at some point between versions 0.05 and
288             0.06). Interestingly, this function does currently have one advantage over the
289             robust alternative: whereas C is limited to a maximum of 32 results by
290             the web service, scraping doesn't seem to be subjected to this requirement.
291              
292             =cut
293              
294             sub scrape {
295 0     0 1 0 my($pkg, $query_string, $limit) = @_;
296 0 0       0 if(wantarray) {
297 0 0       0 $limit = 10 unless defined $limit;
298             }
299             else {
300 0         0 $limit = 1; # no point in searching for more if only one is needed
301             }
302              
303 0         0 my $service = $pkg->service;
304              
305 0         0 my $response = $pkg->_get(sprintf('%s?s=%s&limit=%d', $service,
306             CGI::escape($query_string), $limit));
307              
308 0         0 my @ret = ();
309 0         0 my $inside_list = 0;
310 0         0 my $current_class = undef;
311              
312             my $start_h = sub {
313 0     0   0 my $tagname = lc(shift);
314 0         0 my $attr = shift;
315 0 0 0     0 if( $tagname eq 'ul'
    0 0        
316             && defined($attr->{id})
317             && lc($attr->{id}) eq 'results')
318             {
319 0         0 $inside_list = 1;
320             }
321             elsif($inside_list) {
322 0 0 0     0 if($tagname eq 'span') {
    0          
323 0         0 my $class = $attr->{class};
324 0 0 0     0 if( defined($class)
325             && $class =~ /^(?:album|artist|song title)$/i) {
326 0         0 $current_class = lc $class;
327 0 0 0     0 croak 'Unexpected results while parsing HTML'
328             if !@ret || defined($ret[$#ret]->{$current_class});
329             }
330             }
331             elsif($tagname eq 'a' && $attr->{class} eq 'link') {
332 0         0 my $href = $attr->{href};
333 0 0       0 croak 'Bad song link' unless defined $href;
334 0 0       0 croak 'Song link doesn\'t seem to match service'
335             unless substr($href, 0, length($service)) eq $service;
336 0         0 push @ret, {tinysongLink => $href};
337             }
338             }
339 0         0 };
340              
341             my $text_h = sub {
342 0 0 0 0   0 return unless $inside_list && $current_class;
343 0         0 my $text = shift;
344 0         0 $ret[$#ret]->{$current_class} = $text;
345 0         0 undef $current_class;
346 0         0 };
347              
348             my $end_h = sub {
349 0 0   0   0 return unless $inside_list;
350 0         0 my $tagname = lc(shift);
351 0 0       0 if($tagname eq 'ul') {
    0          
352 0         0 $inside_list = 0;
353             }
354             elsif($tagname eq 'span') {
355 0         0 undef $current_class;
356             }
357 0         0 };
358              
359 0         0 my $parser = HTML::Parser->new(
360             api_version => 3,
361             start_h => [$start_h, 'tagname, attr'],
362             text_h => [$text_h, 'text'],
363             end_h => [$end_h, 'tagname'],
364             marked_sections => 1,
365             );
366 0         0 $parser->parse($response);
367 0         0 $parser->eof;
368              
369 0         0 for my $res (@ret) {
370 0   0     0 $res = {
      0        
      0        
      0        
371             albumName => $res->{album} || '',
372             artistName => $res->{artist} || '',
373             songName => $res->{'song title'} || '',
374             tinysongLink => $res->{tinysongLink} || '',
375             };
376 0         0 $res->{albumName} =~ s/^\s+on\s//;
377 0         0 $res->{artistName} =~ s/^\s+by\s//;
378             }
379              
380 0 0       0 return wantarray ? @ret : $ret[0];
381             }
382              
383             =item WWW::TinySong->ua( [ $USER_AGENT ] )
384              
385             Returns the user agent object used by this module for web retrievals, first
386             setting it to $USER_AGENT if it's specified. Defaults to a C
387             L. If you explicitly set this, you don't have to use a
388             LWP::UserAgent, it may be anything that can C a URL and return a
389             response object.
390              
391             =cut
392              
393             sub ua {
394 4 50   4 1 42 if($_[1]) {
    100          
395 0         0 $ua = $_[1];
396             }
397             elsif(!$ua) {
398 1         3 eval {
399 1         1231 require LWP::UserAgent;
400 1         48597 $ua = new LWP::UserAgent;
401             };
402 1 50       3436 carp 'Problem setting user agent' if $@;
403             }
404 4         29 return $ua;
405             }
406              
407             =item WWW::TinySong->service( [ $URL ] )
408              
409             Returns the web address of the service used by this module, first setting
410             it to $URL if it's specified. Defaults to .
411              
412             =cut
413              
414             sub service {
415 4 50 100 4 1 59 return $service = $_[1] ? $_[1] : $service || 'http://tinysong.com/';
416             }
417              
418             =item WWW::TinySong->retries( [ $COUNT ] )
419              
420             Returns the number of consecutive internal server errors the module will ignore
421             before failing, first setting it to $COUNT if it's specified. Defaults to 0
422             (croak, do not retry in case of internal server error). This was created
423             because read timeouts seem to be a common problem with the web service. The
424             module now provides the option of doing something more useful than immediately
425             failing.
426              
427             =cut
428              
429             sub retries {
430 5 100 100 5 1 40 return $retries = $_[1] ? $_[1] : $retries || 0;
431             }
432              
433             =back
434              
435             =cut
436              
437             ################################################################################
438              
439             sub _get {
440 3     3   8 my($response, $pkg, $url) = (undef, @_);
441 3         14 for(0..$pkg->retries) {
442 3         11 $response = $pkg->ua->get($url);
443 3 50       889562 last if $response->is_success;
444 0 0 0     0 croak $response->message || $response->status_line
      0        
445             if $response->is_error && $response->code != 500;
446             }
447 3   33     72 return $response->decoded_content || $response->content;
448             }
449              
450             1;
451              
452             __END__