File Coverage

blib/lib/MP3/Icecast.pm
Criterion Covered Total %
statement 56 200 28.0
branch 12 78 15.3
condition 3 46 6.5
subroutine 13 27 48.1
pod 15 15 100.0
total 99 366 27.0


line stmt bran cond sub pod time code
1             package MP3::Icecast;
2              
3             =head1 NAME
4              
5             MP3::Icecast - Generate Icecast streams, as well as M3U and PLSv2 playlists.
6              
7             =head1 SYNOPSIS
8              
9             use MP3::Icecast;
10             use MP3::Info;
11             use IO::Socket;
12              
13              
14             my $listen_socket = IO::Socket::INET->new(
15             LocalPort => 8000, #standard Icecast port
16             Listen => 20,
17             Proto => 'tcp',
18             Reuse => 1,
19             Timeout => 3600);
20              
21             #create an instance to find all files below /usr/local/mp3
22             my $finder = MP3::Icecast->new();
23             $finder->recursive(1);
24             $finder->add_directory('/usr/local/mp3');
25             my @files = $finder->files;
26              
27             #accept TCP 8000 connections
28             while(1){
29             next unless my $connection = $listen_socket->accept;
30              
31             defined(my $child = fork()) or die "Can't fork: $!";
32             if($child == 0){
33             $listen_socket->close;
34              
35             my $icy = MP3::Icecast->new;
36              
37             #stream files that have an ID3 genre tag of "jazz"
38             while(@files){
39             my $file = shift @files;
40             my $info = new MP3::Info $file;
41             next unless $info;
42             next unless $info->genre =~ /jazz/i;
43             $icy->stream($file,0,$connection);
44             }
45             exit 0;
46             }
47              
48             #a contrived example to demonstrate that MP3::Icecast
49             #can generate M3U and PLSv2 media playlists.
50             print STDERR $icy->m3u, "\n";
51             print STDERR $icy->pls, "\n";
52              
53             $connection->close;
54             }
55              
56              
57             =head1 ABSTRACT
58              
59             MP3::Icecast supports streaming Icecast protocol over socket
60             or other filehandle (including STDIN). This is useful for writing
61             a streaming media server.
62              
63             MP3::Icecast also includes support for generating M3U and PLSv2
64             playlist files. These are common formats supported by most modern
65             media players, including XMMS, Windows Media Player 9, and Winamp.
66              
67             =head1 SEE ALSO
68              
69             The Icecast project
70             http://www.icecast.org
71              
72             Namp! (Apache::MP3)
73             http://namp.sourceforge.net
74              
75             Unofficial M3U and PLS specifications
76             http://forums.winamp.com/showthread.php?threadid=65772
77              
78             =head1 AUTHOR
79              
80             Allen Day, Eallenday@ucla.eduE
81              
82             =head1 COPYRIGHT AND LICENSE
83              
84             Copyright 2003, Allen Day
85              
86             This library is free software; you can redistribute it and/or modify
87             it under the same terms as Perl itself.
88              
89             =cut
90              
91 1     1   26098 use strict;
  1         3  
  1         267  
92 1     1   7 use File::Spec;
  1         2  
  1         31  
93 1     1   6 use File::Basename 'dirname','basename','fileparse';
  1         7  
  1         144  
94 1     1   937 use URI::Escape;
  1         1533  
  1         68  
95 1     1   1754 use IO::File;
  1         12006  
  1         145  
96 1     1   1270 use MP3::Info;
  1         71140  
  1         110  
97              
98 1     1   10 use constant DEBUG => 0;
  1         3  
  1         3238  
