Listing 1.
Apache::MP3
Managing Streaming Audio
The Perl Journal, Winter 1999
 

Apache::MP3

 
  0 package Apache::MP3;
  1 # file: Apache/MP3.pm
  2 use strict;
  3 use Apache::Constants qw(:common);
  4 use MPEG::MP3Info;
  5 use IO::Dir;
  6 use Apache::File;
  7 # Intercept requests for audio/mpegurl (.pls) files and 
  8 # convert them into an appropriately-formatted playlist.
  9 # Intercept requests for audio/x-shoutcast-stream (.mps)
 10 # and convert them into appropriate shoutcast/icecast 
 11 # output to install:
 12 #
 13 # AddType audio/mpeg .mp3
 14 # AddType audio/mpegurl .pls
 15 # AddType audio/x-shoutcast-stream .mps
 16 #
 17 # <Files ~ "\.(pls|mps)$">
 18 #   SetHandler perl-script
 19 #   PerlHandler Apache::MP3
 20 # </Files>
 21 # entry point for mod_perl
 22 sub handler {
 23   my $r = shift;
 24   my $filename = $r->filename;
 25   # Reconstruct the requested URL.
 26   my $server_url = join '', 'http://',
 27                             $r->server->server_hostname,
 28                             ":",
 29                             $r->get_server_port;
 30   my $filename = $r->filename;
 31   my ($basename) = $filename  =~ m!([^/]+)\.[^/.]*$!;
 32   (my $directory   = $filename) =~ s!/[^/]+$!!;      
 33   (my $virtual_dir = $r->uri)   =~ s!/[^/]+$!!;      
 34   if ($r->content_type eq 'audio/mpegurl') {
 35     # If this is a request for a file of type 
 36     # audio/mpegurl, strip off the extension and look 
 37     # for a directory containing the name.  Generate a 
 38     # playlist from all mp3 files in the directory.
 39     return dir2playlist($r, "$directory/$basename", undef,
 40       "$server_url/$basename/") if -d "$directory/$basename";
 41     # If none found, search for a file of type audio/mpeg 
 42     # sharing the basename, and generate a playlist from that.
 43     return dir2playlist($r, $directory, $basename,
                                   "$server_url$virtual_dir/");
 44   } 
 45   # Otherwise is this a request for stream data?
 46   elsif ($r->content_type eq 'audio/x-shoutcast-stream') {
 47     my ($mp3_file) = search4mp3($r, $directory, $basename);
 48     return DECLINED unless $mp3_file;
 49     return send_stream($r, "$directory/$mp3_file", $server_url);
 50   }
 51 }

 52 # search for an mp3 file that matches a basename
 53 sub search4mp3 {
 54   my ($r, $dir, $basename) = @_;
 55   my $pattern = quotemeta $basename;
 56   my @mp3;
 57   my $dh = IO::Dir->new($dir) || return;
 58   while ( defined($_ = $dh->read) ) {
 59     next if $pattern && !/^$pattern(\.\w+)?$/;
 60     next if $r->lookup_file("$dir/$_")->content_type 
                                           ne 'audio/mpeg';
 61     push (@mp3, $_);
 62   }
 63   return @mp3;
 64 }

 65 # send the playlist...
 66 sub dir2playlist {
 67     my ($r, $dir, $basename, $url) = @_;
 68     my @mp3 = search4mp3($r, $dir, $basename);
 69     return DECLINED unless @mp3;
 70     $r->content_type('audio/mpegurl');
 71     $r->send_http_header;
 72     return OK if $r->header_only;
 73     $r->print ("[playlist]\r\n\r\n");
 74     $r->print ("NumberOfEntries=",scalar(@mp3),"\r\n");
 75     for (my $i=1;$i<=@mp3;$i++) {
 76       (my $file = $mp3[$i-1]) =~ s/(\.[^.]+)?$/.mps/;
 77       $r->print ("File$i=$url$file\r\n");
 78     }
 79     return OK;
 80 }

 81 # send the music stream...
 82 sub send_stream {
 83     my ($r, $file, $url) = @_;
 84     my $tag  = get_mp3tag($file);
 85     my $info = get_mp3info($file);
 86     return DECLINED unless $info;  # not a legit mp3 file?
 87     my $fh = Apache::File->new($file) || return DECLINED;
 88     my $title = $tag->{TITLE} || $url . $r->uri;
 89     foreach ( qw(ARTIST ALBUM YEAR COMMENT) ) {
 90         $title .= ' - ' . $tag->{$_} if $tag->{$_};
 91     }
 92     my $genre = $tag->{GENRE} || 'unknown';
 93     $r->print("ICY 200 OK\r\n");
 94     $r->print("icy-notice1:<BR>This stream requires a 
             shoutcast/icecast compatible player.<BR>\r\n");
 95     $r->print("icy-notice2:Apache::MP3 module<BR>\r\n");
 96     $r->print("icy-name:$title\r\n");
 97     $r->print("icy-genre:$genre\r\n");
 98     $r->print("icy-url:$url\r\n");
 99     $r->print("icy-pub:1\r\n");
100     $r->print("icy-br:$info->{BITRATE}\r\n");
101     $r->print("\r\n");
102     return OK if $r->header_only;
103     $r->send_fd($fh);
104     return OK;
105 }
106 1;
107 __END__