File Coverage

blib/lib/Netx/WebRadio/Station/Shoutcast.pm
Criterion Covered Total %
statement 37 173 21.3
branch 1 76 1.3
condition 1 19 5.2
subroutine 12 21 57.1
pod 6 11 54.5
total 57 300 19.0


line stmt bran cond sub pod time code
1              
2             package Netx::WebRadio::Station::Shoutcast;
3 1     1   22279 use strict;
  1         2  
  1         30  
4 1     1   4 use warnings;
  1         3  
  1         21  
5 1     1   6 use Carp;
  1         2  
  1         95  
6              
7             BEGIN {
8             #use Exporter ();
9 1     1   4 use vars qw ($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
  1         2  
  1         90  
10 1     1   39125 $VERSION = 0.03;
11             #@ISA = qw (Exporter);
12              
13             #Give a hoot don't pollute, do not export more than needed by default
14             #@EXPORT = qw ();
15             #@EXPORT_OK = qw ();
16             #%EXPORT_TAGS = ();
17              
18 1     1   4 use strict;
  1         1  
  1         30  
19 1     1   728 use IO::Socket;
  1         760972  
  1         9  
20 1     1   2527 use IO::Poll 0.04 qw(POLLIN POLLOUT POLLERR POLLHUP);
  1         1486  
  1         115  
21 1     1   10 use Errno qw(EAGAIN EINPROGRESS);
  1         2  
  1         155  
22              
23             use Class::MethodMaker
24 1         9 new_with_init => 'new',
25             get_set =>
26 1     1   881 [qw /pollmode host port socket blocking path useragent stationname/];
  1         18244  
27              
28 1         2043 %Netx::WebRadio::Station::Shoutcast::pollmodes = (
29             START => undef,
30             CONNECT => POLLOUT,
31             SENDHEADER => POLLOUT,
32             READHEADER => POLLIN,
33             MDSYNC => POLLIN,
34             MDNOSYNC => POLLIN,
35             READMD => POLLIN
36             );
37             }
38              
39             =head1 NAME
40              
41             Netx::WebRadio::Station::Shoutcast - receive one shoutcast-stream
42              
43             =head1 SYNOPSIS
44              
45             see Netx::WebRadio
46            
47              
48             =head1 DESCRIPTION
49              
50             Netx::WebRadio::Station::Shoutcast-objects can be used with Netx::WebRadio to receive a Shoutcast-stream.
51              
52             =head1 USAGE
53              
54             You can overload some methods to change the behaviour of the module.
55              
56             The default implementation does not process the received mp3-data in any way.
57             Overload some of the methods to process the sound-data.
58              
59             =head1 METHODS
60              
61             =head2 host
62              
63             Usage : $station->host( $host )
64             Purpose : set the hostname of the server
65             Returns : the actual hostname if called without arguments
66             Argument : hostname
67             Throws : nothing
68             See Also :
69              
70             =head2 port
71              
72             Usage : $station->port( $port )
73             Purpose : set the port of the server
74             Returns : the actual port if called without arguments
75             Argument : portnumber
76             Throws : nothing
77             See Also :
78              
79             =head2 path
80              
81             Usage : $station->path( $path )
82             Purpose : set the path of the stream on the server
83             Returns : the actual path if called without arguments
84             Argument : path
85             Throws : nothing
86             See Also :
87              
88             =head2 useragent
89              
90             Usage : $station->useragent( 'Winamp...' )
91             Purpose : set the useragent. the value is sent to the server on connect
92             Returns : the actual useragent if called without arguments
93             Argument : useragent-string
94             Throws : nothing
95             See Also :
96              
97              
98             =head2 stationname
99              
100             Usage : $stationname = $station->stationname()
101             Purpose : get the stationname
102             Returns : the stationname
103             Argument : nothing
104             Throws : nothing
105             See Also :
106              
107             =head2 receive
108              
109             Usage : $station->receive( )
110             Purpose :
111             Receives next chunk from the station.
112             You have to call it everytime the socket is ready for the next operation.
113             This is done from Netx::WebRadio in most cases.
114             Returns : 1 for 'ok', other values can be specified in the overloadable method 'disconnected'
115             Argument : nothing
116             Throws : nothing
117             See Also :
118              
119             =cut
120              
121             sub receive {
122 0     0 1 0 my $self = shift;
123 0         0 my $socket = $self->socket;
124              
125 0 0       0 if ( $self->get_state eq 'START' ) {
126 0         0 croak "call connect first\n";
127             }
128              
129 0 0       0 if ( $self->get_state eq 'CONNECT' ) {
130 0 0       0 if ( $socket->connected ) {
131 0         0 $self->set_state('SENDHEADER');
132             }
133             else {
134 0         0 return $self->disconnected();
135             }
136 0         0 return 1;
137             }
138              
139 0 0       0 if ( $self->get_state eq 'SENDHEADER' ) {
140 0   0     0 my $path = $self->path || '/';
141 0   0     0 my $header = $self->{_header}
142             || "GET $path HTTP/1.0\r\n" . "Host:"
143             . $self->host() . "\r\n"
144             . "Accept:*/*\r\n"
145             . "User-Agent:"
146             . $self->useragent() . "\r\n"
147             . "Icy-Metadata:1\r\n\r\n";
148 0         0 my $bytes = $self->icySyswrite( $socket, $header );
149 0 0       0 unless ($bytes) {
150 0 0       0 return 1 if $! == EAGAIN;
151 0         0 return $self->disconnected();
152             }
153 0         0 substr( $header, 0, $bytes ) = '';
154 0 0       0 unless ($header) {
155 0         0 $self->set_state('READHEADER');
156             }
157 0         0 $self->{_header} = $header;
158 0 0       0 $self->{_header} || delete $self->{_header};
159 0         0 return 1;
160             }
161              
162 0 0       0 if ( $self->get_state eq 'READHEADER' ) {
163 0         0 my $in;
164 0         0 $self->{_metaLength} = 0;
165 0         0 my $tLength = $self->icySysread( $socket, $in, 1024 );
166 0 0       0 unless ( defined $tLength ) {
167 0         0 return $self->disconnected();
168             }
169 0   0     0 my $tempHeader = $self->{_tempHeader} || '';
170 0         0 $in = $tempHeader . $in;
171              
172 0 0       0 if ( $in =~ /\r\n\r\n/ ) {
173              
174             # header complete
175 0         0 $self->{_audio} .= $'; # post-match
176 0         0 my $header = $`;
177 0 0       0 if ( $header =~ /icy-metaint:\s*(\d*)\r\n/i ) {
178 0         0 $self->{_metaLength} = $1;
179             }
180             else {
181 0         0 croak "no length-information in MetaData\n";
182             }
183 0         0 for my $line ( split /\r\n/, $header ) {
184 0 0       0 if ( $line =~ /^icy/ ) {
185 0         0 my ( $name, $value ) = $line =~ /(icy.*?):(.*)/i;
186 0         0 $self->{ '_' . $name } = $value;
187 0 0       0 $self->stationname($value) if $name eq 'icy-name';
188             }
189             }
190 0         0 $self->set_state('MDNOSYNC');
191              
192             # header complete
193             # header ist in $header
194             }
195             else {
196 0         0 $self->{_tempHeader} = $in;
197             }
198 0         0 return 1;
199             }
200 0 0       0 if ( $self->get_state eq 'MDNOSYNC' ) { # metaData out of sync
201             #print "metadaten aus dem takt: " . $self->stationname() . "\n";
202 0         0 my $in;
203 0         0 my $restLength = 0;
204 0         0 my $tLength = $self->icySysread( $socket, $in, $self->{_metaLength} );
205 0 0       0 unless ( defined $tLength ) {
206 0         0 return $self->disconnected();
207             }
208              
209 0         0 $self->{_audio} .= $in;
210 0 0       0 if ( $self->{_audio} =~ /Stream(.|\n)*\0/i ) {
211 0         0 $self->{_audio} =~ /Stream/i;
212 0         0 my $lastMatch = $-[0];
213 0         0 my $stringPre = $`;
214 0         0 my $stringPost = $';
215              
216 0         0 my $lM = ord( chop($stringPre) ) * 16;
217 0         0 my $metaData = "Stream" . substr( $stringPost, 0, $lM - 6 );
218 0         0 my $mreturn = $self->processMetaData($metaData);
219 0 0       0 if ($mreturn) {
220 0         0 $self->set_state('MDSYNC');
221             }
222              
223 0         0 my $rest = substr( $stringPost, $lM - 6 );
224 0         0 $self->{_audio} = $stringPre;
225 0         0 $self->{_audio} .= $rest;
226 0         0 $restLength = $self->{_metaLength} - length($rest) + 1;
227 0         0 $self->process_chunk( $self->{_audio} );
228             }
229             else {
230 0         0 $self->{_audio} .= $in;
231             }
232 0         0 $self->{_restLength} = $restLength;
233 0         0 return 1;
234             }
235              
236 0 0       0 if ( $self->get_state eq 'MDSYNC' ) {
237 0         0 my $in;
238 0   0     0 $self->{_restLength} ||= ( $self->{_metaLength} + 1 );
239 0         0 my $realLength =
240             $self->icySysread( $socket, $in, $self->{_restLength} );
241 0 0       0 unless ( defined $realLength ) {
242 0         0 return $self->disconnected();
243             }
244              
245 0         0 $self->{_restLength} -= $realLength;
246              
247 0 0       0 if ( $self->{_restLength} == 0 ) {
248 0         0 my $l = chop($in);
249 0 0       0 $self->process_chunk($in) if $in;
250 0 0       0 $self->set_state('READMD') if ord($l);
251 0         0 $self->{_newMetaDataLength} = $l;
252             }
253             else {
254 0 0       0 $self->process_chunk($in) if $in;
255             }
256 0         0 return 1;
257             }
258              
259 0 0       0 if ( $self->get_state eq 'READMD' ) {
260 0         0 my $in;
261 0   0     0 $self->{_newMetaDataLength} ||= 0;
262 0   0     0 $self->{_newMetaData} ||= '';
263 0         0 my $lengthInBytes =
264             ( ord( $self->{_newMetaDataLength} ) * 16 ) -
265             length( $self->{_newMetaData} );
266 0         0 my $realLength = $self->icySysread( $socket, $in, $lengthInBytes );
267 0 0       0 unless ( defined $realLength ) {
268 0         0 return $self->disconnected();
269             }
270              
271 0         0 $self->{_newMetaData} .= $in;
272              
273 0 0       0 if ( $realLength < $lengthInBytes ) { return 1 }
  0         0  
274              
275 0         0 my $metaData = 1;
276 0 0       0 $metaData = $self->processMetaData( $self->{_newMetaData} )
277             if $self->{_newMetaData};
278 0         0 $self->{_newMetaData} = '';
279              
280 0 0       0 if ($metaData) {
281 0         0 $self->set_state('MDSYNC');
282             }
283             else {
284 0         0 $self->set_state('MDNOSYNC');
285              
286             }
287 0         0 return 1;
288             }
289             }
290              
291             =head2 connect
292              
293             Usage : $station->connect( $host, $port );
294             Purpose :
295             connects the station-object with the radio-station
296             Returns : 1 for 'ok',other values can be specified in the overloadable method 'disconnected'
297             Argument : host, port
298             Throws : nothing
299             See Also :
300              
301             =cut
302              
303             sub connect {
304 0     0 1 0 my ( $self, $host, $port ) = @_;
305              
306 0 0       0 $self->host($host) if $host;
307 0 0       0 $self->port($port) if $port;
308              
309 0 0 0     0 croak "need more information to connect" unless $self->port && $self->host;
310              
311 0         0 my $addr = sockaddr_in( $port, inet_aton($host) );
312 0 0       0 if ( $self->socket->connect($addr) ) {
313 0         0 $self->set_state('SENDHEADER');
314 0         0 return 1;
315             }
316             else {
317 0 0       0 if ( $! == EINPROGRESS ) {
318 0         0 $self->set_state('CONNECT');
319             }
320             else {
321 0         0 $self->disconnected();
322 0         0 return 0;
323             }
324             }
325 0         0 return 1;
326             }
327             =pod
328             The following functions are overloadable:
329              
330             =head2 init
331              
332             Usage : init is called from new
333             Purpose :
334             Initializes some values, create socket
335             Always call SUPER::init if you overload this method.
336             Returns : nothing
337             Argument : nothing
338             Throws : nothing
339             See Also :
340              
341             =cut
342              
343             sub init {
344 1     1 1 57 my $self = shift;
345              
346 1         5 $self->set_state('START');
347              
348 1 50       22 my $socket = IO::Socket::INET->new(
349             Proto => 'tcp',
350             Type => SOCK_STREAM
351             )
352             or die $@;
353              
354 1   50     279 $socket->blocking( $self->blocking || 0 );
355              
356 1         60 $self->socket($socket);
357             }
358              
359             =head2 process_chunk
360              
361             Usage : process_chunk is called from receive() for processing audio-data-chunks
362             Purpose :
363             overload it
364             Returns : nothing
365             Argument : audio-data
366             Throws : nothing
367             See Also :
368              
369             =cut
370              
371             sub process_chunk {
372 0     0 1 0 my ( $self, $chunk ) = @_;
373             }
374              
375             =head2 process_new_title
376              
377             Usage : process_new_title is called everytime the station sends a new song-title
378             Purpose :
379             overload it
380             Returns : nothing
381             Argument : new song title
382             Throws : nothing
383             See Also :
384              
385             =cut
386              
387             sub process_new_title {
388 0     0 1 0 my ( $self, $title ) = @_;
389 0         0 print $title, "\n";
390             }
391              
392             =head2 disconnected
393              
394             Usage : is called when there is a write error on a socket
395             Purpose :
396             overload it.
397             The return value of this method is the value the failed method will return.
398             If you can 'fix' the error in this method you normaly return 1, otherwise 0.
399             You can also change the behaviour of Netx::WebRadio for a '0' return value.
400             Returns : what you want
401             Argument : nothing
402             Throws : nothing
403             See Also :
404              
405             =cut
406              
407             sub disconnected {
408 0     0 1 0 my $self = shift;
409 0         0 warn "disconnected " . (caller)[0] . " " . (caller)[2] . "\n";
410 0         0 return 0;
411             }
412              
413             =head1 BUGS
414              
415             =over 2
416              
417             =item 1
418             doesn't work under Win32
419              
420             =item 2
421             only works with stations that transmit metdata
422              
423             =back
424              
425             =head1 SUPPORT
426              
427              
428              
429             =head1 AUTHOR
430              
431             Nathanael Obermayer
432             CPAN ID: nathanael
433             natom-pause@smi2le.net
434              
435             =head1 COPYRIGHT
436              
437             This program is free software; you can redistribute
438             it and/or modify it under the same terms as Perl itself.
439              
440             The full text of the license can be found in the
441             LICENSE file included with this module.
442              
443              
444             =head1 SEE ALSO
445              
446             perl(1).
447             Netx::WebRadio
448              
449             =cut
450              
451             sub set_state {
452 1     1 0 1 my ( $self, $state ) = @_;
453 1         7 $self->{_state} = $state;
454 1         29 $self->pollmode( $Netx::WebRadio::Station::Shoutcast::pollmodes{$state} );
455             }
456              
457             sub get_state {
458 0     0 0   my $self = shift;
459 0           return $self->{_state};
460             }
461              
462             sub icySysread {
463 0     0 0   my ( $self, $socket, $in, $length ) = @_;
464 0           my $ret = sysread( $socket, $_[2], $length );
465 0 0         unless ( defined $ret ) {
466 0 0         if ( $! == EAGAIN ) {
467 0           return 0;
468             }
469 0           return undef;
470             }
471 0           return $ret;
472             }
473              
474             sub processMetaData {
475 0     0 0   my ( $self, $text ) = @_;
476 0           my ($title) = $text =~ /StreamTitle='(.*?)'/i;
477 0 0         return 0 unless $title;
478 0           $self->process_new_title($title);
479 0           return 1;
480             }
481              
482             sub icySyswrite {
483 0     0 0   my ($self, $socket, $string) = @_;
484 0           return syswrite($socket, $string);
485             }
486              
487             1; #this line is important and will help the module return a true value
488             __END__