99              
100             our $VERSION = '0.02';
101              
102             our %AUDIO = (
103             '.mp3' => 'audio/x-mp3',
104             );
105             our %FORMAT_FIELDS = (
106             a => 'artist',
107             c => 'comment',
108             d => 'duration',
109             f => 'filename',
110             g => 'genre',
111             l => 'album',
112             m => 'min',
113             n => 'track',
114             q => 'samplerate',
115             r => 'bitrate',
116             s => 'sec',
117             S => 'seconds',
118             t => 'title',
119             y => 'year',
120             );
121              
122              
123             our $CRLF = "\015\012";
124              
125             =head2 new
126              
127             Title : new
128             Usage : $icy = MP3::Icecast->new(%arg);
129             Function: create a new MP3::Icecast instance
130             Returns : an MP3::Icecast object
131             Args : none
132              
133              
134             =cut
135              
136             sub new{
137 1     1 1 12 my($class,%arg) = @_;
138              
139 1         4 my $self = bless {}, $class;
140              
141 1         3 return $self;
142             }
143              
144             =head2 add_directory
145              
146             Title : add_directory
147             Usage : $icy->add_directory('/usr/local/mp3');
148             Function: add a directory of files to be added to the playlist
149             Returns : true on success, false on failure
150             Args : a system path
151              
152              
153             =cut
154              
155             sub add_directory{
156 1     1 1 6 my ($self,$dir) = @_;
157 1         1 warn "adding directory $dir" if DEBUG;
158 1 50 33     41 if(!-d $dir or !-r $dir){
159 0         0 return undef;
160             } else {
161 1         6 $self->_process_directory($dir);
162 1         3 return 1;
163             }
164             }
165              
166             =head2 _process_directory
167              
168             Title : _process_directory
169             Usage : $icy->_process_directory('/usr/local/mp3');
170             Function: searches a directory for files to add to the playlist
171             Returns : true on success
172             Args : a system path to search for files
173              
174              
175             =cut
176              
177             sub _process_directory{
178 1     1   3 my ($self,$dir) = @_;
179              
180 1 50       13 if(!-r $dir){
181 0         0 return undef;
182             } else {
183 1         2 warn "processing directory: $dir" if DEBUG;
184              
185 1 50       53 opendir(my $d, $dir) or die "couldn't opendir($dir): $!";
186 1 100       21 my @dirents = grep {$_ ne '.' and $_ ne '..'} readdir($d);
  4         22  
187 1 50       17 closedir($d) or die "couldn't closedir($dir): $!";
188              
189 1         3 foreach my $dirent (@dirents){
190 2         4 warn "found dirent: $dirent" if DEBUG;
191              
192 2 50       66 next if !-r File::Spec->catfile($dir,$dirent);
193 2 50       41 if(-d File::Spec->catfile($dir,$dirent)){
194 0 0       0 next unless $self->recursive;
195 0         0 $self->_process_directory(File::Spec->catdir($dir,$dirent));
196             } else {
197 2         21 $self->add_file(File::Spec->catfile($dir,$dirent));
198             }
199             }
200             }
201              
202 1         3 return 1;
203             }
204              
205              
206             =head2 add_file
207              
208             Title : add_file
209             Usage : $icy->add_file('/usr/local/mp3/meow.mp3')
210             Function: add a file to be added to the playlist
211             Returns : true on success, false on failure
212             Args : a system path
213              
214              
215             =cut
216              
217             sub add_file{
218 2     2 1 4 my ($self,$file) = @_;
219              
220 2         90 my(undef,undef,$extension) = fileparse($file,keys(%AUDIO));
221 2         5 warn "adding file $file" if DEBUG;
222 2         3 warn $extension if DEBUG;
223              
224 2 50 33     67 if(!-f $file or !-r $file){
    50          
225 0         0 warn "not a readable file: $file" if DEBUG;
226 0         0 return undef;
227             } elsif($AUDIO{lc($extension)}) {
228 2         3 warn "adding $file" if DEBUG;
229 2         3 push @{$self->{files}}, $file;
  2         8  
230             } else {
231 0         0 warn "not a usable mimetype: $file" if DEBUG;
232 0         0 return undef;
233             }
234              
235 2         10 return 1;
236             }
237              
238             =head2 files
239              
240             Title : files
241             Usage : @files = $icy->files
242             Function: returns a list of all files that have been added
243             from calls to add_file() and add_directory()
244             Returns : a list of files
245             Args : none
246              
247              
248             =cut
249              
250             sub files{
251 0     0 1 0 my $self = shift;
252              
253 0 0       0 if(defined($self->{files})){
254 0 0       0 if($self->shuffle){
255 0         0 for (my $i=0; $i<@{$self->{files}}; $i++) {
  0         0  
256 0         0 my $rand = rand(scalar @{$self->{files}});
  0         0  
257              
258             #swap;
259 0         0 ($self->{files}->[$i],$self->{files}->[$rand])
260             =
261             ($self->{files}->[$rand],$self->{files}->[$i]);
262             }
263             }
264              
265 0         0 return @{$self->{files}};
  0         0  
266              
267             } else {
268 0         0 return ();
269             }
270              
271             }
272              
273             =head2 clear_files
274              
275             Title : clear_files
276             Usage :
277             Function:
278             Example :
279             Returns :
280             Args :
281              
282              
283             =cut
284              
285             sub clear_files{
286 0     0 1 0 my ($self) = @_;
287 0         0 $self->{files} = undef;
288 0         0 return 1;
289             }
290              
291             =head2 m3u
292              
293             Title : m3u
294             Usage : $m3u_text = $icy->m3u
295             Function: generates an Extended M3U string from the
296             contents of the list returned by files().
297             files not recognized by MP3::Info are
298             silently ignored
299             Returns : a Extended M3U string
300             Args : none
301              
302              
303             =cut
304              
305             sub m3u{
306 0     0 1 0 my $self = shift;
307              
308 0         0 my $output = undef;
309              
310              
311             # The extended format is:
312             # #EXTM3U
313             # #EXTINF:seconds,title - artist (album)
314             # URL
315             # but apparently you can override with this
316             # #EXTART:Britney Spears
317             # #EXTALB:Oops!.. I Did It Again
318             # #EXTTIT:Something or other
319             # and there doesn't seem to be a way to escape the -, so that's safer
320             # in theory, but if you send both it seems to ignore all but the EXTINF
321             # and there's no way to send seconds without it anyway, so we'll just do
322             # that.
323             #
324             # .... except that the second format breaks older versions of winamp
325             # so we'll use EXTINF only!
326              
327 0 0       0 $output .= "#EXTM3U$CRLF" if $self->files;
328 0         0 foreach my $file ($self->files){
329 0         0 my $info = $self->_get_info($file);
330              
331 0 0       0 next unless defined($info);
332 0         0 $file = $self->_mangle_path($file);
333              
334 0   0     0 my $time = $info->secs || -1;
335 0   0     0 my $artist = $info->artist || 'Unknown Artist';
336 0   0     0 my $album = $info->album || 'Unknown Album';
337 0   0     0 my $title = $info->title || 'Unknown Title';
338              
339 0         0 $output .= sprintf("#EXTINF:%d,%s - %s (%s)",$time,$title,$artist,$album) . $CRLF;
340 0         0 $output .= $file . $CRLF;
341             }
342              
343 0         0 return $output;
344             }
345              
346             =head2 pls
347              
348             Title : pls
349             Usage : $pls_text = $icy->pls
350             Function: generates a PLSv2 string from the
351             contents of the list returned by files().
352             files not recognized by MP3::Info are
353             silently ignored.
354             Returns : a PLSv2 string
355             Args : none
356              
357              
358             =cut
359              
360             sub pls{
361 0     0 1 0 my $self = shift;
362              
363 0         0 my $output = undef;
364              
365 0 0       0 $output .= "[playlist]$CRLF" if $self->files;
366 0         0 my $c = 0;
367 0         0 foreach my $file ($self->files){
368 0         0 my $info = $self->_get_info($file);
369              
370 0 0       0 next unless defined($info);
371              
372 0         0 $c++;
373              
374 0         0 $file = $self->_mangle_path($file);
375              
376 0   0     0 my $time = $info->secs || -1;
377 0   0     0 my $artist = $info->artist || 'Unknown Artist';
378 0   0     0 my $album = $info->album || 'Unknown Album';
379 0   0     0 my $title = $info->title || 'Unknown Title';
380              
381 0         0 $output .= uri_escape(sprintf("File%d=%s${CRLF}Title%d=%s - %s (%s)${CRLF}Length%d=%d$CRLF",$c,$file,$c,$title,$artist,$album,$c,$time));
382             }
383              
384 0 0       0 $output .= "NumberOfEntries=$c$CRLF" if $self->files;
385 0 0       0 $output .= "Version=2$CRLF" if $self->files;
386              
387 0         0 return $output;
388             }
389              
390             =head2 stream
391              
392             Title : streamll: 1 at /raid5a/allenday/projects/MP3/Icecast.pm line 459.
393              
394             Usage : $icy->stream('/usr/local/mp3/meow.mp3',0);
395             $icy->stream('/usr/local/mp3/meow.mp3',0,$io_handle);
396             Function: stream an audio file. prints to STDOUT unless a
397             third argument is given, in which case ->print() is
398             called on the second argument. An IO::Handle or
399             Apache instance will work here.
400             Returns : true on success, false on failure
401             Args : 1) system path to the file to stream
402             2) offset in file to start streaming
403             3) (optional) object to call ->print() on, rather
404             than printing to STDOUT
405              
406              
407             =cut
408              
409             sub stream{
410 0     0 1 0 my ($self,$file,$offset,$handle) = @_;
411              
412 0 0       0 return undef unless -f $file;
413 0         0 my $info = $self->_get_info($file);
414 0 0       0 return undef unless defined($info);
415              
416 0   0     0 my $genre = $info->genre || 'unknown genre';
417 0   0     0 my $description = $self->description($file) || 'unknown';
418 0   0     0 my $bitrate = $info->bitrate || 0;
419 0   0     0 my $size = -s $file || 0;
420 0         0 my $mime = $AUDIO{ lc((fileparse($file,keys(%AUDIO)))[2]) };
421 0         0 my $path = $self->_mangle_path($file);
422              
423 0   0     0 my $fh = $self->_open_file($file) || die "couldn't open file $file: $!";
424 0         0 binmode($fh);
425 0         0 seek($fh,$offset,0);
426              
427 0         0 my $output = '';
428 0 0       0 $output .= "ICY ". ($offset ? 206 : 200) ." OK$CRLF";
429 0         0 $output .= "icy-notice1:
This stream requires a shoutcast/icecast compatible player.
$CRLF";
430 0         0 $output .= "icy-notice2:MP3::Icecast
$CRLF";
431 0         0 $output .= "icy-name:$description$CRLF";
432 0         0 $output .= "icy-genre:$genre$CRLF";
433 0         0 $output .= "icy-url: $path$CRLF";
434 0         0 $output .= "icy-pub:1$CRLF";
435 0         0 $output .= "icy-br:$bitrate$CRLF";
436 0         0 $output .= "Accept-Ranges: bytes$CRLF";
437 0 0       0 if($offset){ $output .= "Content-Range: bytes $offset-" . ($size-1) . "/$size$CRLF" }
  0         0  
438 0         0 $output .= "Content-Length: $size$CRLF";
439 0         0 $output .= "Content-Type: $mime$CRLF";
440 0         0 $output .= "$CRLF";
441              
442 0 0       0 if(!ref($handle)){
    0          
443 0         0 print $output;
444             } elsif($handle->can('print')) {
445 0         0 $handle->print($output);
446             } else {
447 0         0 return undef;
448             }
449              
450 0         0 my $bytes = $size;
451 0         0 while($bytes > 0){
452 0         0 my $data;
453 0   0     0 my $b = read($fh,$data,2048) || last;
454 0         0 $bytes -= $b;
455              
456 0 0       0 if(!ref($handle)){
457 0         0 print $data;
458             } else {
459 0         0 $handle->print($data);
460             }
461             }
462              
463 0         0 return 1;
464             }
465              
466             =head2 _open_file
467              
468             Title : _open_file
469             Usage : $fh = $icy->open_file('/usr/local/mp3/meow.mp3');
470             Function:
471             Example :
472             Returns :
473             Args :
474              
475              
476             =cut
477              
478             sub _open_file{
479 0     0   0 my ($self,$file) = @_;
480              
481 0 0       0 return undef unless $file;
482 0         0 return IO::File->new($file,O_RDONLY);
483             }
484              
485             =head2 _mangle_path
486              
487             Title : _mangle_path
488             Usage : $path = $icy->_mangle_path('/usr/local/mp3/meow.mp3');
489             Function: applies alias substitutions and prefixes to a system path.
490             this is intended to be used to create resolvable URLs.
491             Returns : a string
492             Args : a system path
493              
494              
495             =cut
496              
497             sub _mangle_path{
498 0     0   0 my ($self,$path) = @_;
499              
500 0         0 my $qpath = quotemeta($path);
501              
502 0         0 foreach my $alias ($self->alias){
503 0         0 warn "replacing $alias..." if DEBUG;
504 0         0 my $search = $alias;
505              
506 0         0 my $qalias = quotemeta($alias);
507              
508 0 0       0 next unless $path =~ /^$qalias/;
509              
510 0         0 my $replace = $self->alias($alias);
511 0         0 $path =~ s/^$qalias/$replace/;
512 0         0 last;
513             }
514 0         0 $self->_uri_path_escape(\$path);
515 0   0     0 $path = join '', ($self->prefix ||'', $path ||'', $self->postfix ||'');
      0        
      0        
516 0         0 return $path;
517             }
518              
519             =head2 _path_escape
520              
521             Title : _path_escape
522             Usage :
523             Function:
524             Example :
525             Returns :
526             Args :
527              
528              
529             =cut
530              
531             sub _uri_path_escape{
532 0     0   0 my ($self,$uri) = @_;
533              
534 0         0 $$uri =~ s!([^a-zA-Z0-9_/.-])!uc sprintf("%%%02x",ord($1))!eg;
  0         0  
535             }
536              
537              
538             =head2 _get_info
539              
540             Title : _get_info
541             Usage : $mp3_info = $icy->_get_info($file)
542             Function: constucts and returns an MP3::Info object. the intended
543             use here is to access MP3 metadata (from ID3 tags,
544             filesize, etc).
545             Returns : a new MP3::Info object on success, false on failure
546             Args : a system path to a file
547              
548              
549             =cut
550              
551             sub _get_info{
552 0     0   0 my ($self,$file) = @_;
553              
554 0 0       0 return undef unless $file;
555 0         0 return new MP3::Info $file;
556             }
557              
558              
559             =head2 alias
560              
561             Title : alias
562             Usage : #returns 1
563             $icy->alias('/home/allenday/mp3' => '/mp3');
564              
565             #returns '/mp3'
566             $icy->alias('/home/allenday/mp3');
567              
568             #returns 1
569             $icy->alias('/usr/local/share/mp3' => '/share/mp3'); #returns 1
570              
571             #returns qw(/mp3 /share/mp3)
572             $icy->alias();
573             Function: this method provides similar behavior to Apache's Alias directive.
574             it allows mapping of system paths to virtual paths for usage by,
575             for instance, a webserver. the mapping is simple: when examining
576             a file, MP3::Icecast tries to match the beginning of the file's
577             full path to a sorted list of aliases. the first alias to match
578             is accepted. this may cause unexpected behavior in the event that
579             a file's path matches multiple alias entries. patches welcome.
580             Returns : see Usage
581             Args : see Usage
582              
583              
584             =cut
585              
586             sub alias{
587 1     1 1 274 my ($self,$search,$replace) = @_;
588              
589 1 50 33     17 if(defined($search) and defined($replace)){
    0          
590 1         5 $self->{alias}{$search} = $replace;
591             } elsif(defined($search)) {
592 0         0 return $self->{alias}{$search};
593             } else {
594 0         0 return sort keys %{$self->{alias}};
  0         0  
595             }
596             }
597              
598             =head2 prefix
599              
600             Title : prefix
601             Usage : $icy->prefix('http://');
602             Function: prefix all entries in the playlist with this value.
603             this string is *not* uri or system path escaped.
604             Returns : value of prefix (a scalar)
605             Args : on set, new value (a scalar or undef, optional)
606              
607              
608             =cut
609              
610             sub prefix{
611 0     0 1 0 my $self = shift;
612              
613 0 0       0 return $self->{'prefix'} = shift if @_;
614 0         0 return $self->{'prefix'};
615             }
616              
617             =head2 postfix
618              
619             Title : postfix
620             Usage : $obj->postfix($newval)
621             Function: postfix all entries in the playlist with this value.
622             this string is *not* uri or system path escaped.
623             uri escaped.
624             Returns : value of postfix (a scalar)
625             Args : on set, new value (a scalar or undef, optional)
626              
627              
628             =cut
629              
630             sub postfix{
631 0     0 1 0 my $self = shift;
632              
633 0 0       0 return $self->{'postfix'} = shift if @_;
634 0         0 return $self->{'postfix'};
635             }
636              
637             =head2 recursive
638              
639             Title : recursive
640             Usage : $obj->recursive($newval)
641             Function: flag determining whether a directory is recursively
642             searched for files when passed to ::add_directory().
643             default is false (no recursion).
644             Example :
645             Returns : value of recursive (a scalar)
646             Args : on set, new value (a scalar or undef, optional)
647              
648              
649             =cut
650              
651             sub recursive{
652 1     1 1 372 my $self = shift;
653              
654 1 50       10 return $self->{'recursive'} = shift if @_;
655 0           return $self->{'recursive'};
656             }
657              
658             =head2 shuffle
659              
660             Title : shuffle
661             Usage : $obj->shuffle($newval)
662             Function:
663             Example :
664             Returns : value of shuffle (a scalar)
665             Args : on set, new value (a scalar or undef, optional)
666              
667              
668             =cut
669              
670             sub shuffle{
671 0     0 1   my $self = shift;
672              
673 0 0         return $self->{'shuffle'} = shift if @_;
674 0           return $self->{'shuffle'};
675             }
676              
677             =head2 description
678              
679             Title : description
680             Usage : $description = $icy->description('/usr/local/mp3/meow.mp3');
681             Function: returns a description string of an MP3. this is extracted
682             from the ID3 tags by MP3::Info. the description format can
683             be customized, see the description_format() method.
684             Returns : a description string
685             Args : a valid system path
686              
687              
688             =cut
689              
690             sub description{
691 0     0 1   my $self = shift;
692 0           my $file = shift;
693 0           my $data = new MP3::Info $file;
694 0           my $description;
695 0           my $format = $self->description_format;
696 0 0         if ($format) {
697 0           ($description = $format) =~ s{%([atfglncrdmsqS%])}
698 0 0         {$1 eq '%' ? '%'
699             : $data->{$FORMAT_FIELDS{$1}}
700             }gxe;
701             } else {
702 0   0       $description = $data->{title} || basename($file, qw(.mp3 .MP3 .mp2 .MP2) );
703 0 0         $description .= " - $data->{artist}" if $data->{artist};
704 0 0         $description .= " ($data->{album})" if $data->{album};
705             }
706 0           return $description;
707             }
708              
709             =head2 description_format
710              
711             Title : description_format
712             Usage : $icy->description_format($format_string)
713             Function:
714             Returns : value of description_format (a scalar)
715             Args : on set, new value (a scalar or undef, optional)
716              
717              
718             =cut
719              
720             sub description_format{
721 0     0 1   my $self = shift;
722              
723 0 0         return $self->{'description_format'} = shift if @_;
724 0           return $self->{'description_format'};
725             }
726             1;