diff options
| author | Andreas Brachold <vdr07@deltab.de> | 2007-08-13 18:41:27 +0000 |
|---|---|---|
| committer | Andreas Brachold <vdr07@deltab.de> | 2007-08-13 18:41:27 +0000 |
| commit | bcbf441e09fb502cf64924ff2530fa144bdf52c5 (patch) | |
| tree | f377707a2dac078db8cd0c7d7abfe69ac1006d71 /lib/MP3/Icecast.pm | |
| download | xxv-bcbf441e09fb502cf64924ff2530fa144bdf52c5.tar.gz xxv-bcbf441e09fb502cf64924ff2530fa144bdf52c5.tar.bz2 | |
* Move files to trunk
Diffstat (limited to 'lib/MP3/Icecast.pm')
| -rw-r--r-- | lib/MP3/Icecast.pm | 727 |
1 files changed, 727 insertions, 0 deletions
diff --git a/lib/MP3/Icecast.pm b/lib/MP3/Icecast.pm new file mode 100644 index 0000000..729e959 --- /dev/null +++ b/lib/MP3/Icecast.pm @@ -0,0 +1,727 @@ +package MP3::Icecast; + +=head1 NAME + +MP3::Icecast - Generate Icecast streams, as well as M3U and PLSv2 playlists. + +=head1 SYNOPSIS + + use MP3::Icecast; + use MP3::Info; + use IO::Socket; + + + my $listen_socket = IO::Socket::INET->new( + LocalPort => 8000, #standard Icecast port + Listen => 20, + Proto => 'tcp', + Reuse => 1, + Timeout => 3600); + + #create an instance to find all files below /usr/local/mp3 + my $finder = MP3::Icecast->new(); + $finder->recursive(1); + $finder->add_directory('/usr/local/mp3'); + my @files = $finder->files; + + #accept TCP 8000 connections + while(1){ + next unless my $connection = $listen_socket->accept; + + defined(my $child = fork()) or die "Can't fork: $!"; + if($child == 0){ + $listen_socket->close; + + my $icy = MP3::Icecast->new; + + #stream files that have an ID3 genre tag of "jazz" + while(@files){ + my $file = shift @files; + my $info = new MP3::Info $file; + next unless $info; + next unless $info->genre =~ /jazz/i; + $icy->stream($file,0,$connection); + } + exit 0; + } + + #a contrived example to demonstrate that MP3::Icecast + #can generate M3U and PLSv2 media playlists. + print STDERR $icy->m3u, "\n"; + print STDERR $icy->pls, "\n"; + + $connection->close; + } + + +=head1 ABSTRACT + +MP3::Icecast supports streaming Icecast protocol over socket +or other filehandle (including STDIN). This is useful for writing +a streaming media server. + +MP3::Icecast also includes support for generating M3U and PLSv2 +playlist files. These are common formats supported by most modern +media players, including XMMS, Windows Media Player 9, and Winamp. + +=head1 SEE ALSO + + The Icecast project + http://www.icecast.org + + Namp! (Apache::MP3) + http://namp.sourceforge.net + + Unofficial M3U and PLS specifications + http://forums.winamp.com/showthread.php?threadid=65772 + +=head1 AUTHOR + + Allen Day, E<lt>allenday@ucla.eduE<gt> + +=head1 COPYRIGHT AND LICENSE + +Copyright 2003, Allen Day + +This library is free software; you can redistribute it and/or modify +it under the same terms as Perl itself. + +=cut + +use strict; +use File::Spec; +use File::Basename 'dirname','basename','fileparse'; +use URI::Escape; +use IO::File; +use MP3::Info; + +use constant DEBUG => 0; + +our $VERSION = '0.02'; + +our %AUDIO = ( + '.mp3' => 'audio/x-mp3', + ); +our %FORMAT_FIELDS = ( + a => 'artist', + c => 'comment', + d => 'duration', + f => 'filename', + g => 'genre', + l => 'album', + m => 'min', + n => 'track', + q => 'samplerate', + r => 'bitrate', + s => 'sec', + S => 'seconds', + t => 'title', + y => 'year', + ); + + +our $CRLF = "\015\012"; + +=head2 new + + Title : new + Usage : $icy = MP3::Icecast->new(%arg); + Function: create a new MP3::Icecast instance + Returns : an MP3::Icecast object + Args : none + + +=cut + +sub new{ + my($class,%arg) = @_; + + my $self = bless {}, $class; + + return $self; +} + +=head2 add_directory + + Title : add_directory + Usage : $icy->add_directory('/usr/local/mp3'); + Function: add a directory of files to be added to the playlist + Returns : true on success, false on failure + Args : a system path + + +=cut + +sub add_directory{ + my ($self,$dir) = @_; + warn "adding directory $dir" if DEBUG; + if(!-d $dir or !-r $dir){ + return undef; + } else { + $self->_process_directory($dir); + return 1; + } +} + +=head2 _process_directory + + Title : _process_directory + Usage : $icy->_process_directory('/usr/local/mp3'); + Function: searches a directory for files to add to the playlist + Returns : true on success + Args : a system path to search for files + + +=cut + +sub _process_directory{ + my ($self,$dir) = @_; + + if(!-r $dir){ + return undef; + } else { + warn "processing directory: $dir" if DEBUG; + + opendir(my $d, $dir) or die "couldn't opendir($dir): $!"; + my @dirents = grep {$_ ne '.' and $_ ne '..'} readdir($d); + closedir($d) or die "couldn't closedir($dir): $!"; + + foreach my $dirent (@dirents){ + warn "found dirent: $dirent" if DEBUG; + + next if !-r File::Spec->catfile($dir,$dirent); + if(-d File::Spec->catfile($dir,$dirent)){ + next unless $self->recursive; + $self->_process_directory(File::Spec->catdir($dir,$dirent)); + } else { + $self->add_file(File::Spec->catfile($dir,$dirent)); + } + } + } + + return 1; +} + + +=head2 add_file + + Title : add_file + Usage : $icy->add_file('/usr/local/mp3/meow.mp3') + Function: add a file to be added to the playlist + Returns : true on success, false on failure + Args : a system path + + +=cut + +sub add_file{ + my ($self,$file) = @_; + + my(undef,undef,$extension) = fileparse($file,keys(%AUDIO)); + warn "adding file $file" if DEBUG; + warn $extension if DEBUG; + + if(!-f $file or !-r $file){ + warn "not a readable file: $file" if DEBUG; + return undef; + } elsif($AUDIO{lc($extension)}) { + warn "adding $file" if DEBUG; + push @{$self->{files}}, $file; + } else { + warn "not a usable mimetype: $file" if DEBUG; + return undef; + } + + return 1; +} + +=head2 files + + Title : files + Usage : @files = $icy->files + Function: returns a list of all files that have been added + from calls to add_file() and add_directory() + Returns : a list of files + Args : none + + +=cut + +sub files{ + my $self = shift; + + if(defined($self->{files})){ + if($self->shuffle){ + for (my $i=0; $i<@{$self->{files}}; $i++) { + my $rand = rand(scalar @{$self->{files}}); + + #swap; + ($self->{files}->[$i],$self->{files}->[$rand]) + = + ($self->{files}->[$rand],$self->{files}->[$i]); + } + } + + return @{$self->{files}}; + + } else { + return (); + } + +} + +=head2 clear_files + + Title : clear_files + Usage : + Function: + Example : + Returns : + Args : + + +=cut + +sub clear_files{ + my ($self) = @_; + $self->{files} = undef; + return 1; +} + +=head2 m3u + + Title : m3u + Usage : $m3u_text = $icy->m3u + Function: generates an Extended M3U string from the + contents of the list returned by files(). + files not recognized by MP3::Info are + silently ignored + Returns : a Extended M3U string + Args : none + + +=cut + +sub m3u{ + my $self = shift; + + my $output = undef; + + + # The extended format is: + # #EXTM3U + # #EXTINF:seconds,title - artist (album) + # URL + # but apparently you can override with this + # #EXTART:Britney Spears + # #EXTALB:Oops!.. I Did It Again + # #EXTTIT:Something or other + # and there doesn't seem to be a way to escape the -, so that's safer + # in theory, but if you send both it seems to ignore all but the EXTINF + # and there's no way to send seconds without it anyway, so we'll just do + # that. + # + # .... except that the second format breaks older versions of winamp + # so we'll use EXTINF only! + + $output .= "#EXTM3U$CRLF" if $self->files; + foreach my $file ($self->files){ + my $info = $self->_get_info($file); + + next unless defined($info); + $file = $self->_mangle_path($file); + + my $time = $info->secs || -1; + my $artist = $info->artist || 'Unknown Artist'; + my $album = $info->album || 'Unknown Album'; + my $title = $info->title || 'Unknown Title'; + + $output .= sprintf("#EXTINF:%d,%s - %s (%s)",$time,$title,$artist,$album) . $CRLF; + $output .= $file . $CRLF; + } + + return $output; +} + +=head2 pls + + Title : pls + Usage : $pls_text = $icy->pls + Function: generates a PLSv2 string from the + contents of the list returned by files(). + files not recognized by MP3::Info are + silently ignored. + Returns : a PLSv2 string + Args : none + + +=cut + +sub pls{ + my $self = shift; + + my $output = undef; + + $output .= "[playlist]$CRLF" if $self->files; + my $c = 0; + foreach my $file ($self->files){ + my $info = $self->_get_info($file); + + next unless defined($info); + + $c++; + + $file = $self->_mangle_path($file); + + my $time = $info->secs || -1; + my $artist = $info->artist || 'Unknown Artist'; + my $album = $info->album || 'Unknown Album'; + my $title = $info->title || 'Unknown Title'; + + $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)); + } + + $output .= "NumberOfEntries=$c$CRLF" if $self->files; + $output .= "Version=2$CRLF" if $self->files; + + return $output; +} + +=head2 stream + + Title : streamll: 1 at /raid5a/allenday/projects/MP3/Icecast.pm line 459. + + Usage : $icy->stream('/usr/local/mp3/meow.mp3',0); + $icy->stream('/usr/local/mp3/meow.mp3',0,$io_handle); + Function: stream an audio file. prints to STDOUT unless a + third argument is given, in which case ->print() is + called on the second argument. An IO::Handle or + Apache instance will work here. + Returns : true on success, false on failure + Args : 1) system path to the file to stream + 2) offset in file to start streaming + 3) (optional) object to call ->print() on, rather + than printing to STDOUT + + +=cut + +sub stream{ + my ($self,$file,$offset,$handle) = @_; + + return undef unless -f $file; + my $info = $self->_get_info($file); + return undef unless defined($info); + + my $genre = $info->genre || 'unknown genre'; + my $description = $self->description($file) || 'unknown'; + my $bitrate = $info->bitrate || 0; + my $size = -s $file || 0; + my $mime = $AUDIO{ lc((fileparse($file,keys(%AUDIO)))[2]) }; + my $path = $self->_mangle_path($file); + + my $fh = $self->_open_file($file) || die "couldn't open file $file: $!"; + binmode($fh); + seek($fh,$offset,0); + + my $output = ''; + $output .= "ICY ". ($offset ? 206 : 200) ." OK$CRLF"; + $output .= "icy-notice1:<BR>This stream requires a shoutcast/icecast compatible player.<BR>$CRLF"; + $output .= "icy-notice2:MP3::Icecast<BR>$CRLF"; + $output .= "icy-name:$description$CRLF"; + $output .= "icy-genre:$genre$CRLF"; + $output .= "icy-url: $path$CRLF"; + $output .= "icy-pub:1$CRLF"; + $output .= "icy-br:$bitrate$CRLF"; + $output .= "Accept-Ranges: bytes$CRLF"; + if($offset){ $output .= "Content-Range: bytes $offset-" . ($size-1) . "/$size$CRLF" } + $output .= "Content-Length: $size$CRLF"; + $output .= "Content-Type: $mime$CRLF"; + $output .= "$CRLF"; + + if(!ref($handle)){ + print $output; + } elsif($handle->can('print')) { + $handle->print($output); + } else { + return undef; + } + + my $bytes = $size; + while($bytes > 0){ + my $data; + my $b = read($fh,$data,2048) || last; + $bytes -= $b; + + if(!ref($handle)){ + print $data; + } else { + $handle->print($data) + or return undef; + } + } + + return 1; +} + +=head2 _open_file + + Title : _open_file + Usage : $fh = $icy->open_file('/usr/local/mp3/meow.mp3'); + Function: + Example : + Returns : + Args : + + +=cut + +sub _open_file{ + my ($self,$file) = @_; + + return undef unless $file; + return IO::File->new($file,O_RDONLY); +} + +=head2 _mangle_path + + Title : _mangle_path + Usage : $path = $icy->_mangle_path('/usr/local/mp3/meow.mp3'); + Function: applies alias substitutions and prefixes to a system path. + this is intended to be used to create resolvable URLs. + Returns : a string + Args : a system path + + +=cut + +sub _mangle_path{ + my ($self,$path) = @_; + + my $qpath = quotemeta($path); + + foreach my $alias ($self->alias){ + warn "replacing $alias..." if DEBUG; + my $search = $alias; + + my $qalias = quotemeta($alias); + + next unless $path =~ /^$qalias/; + + my $replace = $self->alias($alias); + $path =~ s/^$qalias/$replace/; + last; + } + $self->_uri_path_escape(\$path); + $path = join '', ($self->prefix ||'', $path ||'', $self->postfix ||''); + return $path; +} + +=head2 _path_escape + + Title : _path_escape + Usage : + Function: + Example : + Returns : + Args : + + +=cut + +sub _uri_path_escape{ + my ($self,$uri) = @_; + + $$uri =~ s!([^a-zA-Z0-9_/.-])!uc sprintf("%%%02x",ord($1))!eg; +} + + +=head2 _get_info + + Title : _get_info + Usage : $mp3_info = $icy->_get_info($file) + Function: constucts and returns an MP3::Info object. the intended + use here is to access MP3 metadata (from ID3 tags, + filesize, etc). + Returns : a new MP3::Info object on success, false on failure + Args : a system path to a file + + +=cut + +sub _get_info{ + my ($self,$file) = @_; + + return undef unless $file; + return new MP3::Info $file; +} + + +=head2 alias + + Title : alias + Usage : #returns 1 + $icy->alias('/home/allenday/mp3' => '/mp3'); + + #returns '/mp3' + $icy->alias('/home/allenday/mp3'); + + #returns 1 + $icy->alias('/usr/local/share/mp3' => '/share/mp3'); #returns 1 + + #returns qw(/mp3 /share/mp3) + $icy->alias(); + Function: this method provides similar behavior to Apache's Alias directive. + it allows mapping of system paths to virtual paths for usage by, + for instance, a webserver. the mapping is simple: when examining + a file, MP3::Icecast tries to match the beginning of the file's + full path to a sorted list of aliases. the first alias to match + is accepted. this may cause unexpected behavior in the event that + a file's path matches multiple alias entries. patches welcome. + Returns : see Usage + Args : see Usage + + +=cut + +sub alias{ + my ($self,$search,$replace) = @_; + + if(defined($search) and defined($replace)){ + $self->{alias}{$search} = $replace; + } elsif(defined($search)) { + return $self->{alias}{$search}; + } else { + return sort keys %{$self->{alias}}; + } +} + +=head2 prefix + + Title : prefix + Usage : $icy->prefix('http://'); + Function: prefix all entries in the playlist with this value. + this string is *not* uri or system path escaped. + Returns : value of prefix (a scalar) + Args : on set, new value (a scalar or undef, optional) + + +=cut + +sub prefix{ + my $self = shift; + + return $self->{'prefix'} = shift if @_; + return $self->{'prefix'}; +} + +=head2 postfix + + Title : postfix + Usage : $obj->postfix($newval) + Function: postfix all entries in the playlist with this value. + this string is *not* uri or system path escaped. + uri escaped. + Returns : value of postfix (a scalar) + Args : on set, new value (a scalar or undef, optional) + + +=cut + +sub postfix{ + my $self = shift; + + return $self->{'postfix'} = shift if @_; + return $self->{'postfix'}; +} + +=head2 recursive + + Title : recursive + Usage : $obj->recursive($newval) + Function: flag determining whether a directory is recursively + searched for files when passed to ::add_directory(). + default is false (no recursion). + Example : + Returns : value of recursive (a scalar) + Args : on set, new value (a scalar or undef, optional) + + +=cut + +sub recursive{ + my $self = shift; + + return $self->{'recursive'} = shift if @_; + return $self->{'recursive'}; +} + +=head2 shuffle + + Title : shuffle + Usage : $obj->shuffle($newval) + Function: + Example : + Returns : value of shuffle (a scalar) + Args : on set, new value (a scalar or undef, optional) + + +=cut + +sub shuffle{ + my $self = shift; + + return $self->{'shuffle'} = shift if @_; + return $self->{'shuffle'}; +} + +=head2 description + + Title : description + Usage : $description = $icy->description('/usr/local/mp3/meow.mp3'); + Function: returns a description string of an MP3. this is extracted + from the ID3 tags by MP3::Info. the description format can + be customized, see the description_format() method. + Returns : a description string + Args : a valid system path + + +=cut + +sub description{ + my $self = shift; + my $file = shift; + my $data = new MP3::Info $file; + my $description; + my $format = $self->description_format; + if ($format) { + ($description = $format) =~ s{%([atfglncrdmsqS%])} + {$1 eq '%' ? '%' + : $data->{$FORMAT_FIELDS{$1}} + }gxe; + } else { + $description = $data->{title} || basename($file, qw(.mp3 .MP3 .mp2 .MP2) ); + $description .= " - $data->{artist}" if $data->{artist}; + $description .= " ($data->{album})" if $data->{album}; + } + return $description; +} + +=head2 description_format + + Title : description_format + Usage : $icy->description_format($format_string) + Function: + Returns : value of description_format (a scalar) + Args : on set, new value (a scalar or undef, optional) + + +=cut + +sub description_format{ + my $self = shift; + + return $self->{'description_format'} = shift if @_; + return $self->{'description_format'}; +} +1; |
