package Apache::MP3;
# $Id: MP3.pm,v 1.14 2002/08/18 17:33:49 lstein Exp $
use strict;
use Apache::Constants qw(:common REDIRECT HTTP_NO_CONTENT DIR_MAGIC_TYPE HTTP_NOT_MODIFIED);
use Apache::MP3::L10N;
use IO::File;
use Socket 'sockaddr_in';
use CGI qw/:standard escape *table *TR *blockquote *center *h1/;
use File::Basename 'dirname','basename';
use File::Path;
use vars qw($VERSION);
$VERSION = '3.01';
my $CRLF = "\015\012";
# defaults:
use constant BASE_DIR => '/apache_mp3';
use constant STYLESHEET => 'apache_mp3.css';
use constant PARENTICON => 'back.gif';
use constant PLAYICON => 'play.gif';
use constant SHUFFLEICON => 'shuffle.gif';
use constant CDICON => 'cd_icon.gif';
use constant CDLISTICON => 'cd_icon_small.gif';
use constant PLAYLISTICON => 'playlist.gif';
use constant COVERIMAGE => 'cover.jpg';
use constant COVERIMAGESMALL => 'cover_small.jpg';
use constant PLAYLISTIMAGE=> 'playlist.jpg';
use constant SONGICON => 'sound.gif';
use constant ARROWICON => 'right_arrow.gif';
use constant SUBDIRCOLUMNS => 3;
use constant PLAYLISTCOLUMNS => 3;
use constant HELPIMGURL => 'apache_mp3_fig1.gif:374x292';
my %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',
);
my @suffix = qw(.ogg .OGG .wav .WAV .mp3 .MP3 .mpeg .MPEG);
my %supported_types = (
# type condition handler method
'audio/mpeg' => eval "use MP3::Info; 1;" && 'read_mpeg',
'application/x-ogg' => eval "use Ogg::Vorbis; 1;" && 'read_vorbis',
'audio/x-wav' => eval "use Audio::Wav; 1;" && 'read_wav',
);
my $NO = '^(no|false)$'; # regular expression
my $YES = '^(yes|true)$'; # regular expression
sub handler ($$) {
my $class = shift;
my $obj = $class->new(@_) or die "Can't create object: $!";
return $obj->run();
}
sub new {
my $class = shift;
my $r = shift if @_ == 1;
my $new = bless {@_}, ref($class) || $class;
$new->{'r'} ||= $r if $r;
my @lang_tags;
push @lang_tags,split /,\s+/,$r->header_in('Accept-language')
if $r->header_in('Accept-language');
push @lang_tags,$r->dir_config('DefaultLanguage') || 'en-US';
$new->{'lh'} ||=
Apache::MP3::L10N->get_handle(@lang_tags)
|| die "No language handle?"; # shouldn't ever happen!
return $new;
}
sub x { # maketext plus maybe escape. The "x" for "xlate"
my $x = (my $lh = shift->{'lh'})->maketext(@_);
$x =~ s/([^\x00-\x7f])/''.ord($1).';'/eg
if $x =~ m/[^\x00-\x7f]/ and $lh->must_escape;
return $x;
}
sub lh { return shift->{lh} } # language handle
sub aright { -align => shift->{lh}->right }
# align "right" (or, in case of Arabic (etc), really left).
sub aleft { -align => shift->{lh}->left }
# align "light" (or, in case of Arabic (etc), really right).
sub r { return shift->{r} }
sub html_content_type {
my $self = shift;
return 'text/html; charset=' . $self->lh->encoding
}
sub help_screen {
my $self = shift;
$self->r->send_http_header( $self->html_content_type );
return OK if $self->r->header_only;
print start_html(
-lang => $self->lh->language_tag,
-title => $self->x('Quick Help Summary'),
-dir => $self->lh->direction,
-head => meta({-http_equiv => 'Content-Type',
-content => 'text/html; charset='
. $self->html_content_type
}),
);
my $help_img_url = $self->help_img_url; # URL for the image
my ($url,$width,$height) = $help_img_url=~/(.+):(\d+)x(\d+)/;
$url ||= $help_img_url;
$width ||= 500;
$height ||= 400;
print img({-src => $url,
-alt => "",
-height => $height,
-width => $width,
$self->aleft,
}), "\n";
print join "\n".br(),
$self->help_figure_list
;
print "\n", end_html();
return;
}
sub help_figure_list {
my $self = shift;
# Provide a legend for the items in the figure
return(
b("A"). $self->x("= Stream all songs"),
b("B"). $self->x("= Shuffle-play all Songs"),
b("C"). $self->x("= Stream all songs"),
b("D"). $self->x("= Go to earlier directory"),
b("E"). $self->x("= Stream contents"),
b("F"). $self->x("= Enter directory"),
b("G"). $self->x("= Stream this song"),
b("H"). $self->x("= Select for streaming"),
b("I"). $self->x("= Download this song"),
b("J"). $self->x("= Stream this song"),
b("K"). $self->x("= Sort by field"),
);
}
sub run {
my $self = shift;
my $r = $self->r;
local $CGI::XHTML = 0;
# check that we aren't running under PerlSetupEnv Off
if ($ENV{MOD_PERL} && !$ENV{SCRIPT_FILENAME}) {
warn "CGI.pm cannot run with 'PerlSetupEnv Off', please set it to On";
}
# this is called to show a help screen
return $self->help_screen if param('help_screen');
# generate directory listing
return $self->process_directory($r->filename)
if -d $r->filename; # should be $r->finfo, but STILL problems with this
#simple download of file
return $self->download_file($r->filename) unless param;
# this is called to stream a file
return $self->stream if param('stream');
# this is called to generate a playlist on the current directory
return $self->send_playlist($self->find_mp3s)
if param('Play All');
# this is called to generate a playlist on the current directory
# and everything beneath
return $self->send_playlist($self->find_mp3s('recursive'))
if param('Play All Recursive') ;
# this is called to generate a shuffled playlist of current directory
return $self->send_playlist($self->find_mp3s,'shuffle')
if param('Shuffle');
# this is called to generate a shuffled playlist of current directory
return $self->send_playlist($self->find_mp3s,'shuffle')
if param('Shuffle All');
# this is called to generate a shuffled playlist of current directory
# and everything beneath
return $self->send_playlist($self->find_mp3s('recursive'),'shuffle')
if param('Shuffle All Recursive');
# this is called to generate a playlist for one file
if (param('play')) {
my $dot3 = '.m3u|.pls';
my($basename) = $r->uri =~ m!([^/]+?)($dot3)?$!;
$basename = quotemeta($basename);
my @matches;
if (-e $self->r->filename) {
# If the actual .m3u file exists (it's a playlist), then we read it
# to get the list of files to send
@matches = $self->load_playlist($self->r->filename);
} else {
# find the MP3 file that corresponds to basename.m3u
@matches = grep { m!/$basename[^/]*$! } @{$self->find_mp3s};
}
if ($r->request($r->uri)->content_type eq 'audio/playlist'){
$self->send_playlist(\@matches);
}
elsif($r->request($r->uri)->content_type eq 'audio/x-scpls'){
open(FILE,$r->filename) || return 404;
$r->send_fd(\*FILE);
close(FILE);
}
return OK;
}
# this is called to generate a playlist for selected files
if (param('Play Selected')) {
return HTTP_NO_CONTENT unless my @files = param('file');
my $uri = dirname($r->uri);
$self->send_playlist([map { "$uri/$_" } @files]);
return OK;
}
# otherwise don't know how to deal with this
$self->r->log_reason('Invalid parameters -- possible attempt to circumvent checks.');
return FORBIDDEN;
}
# this generates the top-level directory listing
sub process_directory {
my $self = shift;
my $dir = shift;
unless ($self->r->path_info){
#Issue an external redirect if the dir isn't tailed with a '/'
my $uri = $self->r->uri;
my $query = $self->r->args;
$query = "?" . $query if defined $query;
$self->r->header_out(Location => "$uri/$query");
return REDIRECT;
}
return $self->list_directory($dir);
}
# this downloads the file
sub download_file {
my $self = shift;
my $file = shift;
my $type = $self->r->content_type;
my $is_audio = $self->r->content_type eq 'audio/mpeg'
|| $self->r->content_type eq 'application/x-ogg';
if ($is_audio && !$self->download_ok) {
$self->r->log_reason('File downloading is forbidden');
return FORBIDDEN;
} else {
return DECLINED; # allow Apache to do its standard thing
}
}
# stream the indicated file
sub stream {
my $self = shift;
my $r = $self->r;
return DECLINED unless -e $r->filename; # should be $r->finfo
unless ($self->stream_ok) {
$r->log_reason('AllowStream forbidden');
return FORBIDDEN;
}
if ($self->check_stream_client and !$self->is_stream_client) {
my $useragent = $r->header_in('User-Agent');
$r->log_reason("CheckStreamClient is true and $useragent is not a streaming client");
return FORBIDDEN;
}
return $self->send_stream($r->filename,$r->uri);
}
# this generates a playlist for the MP3 player
sub send_playlist {
my $self = shift;
my ($urls,$shuffle) = @_;
return HTTP_NO_CONTENT unless @$urls;
my $r = $self->r;
my $base = $self->stream_base;
$r->send_http_header('audio/mpegurl');
return OK if $r->header_only;
# local user
my $local = $self->playlocal_ok && $self->is_local;
# 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!
$self->shuffle($urls) if $shuffle;
$r->print("#EXTM3U$CRLF");
my $stream_parms = $self->stream_parms;
foreach (@$urls) {
$self->path_escape(\$_);
my $subr = $r->lookup_uri($_) or next;
my $file = $subr->filename;
my $type = $subr->content_type;
my $data = $self->fetch_info($file,$type);
my $format = $self->r->dir_config('DescriptionFormat');
if ($format) {
$r->print('#EXTINF:' , $data->{seconds} , ',');
(my $description = $format) =~ s{%([atfglncrdmsqS%])}
{$1 eq '%' ? '%' : $data->{$FORMAT_FIELDS{$1}}}gxe;
print $description;
print $CRLF;
} else {
$r->print('#EXTINF:' , $data->{seconds} ,
',', $data->{title},
' - ',$data->{artist},
' (',$data->{album},')',
$CRLF);
}
if ($local) {
$r->print($file,$CRLF);
} else {
$r->print ("$base$_?$stream_parms$CRLF");
}
}
return OK;
}
sub stream_parms {
my $self = shift;
return "stream=1";
}
# this searches the current directory for MP3 files and subdirectories
sub find_mp3s {
my $self = shift;
my $recurse = shift;
my $dir = dirname($self->r->filename);
my $uri = dirname($self->r->uri);
my @uris = $self->sort_mp3s($self->_find_mp3s($dir,$recurse));
foreach (@uris) {
# strip directory part
substr($_,0,length($dir)+1) = '' if index($_,$dir) == 0;
# turn into a URL
$_ = "$uri/$_";
}
return \@uris;
}
# recursive find
sub _find_mp3s {
my $self = shift;
my ($d,$recurse) = @_;
my ($directories,$files) = $self->read_directory($d);
# Add the directory back onto each file
unless ($d eq '.') {
foreach my $k (keys %$files) {
$files->{"$d/$k"} = $files->{$k};
delete $files->{$k};
}
}
if ($recurse) {
foreach (@$directories) {
my $f = $self->_find_mp3s("$d/$_",$recurse);
# Add the new files to our main hash
$files->{$_} = $f->{$_} foreach keys %$f;
}
}
return $files;
}
# sort MP3s
sub sort_mp3s {
my $self = shift;
my $files = shift;
return sort keys %$files;
}
# load the contents of a playlist (.m3u) from disk
sub load_playlist {
my $self = shift;
my $playlist = shift;
my @mp3s = ();
my $uri = dirname($self->r->uri);
local $_;
my $fh = IO::File->new($playlist)
or die "Failed to open $playlist";
while(<$fh>) {
chomp;
s/\#.*//; # get rid of comment and hint lines
s/\s+$//; # get rid of whitespace at end of lines
next unless $_;
push @mp3s, "$uri/$_";
}
$fh->close;
return @mp3s
}
# shuffle an array
sub shuffle {
my $self = shift;
my $list = shift;
for (my $i=0; $i<@$list; $i++) {
my $rand = rand(scalar @$list);
($list->[$i],$list->[$rand]) = ($list->[$rand],$list->[$i]); # swap
}
}
# top level for directory display
sub list_directory {
my $self = shift;
my $dir = shift;
return DECLINED unless -d $dir;
my $last_modified = (stat(_))[9];
$self->r->header_out('ETag' => sprintf("%lx-%s", $last_modified, $VERSION));
if (my $check = $self->r->header_in("If-None-Match")) {
my ($time, $ver) = $check =~ /^([a-f0-9]+)-([0-9.]+)$/;
if ($check eq '*' or (hex($time) == $last_modified and $ver == $VERSION)) {
return HTTP_NOT_MODIFIED;
}
}
return DECLINED unless my ($directories,$mp3s,$playlists)
= $self->read_directory($dir);
$self->r->send_http_header( $self->html_content_type );
return OK if $self->r->header_only;
$self->page_top($dir);
$self->directory_top($dir);
print "\n\n";
if(@$directories) {
print "\n\n";
$self->list_subdirs($directories);
print "\n\n";
}
if(@$playlists) {
print "\n\n";
$self->list_playlists($playlists);
print "\n\n";
}
if(%$mp3s) {
print "\n\n";
$self->list_mp3s($mp3s);
print "\n\n";
}
print hr unless %$mp3s;
print "\n\n";
$self->directory_bottom($dir);
return OK;
}
# print the HTML at the top of the page
sub page_top {
my $self = shift;
my $dir = shift;
my $title = $self->r->uri;
print start_html(
-title => $title,
-head => meta({-http_equiv => 'Content-Type',
-content => 'text/html; charset='
. $self->html_content_type
}),
-lang => $self->lh->language_tag,
-dir => $self->lh->direction,
-style => {-src=>$self->stylesheet}
);
}
# print the HTML at the top of a directory listing
sub directory_top {
my $self = shift;
my $dir = shift;
my $title = $self->r->uri;
my $links;
if ($self->path_style eq 'staircase') {
$links = $self->generate_navpath_staircase($title);
} else {
$links = $self->generate_navpath_arrows($title);
}
print a({-href=>'./playlist.m3u?Play+All+Recursive=1'},
img({-src => $self->cd_icon($dir), $self->aleft, -alt=>
$self->x('Stream All'),
-border=>0})),
$links,
a({-href=>'./playlist.m3u?Shuffle+All+Recursive=1'},
font({-class=>'directory'}, '[',
$self->x('Shuffle All'),
']'
))
.' '.
a({-href=>'./playlist.m3u?Play+All+Recursive=1'},
font({-class=>'directory'}, '[',
$self->x('Stream All'),
']'
)),
br({-clear=>'ALL'}),;
if (my $t = $self->stream_timeout) {
print p(strong(
$self->x('Note:')
),' ',
$self->x("In this demo, streaming is limited to approximately [_1,second].", $t),
"\n"
);
}
}
# staircase style path
sub generate_navpath_staircase {
my $self = shift;
my $uri = shift;
my $home = $self->home_label;
my $indent = 3.0;
my @components = split '/',$uri;
unshift @components,'' unless @components;
my ($path,$links);
my $current_style = "line-height: 1.2; font-weight: bold; color: red;";
my $parent_style = "line-height: 1.2; font-weight: bold;";
for (my $c=0; $c < @components-1; $c++) {
$path .= escape($components[$c]) ."/";
my $idt = $c * $indent;
my $l = a({-href=>$path},$components[$c] || $home);
$links .= div({-style=>"text-indent: ${idt}em; $parent_style"},
font({-size=>'+1'},$l))."\n";
}
my $idt = (@components-1) * $indent;
$links .= div({-style=>"text-indent: ${idt}em; $current_style"},
font({-size=>'+1'},$components[-1] || $home))."\n";
return $links;
}
# alternative display on one line using arrows
sub generate_navpath_arrows {
my $self = shift;
my $uri = shift;
my $home = $self->home_label;
my @components = split '/',$uri;
unshift @components,'' unless @components;
my $path;
my $links = start_h1();
my $arrow = $self->arrow_icon;
for (my $c=0; $c < @components-1; $c++) {
$links .= ' ' . img({-src=>$arrow}) if $path;
$path .= escape($components[$c]) . "/";
$links .= ' ' . a({-href=>$path},$components[$c] || $home);
}
$links .= ' ' . img({-src=>$arrow}) if $path;
$links .= " ". ($components[-1] || $home);
$links .= end_h1();
return $links;
}
# print the HTML at the bottom of the page
sub directory_bottom {
my $self = shift;
my $dir = shift; # actually not used
print
table({-width=>'100%',-border=>0},
TR(
td({$self->aleft},
#address( # Unpredictable and/or flaky rendering
$self ->x( "_CREDITS_before_author" )
.
a({-href=>'http://stein.cshl.org'},
$self->x( "_CREDITS_author" )
)
.
$self ->x( "_CREDITS_after_author" )
#)
),
td({$self->aright},$self->get_help))
);
print "",
;
print end_html();
}
# print the HTML at the top of the list of subdirs
sub subdir_list_top {
my $self = shift;
my $subdirs = shift; # array reference
print "\n", hr;
print "\n\n", h2({-class=>'CDdirectories'},
$self->x('CD Directories ([_1])',
scalar @$subdirs),
), "\n";
}
# print the HTML at the bottom of the list of subdirs
sub subdir_list_bottom {
my $self = shift;
my $subdirs = shift; # array reference
}
# print the HTML to format the list of subdirs
sub subdir_list {
my $self = shift;
my $subdirs = shift; #array reference
my @subdirs = $self->sort_subdirs($subdirs);
my $cols = $self->subdir_columns;
my $rows = int(0.99 + @subdirs/$cols);
print start_center,
start_table({-border=>0,-width=>'95%'}),"\n";
for (my $row=0; $row < $rows; $row++) {
print start_TR({-valign=>'BOTTOM'});
for (my $col=0; $col<$cols; $col++) {
my $i = $col * $rows + $row;
my $contents = $subdirs[$i] ? $self->format_subdir($subdirs[$i]) : ' ';
print td($contents);
}
print end_TR,"\n";
}
print end_table,end_center;
}
# given a list of CD directories, sort them
sub sort_subdirs {
my $self = shift;
my $subdirs = shift;
return sort @$subdirs; # alphabetic sort by default
}
# format a subdir entry and return its HTML
sub format_subdir {
my $self = shift;
my $subdir = shift;
my $nb = ' ';
(my $title = $subdir) =~ s/\s/$nb/og; # replace whitespace with
my $result = p(
a({-href=>escape($subdir).'/playlist.m3u?Play+All+Recursive=1'},
img({-src=>$self->cd_list_icon($subdir),
-align=>'ABSMIDDLE',
-class=>'subdir',
-alt=>
$self->x('Stream'),
-border=>0}))
.$nb.
a({-href=>escape($subdir).'/'},font({-class=>'subdirectory'},$title)),
br, "\n",
a({-class=>'subdirbuttons',
-href=>escape($subdir).'/playlist.m3u?Shuffle+All+Recursive=1'},
'[' .
$self->x('Shuffle')
.']')
.$nb.
a({-class=>'subdirbuttons',
-href=>escape($subdir).'/playlist.m3u?Play+All+Recursive=1'},
'['.
$self->x('Stream')
.']'))."\n";
return $result;
}
sub playlist_list_top {
my $self = shift;
my $playlists = shift; # array ref
print hr;
print "\n\n", h2({-class=>'CDdirectories'},
$self->x('Playlists ([_1])',
scalar @$playlists));
}
# print the HTML at the bottom of the list of playlists
sub playlist_list_bottom {
my $self = shift;
my $playlists = shift; # array ref
}
# print the HTML to format the list of playlists
sub playlist_list {
my $self = shift;
my $playlists = shift; # array ref
my $cols = $self->playlist_columns;
my $rows = int(0.99 + @$playlists / $cols);
print start_center,
start_table({-border => 0, -width => '95%'}), "\n";
for(my $row = 0; $row < $rows; $row++) {
print start_TR({-valign => 'BOTTOM'});
for(my $col = 0; $col < $cols; $col++) {
my $i = $col * $rows + $row;
my $contents = $playlists->[$i]
? $self->format_playlist( $playlists->[$i] )
: ' ';
print td($contents);
}
print end_TR, "\n";
}
print end_table,
end_center;
}
# format a playlist entry and return its HTML
sub format_playlist {
my $self = shift;
my $playlist = shift;
my $nb = ' ';
my $dot3 = '.m3u|.pls';
my($param) = $playlist =~ /\.m3u$/ ? '?play=1' : '';
(my $title = $playlist) =~ s/$dot3$//;
$title =~ s/\s/$nb/og;
my $url = escape($playlist) . $param;
return p(a({-href => $url},
img({-src => $self->playlist_icon,
-align => 'ABSMIDDLE',
-class => 'subdir',
-alt =>
$self->x('Playlist'),
-border => 0}))
. $nb .
a({-href => $url},
font({-class => 'subdirectory'},
$title)));
}
# This generates the link for help
sub get_help {
my $self = shift;
return a({-href => "?help_screen=1",}, $self->x('Quick Help Summary'));
}
# this is called to display the subdirs (subdirectories) within the current directory
sub list_subdirs {
my $self = shift;
my $subdirs = shift; # arrayref
$self->subdir_list_top($subdirs);
$self->subdir_list($subdirs);
$self->subdir_list_bottom($subdirs);
}
# this is called to display the playlists within the current directory
sub list_playlists {
my $self = shift;
my $playlists = shift; # arrayref
$self->playlist_list_top($playlists);
$self->playlist_list($playlists);
$self->playlist_list_bottom($playlists);
}
# this is called to display the MP3 files within the current directory
sub list_mp3s {
my $self = shift;
my $mp3s = shift; #hashref
$self->mp3_list_top($mp3s);
$self->mp3_list($mp3s);
$self->mp3_list_bottom($mp3s);
}
# top of MP3 file listing
sub mp3_list_top {
my $self = shift;
my $mp3s = shift; #hashref
print hr;
my $uri = $self->r->uri; # for self referencing
$uri =~ s!([^a-zA-Z0-9/])!uc sprintf("%%%02x",ord($1))!eg;
# apache and/or mod_perl has some problem redirecting from POST requests...
print start_form(-name=>'form',-action=>"${uri}playlist.m3u",-method=>'GET');
# print start_form(-action=>"${uri}playlist.m3u");
my $count = keys %$mp3s;
print
"\n\n",
h2({-class=>'SongList'},
a({-name=>'cds'},
$self->x("Song List ([_1])", $count),
),
),
"\n",
start_table({-border=>0,-cellspacing=>0,-width=>'100%'}),"\n";
print TR(td(),
td({$self->aleft,-colspan=>4},$self->control_buttons))
if $self->stream_ok and keys %$mp3s > $self->file_list_is_long;
$self->mp3_table_header;
}
sub control_buttons {
my $self = shift;
return (
sprintf('',
$self->x('Play Selected'),
),
sprintf('',
$self->x('Shuffle All'),
),
sprintf('',
$self->x('Play All'),
),
);
}
sub mp3_table_header {
my $self = shift;
my $url = url(-absolute=>1,-path_info=>1);
my @fields = map {
$self->x(ucfirst($_))
} $self->fields;
print TR({-class=>'title',$self->aleft,},
th({-colspan=>2,-align=>'CENTER'},
p($self->stream_ok ?
$self->x('Select')
: ''
)
),
th(\@fields)),"\n";
}
# bottom of MP3 file listing
sub mp3_list_bottom {
my $self = shift;
my $mp3s = shift; #hashref
print TR(td(),
td({$self->aleft,-colspan=>10},$self->control_buttons))
if $self->stream_ok;
print end_table,"\n";
print end_form;
print hr;
}
# each item of the list
sub mp3_list {
my $self = shift;
my $mp3s = shift; #hashref
my @f = $self->sort_mp3s($mp3s);
my $count = 0;
for my $song (@f) {
my $highlight = $count++ % 2 ? 'highlight' : 'normal';
my $contents = $self->format_song($song,$mp3s->{$song},$count);
print TR({-class=>$highlight},td($contents)), "\n";
}
}
# return the contents of the table for each mp3
sub format_song {
my $self = shift;
my ($song,$info,$count) = @_;
my @contents = ($self->format_song_controls($song,$info,$count),
$self->format_song_fields ($song,$info,$count));
return \@contents;
}
# Format the control part of each mp3 in the listing (checkbox, etc).
# Each list item becomes a cell in the table.
sub format_song_controls {
my $self = shift;
my ($song,$info,$count) = @_;
my $song_title = sprintf("%3d. %s", $count, $info->{title} || $song);
my $url = escape($song);
(my $play = $url) =~ s/(\.[^.]+)?$/.m3u?play=1/;
my $controls = '';
$controls .= checkbox(-name=>'file',-value=>$song,-label=>'') if $self->stream_ok;
$controls .= a({ -href=>$url, class => 'fetch' }, b(' ['.
$self->x('fetch')
.']'
))
if $self->download_ok;
$controls .= a({-href=>$play},b(' ['. # TODO: make an nbsp joiner?
$self->x('stream')
.']'
))
if $self->stream_ok;
return (
p(
$self->stream_ok ? a({-href=>$play},
img({-src => $self->song_icon,-alt =>
$self->x('stream'),
-border => 0}))
: img({-src => $self->song_icon})
),
p(
$controls
)
);
}
# format the fields of each mp3 in the listing (artist, bitrate, etc)
sub format_song_fields {
my $self = shift;
my ($song,$info,$count) = @_;
return map { $info->{lc $_}=~/^\d+$/ ?
p({$self->aright},$info->{lc($_)},' ') :
p($info->{lc($_)} || ' ') } $self->fields;
}
# read a single directory, returning lists of subdirectories and MP3 files
sub read_directory {
my $self = shift;
my $dir = shift;
my (@directories,%seen,%mp3s,@playlists);
opendir D,$dir or return;
while (defined(my $d = readdir(D))) {
next if $self->skip_directory($d);
# skip if file is unreadable
next unless -r "$dir/$d";
my $mime = $self->r->lookup_file("$dir/$d")->content_type;
push(@directories,$d) if !$seen{$d}++ && $mime eq DIR_MAGIC_TYPE;
# .m3u files should be configured as audio/playlist MIME types in your apache .conf file
push(@playlists,$d) if $mime =~ m!^audio/(playlist|x-mpegurl|mpegurl|x-scpls)$!;
next unless $supported_types{$mime};
next unless $mp3s{$d} = $self->fetch_info("$dir/$d", $mime);
}
closedir D;
return \(@directories,%mp3s,@playlists);
}
# return title, artist, duration, and kbps
sub fetch_info {
my $self = shift;
my ($file,$type) = @_;
return unless $supported_types{$type};
if (!$self->read_mp3_info) { # don't read config info
my $f = basename($file,@suffix);
return {
filename => $f,
description => $f,
};
}
my %data = $self->read_cache($file);
unless (%data and keys(%data) == keys(%FORMAT_FIELDS)) {
my $handler = $supported_types{$type};
$self->$handler($file,\%data);
# fill in missing fields
$data{filename} ||= basename($file);
$data{title} ||= basename($file,@suffix);
$self->write_cache($file => \%data);
}
if (my $blank = $self->missing_comment) {
foreach (qw(artist duration genre album track)) {
$data{$_} ||= $blank;
}
}
$data{description} = $self->description(\%data);
return \%data;
}
# these methods are called to read the MIME types specified in %supported_types
sub read_mpeg {
my $self = shift;
my ($file,$data) = @_;
return unless my $info = get_mp3info($file);
my $tag = get_mp3tag($file);
my ($title,$artist,$album,$year,$comment,$genre,$track) =
@{$tag}{qw(TITLE ARTIST ALBUM YEAR COMMENT GENRE TRACKNUM)} if $tag;
my $duration = sprintf "%d:%2.2d", $info->{MM}, $info->{SS};
my $seconds = ($info->{MM} * 60) + $info->{SS};
my $dir = dirname ($file);
if (basename ($file) =~ /^track-([0-9]+).mp3$/ && open INDEX, "<$dir/INDEX") {
my $track_num = $1;
while (my $line = ) {
if ($line =~ /^DTITLE=(.+)$/) {
($artist, $album) = split /\//, $1;
}
if ($line =~ /^TTITLE([0-9]+)=(.+)$/ && $track_num == $1+1) {
$title = $2;
}
}
close INDEX;
}
%$data =(
title => $title || '',
artist => $artist || '' ,
duration => $duration || '' ,
genre => $genre || '' ,
album => $album || '' ,
comment => $comment || '',
year => $year || '',
min => $info->{MM},
sec => $info->{SS},
seconds => $seconds,
track => $track || '',
bitrate => $info->{BITRATE},
samplerate => $info->{FREQUENCY},
);
}
sub read_vorbis {
my $self = shift;
my ($file,$data) = @_;
my $ogg = Ogg::Vorbis->new or return;
my $oggfh = IO::File->new($file) || die "$file: $!";
$ogg->open($oggfh);
my $comments = $ogg->comment;
my $info = $ogg->info;
my $sec = int $ogg->time_total;
# LS: it is unclear to me from the documentation at
# http://xiph.org/ogg/vorbis/doc/v-comment.html
# whether the fields are required to be case sensitive. The patch
# submitted by Devi Carraway used lower case, but is that right
# in general?
%$data = (
title => $comments->{title} || $comments->{TITLE} || '',
artist => $comments->{artist} || $comments->{ARTIST} || '',
duration => sprintf("%d:%2.2d", int($sec/60), $sec%60),
genre => $comments->{genre} || $comments->{GENRE} || '',
album => $comments->{album} || $comments->{ALBUM} || '',
comment => $comments->{comment} || $comments->{COMMENT} || '',
year => $comments->{year} || $comments->{YEAR} || '',
track => $comments->{tracknumber} || $comments->{TRACKNUMBER} || '',
bitrate => $ogg->bitrate/1000,
samplerate => $info->rate,
seconds => $sec,
min => int $sec/60,
sec => $sec%60,
);
close $oggfh;
}
sub read_wav {
my $self = shift;
my ($file,$data) = @_;
my $wav = Audio::Wav->new;
my $reader = $wav->read($file);
my $comments = $reader->get_info() || {};
my $details = $reader->details() || {};
my $sec = $reader->length_seconds;
%$data = (
title => $comments->{title} || $comments->{TITLE} || '',
artist => $comments->{artist} || $comments->{ARTIST} || '',
album => $comments->{album} || $comments->{ALBUM} || '',
year => $comments->{year} || $comments->{YEAR} || '',
genre => $comments->{genre} || $comments->{GENRE} || '',
track => $comments->{tracknumber} || $comments->{TRACKNUMBER} || '',
comment => $comments->{comment} || $comments->{COMMENT} || '',
min => int $sec/60,
sec => $sec %60,
seconds => $sec,
bitrate => int($details->{bytes_sec}*8/1024),
samplerate => $details->{sample_rate},
duration => sprintf("%d:%2.2d", int $sec/60,$sec%60),
)
}
# a limited escape of URLs (does not escape directory slashes)
sub path_escape {
my $self = shift;
my $uri = shift;
$$uri =~ s!([^a-zA-Z0-9_/.-])!uc sprintf("%%%02x",ord($1))!eg;
}
# get fields to display in list of MP3 files
sub fields {
my $self = shift;
my @f = split /\W+/,$self->r->dir_config('Fields');
return map { lc $_ } @f if @f; # lower case
return qw(title artist duration bitrate); # default
}
# read from the cache
sub read_cache {
my $self = shift;
my $file = shift;
return unless my $cache = $self->cache_dir;
my $cache_file = "$cache$file";
my $file_age = -M $file;
return unless -e $cache_file && -M $cache_file <= $file_age;
return unless my $c = IO::File->new($cache_file);
my ($data,$buffer);
while (read($c,$buffer,5000)) {
$data .= $buffer;
}
close $c;
return split $;,$data; # split into fields
}
# write to the cache
sub write_cache {
my $self = shift;
my ($file,$data) = @_;
return unless my $cache = $self->cache_dir;
my $cache_file = "$cache$file";
# some checks and untaint
return if $cache_file =~ m!/\.\./!; # no relative path tricks
$cache_file =~ m!^(/.+)$! or return;
$cache_file = $1;
my $dirname = dirname($cache_file);
-d $dirname || eval{mkpath($dirname)} || return;
if (my $c = IO::File->new(">$cache_file")) {
print $c join $;,%$data;
}
1;
}
# stream an MP3 file
sub send_stream {
my $self = shift;
my ($file,$url) = @_;
my $r = $self->r;
my $mime = $r->content_type;
my $info = $self->fetch_info($file,$mime);
return DECLINED unless $info; # not a legit mp3 file?
my $fh = $self->open_file($file) || return DECLINED;
binmode($fh); # to prevent DOS text-mode foolishness
my $size = -s $file;
my $bitrate = $info->{bitrate};
if ($self->can('bitrate') && $self->bitrate) {
($bitrate = $self->bitrate) =~ s/ kbps//i;
# quick approximation
$size = int($size * ($bitrate / $info->{bitrate}));
}
my $description = $info->{description};
my $genre = $info->{genre} || $self->lh->maketext('unknown');
my $range = 0;
$r->header_in("Range")
and $r->header_in("Range") =~ m/bytes=(\d+)/
and $range = $1
and seek($fh,$range,0);
# Look for a descriptive file that has the same base as the mp3 file.
# Also look for various index files.
my $icyurl = $self->stream_base(1);
my $base = basename($file);
$base =~ s/\.\w+$//; # get rid of suffix
my $dirbase = dirname($file);
my $urlbase = dirname($url);
foreach ("$base.html","$base.htm","index.html","index.htm") {
my $file = "$dirbase/$_";
if (-r $file) {
$icyurl .= "$urlbase/$_";
last;
}
}
$r->print("ICY ". ($range ? 206 : 200) ." OK$CRLF");
$r->print("icy-notice1: This stream requires a shoutcast/icecast compatible player. $CRLF");
$r->print("icy-notice2:Namp! (Apache::MP3) $CRLF");
$r->print("icy-name:$description$CRLF");
$r->print("icy-genre:$genre$CRLF");
$r->print("icy-url: $icyurl$CRLF");
$r->print("icy-pub:1$CRLF");
$r->print("icy-br:$bitrate$CRLF");
$r->print("Accept-Ranges: bytes$CRLF");
$r->print("Content-Range: bytes $range-" . ($size-1) . "/$size$CRLF")
if $range;
$r->print("Content-Length: $size$CRLF");
$r->print("Content-Type: $mime$CRLF");
$r->print("$CRLF");
return OK if $r->header_only;
if (my $timeout = $self->stream_timeout) {
my $seconds = $info->{seconds};
$seconds ||= 60; # shouldn't happen
my $fraction = $timeout/$seconds;
my $bytes = int($fraction * $size);
while ($bytes > 0) {
my $data;
my $b = read($fh,$data,2048) || last;
$bytes -= $b;
$r->print($data);
}
return OK;
}
# we get here for untimed transmits
$r->send_fd($fh);
return OK;
}
# called to open the MP3 file
# can override to do downsampling, etc
sub open_file {
my $self = shift;
my $file = shift;
return IO::File->new($file,O_RDONLY);
}
#################################################
# interesting configuration directives start here
#################################################
#utility subroutine for configuration
sub get_dir {
my $self = shift;
my ($config,$default) = @_;
my $dir = $self->r->dir_config($config) || $default;
return $dir if $dir =~ m!^/!; # looks like a path
return $dir if $dir =~ m!^\w+://!; # looks like a URL
return $self->default_dir . '/' . $dir;
}
# return true if downloads are allowed from this directory
sub download_ok {
shift->r->dir_config('AllowDownload') !~ /$NO/oi;
}
# return true if streaming is allowed from this directory
sub stream_ok {
shift->r->dir_config('AllowStream') !~ /$NO/oi;
}
# return true if playing locally is allowed
sub playlocal_ok {
shift->r->dir_config('AllowPlayLocally') =~ /$YES/oi;
}
# return true if we should check that the client can accomodate streaming
sub check_stream_client {
shift->r->dir_config('CheckStreamClient') =~ /$YES/oi;
}
# return true if client can stream
sub is_stream_client {
my $r = shift->r;
$r->header_in('Icy-MetaData') # winamp/xmms
|| $r->header_in('Bandwidth') # realplayer
|| $r->header_in('Accept') =~ m!\baudio/mpeg\b! # mpg123 and others
|| $r->header_in('User-Agent') =~ m!^NSPlayer/! # Microsoft media player
|| $r->header_in('User-Agent') =~ m!^xmms/!;
}
# whether to read info for each MP3 file (might take a long time)
sub read_mp3_info {
shift->r->dir_config('ReadMP3Info') !~ /$NO/oi;
}
# whether to time out streams
sub stream_timeout {
shift->r->dir_config('StreamTimeout') || 0;
}
# how long an album list is considered so long we should put buttons
# at the top as well as the bottom
sub file_list_is_long { shift->r->dir_config('LongList') || 10 }
sub home_label {
my $self = shift;
my $home = $self->r->dir_config('HomeLabel') ||
$self->x('Home');
return lc($home) eq 'hostname' ? $self->r->hostname : $home;
}
sub path_style { # style for the path to parent directories
lc(shift->r->dir_config('PathStyle')) || 'staircase';
}
# where is our cache directory (if any)
sub cache_dir {
my $self = shift;
return unless my $dir = $self->r->dir_config('CacheDir');
return $self->r->server_root_relative($dir);
}
# columns to display
sub subdir_columns {shift->r->dir_config('SubdirColumns') || SUBDIRCOLUMNS }
sub playlist_columns {shift->r->dir_config('PlaylistColumns') || PLAYLISTCOLUMNS }
# various configuration variables
sub default_dir { shift->r->dir_config('BaseDir') || BASE_DIR }
sub stylesheet { shift->get_dir('Stylesheet', STYLESHEET) }
sub parent_icon { shift->get_dir('ParentIcon',PARENTICON) }
sub cd_list_icon {
my $self = shift;
my $subdir = shift;
my $image = $self->r->dir_config('CoverImageSmall') || COVERIMAGESMALL;
my $directory_specific_icon = $self->r->filename."/$subdir/$image";
return -e $directory_specific_icon
? $self->r->uri . sprintf("/%s/%s", escape($subdir), $image)
: $self->get_dir('DirectoryIcon',CDLISTICON);
}
sub playlist_icon {
my $self = shift;
my $image = $self->r->dir_config('PlaylistImage') || PLAYLISTIMAGE;
my $directory_specific_icon = $self->r->filename."/$image";
warn $directory_specific_icon;
return -e $directory_specific_icon
? $self->r->uri . "/$image"
: $self->get_dir('PlaylistIcon',PLAYLISTICON);
}
sub song_icon { shift->get_dir('SongIcon',SONGICON) }
sub arrow_icon { shift->get_dir('ArrowIcon',ARROWICON) }
sub help_url { shift->get_dir('HelpURL', HELPIMGURL) }
sub help_img_url { shift->get_dir('HelpImgURL',HELPIMGURL) }
sub cd_icon {
my $self = shift;
my $dir = shift;
my $coverimg = $self->r->dir_config('CoverImage') || COVERIMAGE;
if (-e "$dir/$coverimg") {
$coverimg;
} else {
$self->get_dir('TitleIcon',CDICON);
}
}
sub missing_comment {
my $self = shift;
my $missing = $self->r->dir_config('MissingComment');
return if $missing eq 'off';
$missing = $self->lh->maketext('unknown') unless $missing;
$missing;
}
# create description string
sub description {
my $self = shift;
my $data = shift;
my $description;
my $format = $self->r->dir_config('DescriptionFormat');
if ($format) {
($description = $format) =~ s{%([atfglncrdmsqS%])}
{$1 eq '%' ? '%'
: $data->{$FORMAT_FIELDS{$1}}
}gxe;
} else {
$description = $data->{title} || basename($data->{filename},@suffix);
$description .= " - $data->{artist}" if $data->{artist};
$description .= " ($data->{album})" if $data->{album};
}
return $description;
}
sub stream_base {
my $self = shift;
my $suppress_auth = shift;
my $r = $self->r;
my $auth_info;
# the check for auth_name() prevents an annoying message in
# the apache server log when authentication is not in use.
if ($r->auth_name && !$suppress_auth) {
my ($res,$pw) = $r->get_basic_auth_pw;
if ($res == 0) { # authentication in use
my $user = $r->connection->user;
$auth_info = "$user:$pw\@";
}
}
if ((my $basename = $r->dir_config('StreamBase')) && !$self->is_localnet()) {
$basename =~ s!http://!http://$auth_info! if $auth_info;
return $basename;
}
my $vhost = $r->hostname;
unless ($vhost) {
$vhost = $r->server->server_hostname;
$vhost .= ':' . $r->get_server_port unless $r->get_server_port == 80;
}
return "http://${auth_info}${vhost}";
}
# patterns to skip
sub skip_directory {
my $self = shift;
my $dir = shift;
return 1 if $dir =~ /^\./;
return 1 if $dir eq 'CVS';
return 1 if $dir eq 'RCS';
return 1 if $dir eq 'SCCS';
undef;
}
# Checks if the requesting client is on the same machine as the server.
# If it is, then it points the playlist at the physical file, which
# allows the player to fast forward, pause, etc.
sub is_local {
my $self = shift;
my $r = $self->r;
my ($serverport,$serveraddr) = sockaddr_in($r->connection->local_addr);
my ($remoteport,$remoteaddr) = sockaddr_in($r->connection->remote_addr);
return $serveraddr eq $remoteaddr;
}
# Check if the requesting client is on the local network, as defined by
# the LocalNet directive
sub is_localnet {
my $self = shift;
return 1 if $self->is_local; # d'uh
my @local = split /\s+/,$self->r->dir_config('LocalNet') or return;
my $remote_ip = $self->r->connection->remote_ip . '.';
foreach (@local) {
$_ .= '.' unless /\.$/;
return 1 if index($remote_ip,$_) == 0;
}
return;
}
1;
# SAVED CODE:
# This was the old way I used to do create and run objects. The advantage was that
# you did not have to stop and start the server in order to see changes.
# The disadvantage was that it was a bit more work to write subclasses.
# sub handler {
# __PACKAGE__->handle_request(@_);
# }
# sub handle_request {
# my $pack = shift;
# my $obj = $pack->new(@_) or die "Can't create object: $!";
# $obj->run();
# }
__END__
=head1 NAME
Apache::MP3 - Generate streamable directories of MP3 and Ogg Vorbis files
=head1 SYNOPSIS
# httpd.conf or srm.conf
AddType audio/mpeg mp3 MP3
AddType audio/playlist m3u M3U
AddType audio/x-scpls pls PLS
AddType application/x-ogg ogg OGG
# httpd.conf or access.conf
SetHandler perl-script
PerlHandler Apache::MP3
# Or use the Apache::MP3::Sorted subclass to get sortable directory listings
SetHandler perl-script
PerlHandler Apache::MP3::Sorted
# Or use the Apache::MP3::Playlist subclass to get persistent playlists
SetHandler perl-script
PerlHandler Apache::MP3::Playlist
A B can be browsed at http://www.modperl.com/Songs/.
=head1 DESCRIPTION
This module makes it possible to browse a directory hierarchy
containing MP3, Ogg Vorbis, or Wav files, sort them on various
fields, download them, stream them to an MP3 decoder like WinAmp, and
construct playlists. The display is configurable and subclassable.
NOTE: This version of Apache::MP3 is substantially different from
the pre-2.0 version described in The Perl Journal. Specifically, the
format to use for HREF links has changed. See I for details.
=head2 Installation
This section describes the installation process.
=over 4
=item 1. Prequisites
This module requires mod_perl, MP3::Info (to stream MP3 files),
Ogg::Vorbis (to stream OggVorbis files), and Audio::Wav (for Wave
files) all of which are available on CPAN.
The module will automatically adjust for the absence of one or more of
the MP3::Info, Ogg::Vorbis or Audio::Wav modules by inhibiting the
display of the corresponding file type.
=item 2. Configure MIME types
Apache must be configured to recognize the mp3 and MP3 extensions as
MIME type audio/mpeg. Add the following to httpd.conf or srm.conf:
AddType audio/mpeg mp3 MP3
AddType audio/playlist m3u M3U
AddType audio/x-scpls pls PLS
AddType application/x-ogg ogg OGG
AddType audio/wav wav WAV
Note that you need extemely large amounts of bandwidth to stream Wav
files, and that few audio file players currently support this type of
streaming. Wav file support is primarily intended to allow for
convenient downloads.
=item 3. Install icons and stylesheet
This module uses a set of icons and a cascading stylesheet to generate
its song listings. By default, the module expects to find them at the
url /apache_mp3. Create a directory named apache_mp3 in your document
root, and copy into it the contents of the F directory
from the Apache-MP3 distribution.
You may change the location of this directory by setting the
I configuration variable. See the I section for
more details.
=item 4. Set Apache::MP3 to be the handler for the MP3 directory
In httpd.conf or access.conf, create a ELocationE or
EDirectoryE section, and make Apache::MP3 the handler for this
directory. This example assumes you are using the URL /Songs as the
directory where you will be storing song files:
SetHandler perl-script
PerlHandler Apache::MP3
If you would prefer an MP3 file listing that allows the user to sort
it in various ways, set the handler to use the Apache::MP3::Sorted
subclass instead. A further elaboration is Apache::MP3::Playlist,
which uses cookies to manage a persistent playlist for the user.
=item 5. Load MP3::Info in the Perl Startup file (optional)
For the purposes of faster startup and memory efficiency, you may load
the MP3::Info module at server startup time. If you have a mod_perl
"startup" file, enter these lines:
use MP3::Info;
use Apache::MP3;
=item 6. Set up MP3 directory
Create a directory in the web server document tree that will contain
the MP3 files to be served. The module recognizes and handles
subdirectories appropriately. I suggest organizing directories
hierarchically by artist and/or album name.
If you place a file named "cover.jpg" in any of the directories, that
image will be displayed at the top of the directory listing. You can
use this to display cover art.
If you place a list of .mp3 file names in a file with the .m3u
extension, it will be treated as a playlist and displayed to the user
with a distinctive icon. Selecting the playlist icon will download
the playlist and stream its contents. The playlist must contain
relative file names, but may refer to subdirectories, as in this
example:
# file: folk_favorites.m3u
Never_a_Moment_s_Thought_v2.mp3
Peter Paul & Mary - Leaving On A Jet Plane.mp3
Simon and Garfunkel/Simon And Garfunkel - April Come She Will.mp3
Likewise, if you place a list of shoutcast URLs into a file with the
.pls extension, it will be treated as a playlist and displayed to the
user with a distinctive icon. Selecting the playlist icon will
contact the shoutcast servers in the playlist and stream their
contents. The playlist syntax is as in this example:
[playlist]
numberofentries=2
File1=http://205.188.245.132:8038
Title1=Monkey Radio: Grooving. Sexy. Beats.
Length1=-1
File2=http://205.188.234.67:8052
Title2=SmoothJazz
Length2=-1
Version=2
Likewise, if you place a list of shoutcast URLs into a file with the
.pls extension, it will be treated as a playlist and displayed to the
user with a distinctive icon. Selecting the playlist icon will
contact the shoutcast servers in the playlist and stream their
contents. The playlist syntax is as in this example:
[playlist]
numberofentries=2
File1=http://205.188.245.132:8038
Title1=Monkey Radio: Grooving. Sexy. Beats.
Length1=-1
File2=http://205.188.234.67:8052
Title2=SmoothJazz
Length2=-1
Version=2
Apache::MP3 permits you to directly use CDDB data without embedding it
in ID3 tags. To take advantage of this feature, your MP3 files should
have file names of this form: track-XX.mp3. Then, place a CDDB index
file in the same directory as the tracks and name it INDEX. For
example, you might execute this command
cddbcmd cddb read soundtrack cb115c11 > INDEX
to create an INDEX file for the Mulholland Drive soundtrack. The
32-bit disc ID can be obtained with a program such as cd-discid.
=item 7. Set up an information cache directory (optional)
In order to generate its MP3 listing, Apache::MP3 must open each sound
file, extract its header information, and close it. This is time
consuming, particularly when recursively generating playlists across
multiple directories. To speed up this process, Apache::MP3 has the
ability cache MP3 file information in a separate directory area.
To configure this, choose a directory that the Web server has write
access for, such as /usr/tmp. Then add a configuration variable like
the following to the directive:
PerlSetVar CacheDir /usr/tmp/mp3_cache
If the designated directory does not exist, Apache::MP3 will attempt
to create it, limited of course by the Web server's privileges. You
may need to create the mp3_cache directory yourself if /usr/tmp is not
world writable.
=back
Open up the MP3 URL in your favorite browser. You should be able to
see directory listings, and download and stream your songs. If things
don't seem to be working, checking the server error log for messages.
=head1 CUSTOMIZING
Apache::MP3 can be customized in three ways: (1) by changing
per-directory variables; (2) changing settings in the Apache::MP3
cascading stylesheet; and (3) subclassing Apache::MP3 or
Apache::MP3::Sorted.
=head2 Per-directory configuration variables
Per-directory variables are set by I directives in the
Apache::MP3 ELocationE or EDirectoryE section. For
example, to change the icon displayed next to subdirectories of MP3s,
you would use I to change the I variable:
PerlSetVar DirectoryIcon big_cd.gif
This following table summarizes the configuration variables. A more
detailed explanation of each follows in the subsequent sections.
Table 1: Configuration Variables
Name Value Default
---- ----- -------
GENERAL OPTIONS
AllowDownload yes|no yes
AllowStream yes|no yes
AllowPlayLocally yes|no yes
CheckStreamClient yes|no no
ReadMP3Info yes|no yes
StreamTimeout integer 0
DIRECTORY OPTIONS
BaseDir URL /apache_mp3
CacheDir path -none-
HelpImgURL URL apache_mp3_fig1.gif:374x292
StreamBase URL -none-
LocalNet subnet -none-
DISPLAY OPTIONS
ArrowIcon URL right_arrow.gif
CoverImage filename cover.jpg
CoverImageSmall filename cover_small.jpg
PlaylistImage filename playlist.jpg
DescriptionFormat string -see below-
DirectoryIcon URL cd_icon_small.gif
PlaylistIcon URL playlist.gif
Fields list title,artist,duration,bitrate
HomeLabel string "Home" (or translation)
LongList integer 10
MissingComment string "unknown" (or translation)
PathStyle Staircase|Arrows Staircase
SongIcon URL sound.gif
SubdirColumns integer 3
Stylesheet URL apache_mp3.css
TitleIcon URL cd_icon.gif
DefaultLanguage languagetag en-US
=head2 General Configuration Variables
=over 4
=item AllowDownload I
You may wish for users to be able to stream songs but not download
them to their local disk. If you set AllowDownload to "no",
Apache::MP3 will not generate a download link for MP3 files. It will
also activate some code that makes it inconvenient (but not
impossible) for users to download the MP3s.
The module recognizes the arguments "yes", "no", "true" and "false".
The default is "yes".
Note that this setting only affects MP3 files. Other files, including
cover art and playlists, can still be downloaded.
=item AllowStream I
If you set AllowStream to "no", users will not be able to stream songs
or generate playlists. I am not sure why one would want this feature,
but it is included for completeness. The default is "yes."
=item AllowPlayLocally I
If you set AllowPlayLocally to "yes", then the playlists generated by
the module will point to the physical files when handling requests
from a user that happens to be working on the same machine. This is
more efficient, and allows the user to pause playback, fast forward,
and so on. Otherwise, the module will treat local users and remote
users the same. The default is "no".
=item CheckStreamClient I
Setting CheckStreamClient to "yes" enables code that checks whether
the client claims to be able to accept streaming MPEG data. This
check isn't foolproof, but supports at least the most popular MP3
decoders (WinAmp, RealPlayer, xmms, mpg123). It also makes it harder
for users to download songs by pretending to be a streaming player.
The default is "no".
=item ReadMP3Info I
This controls whether to extract field information from the MP3
files. The default is "yes".
If "no" is specified, all fields in the directory listing will be
blank except for I and I, which will both be
set to the physical filename of the MP3 file.
=item StreamTimeout I
For demo mode, you can specify a stream timeout in seconds.
Apache::MP3 will cease streaming the file after the time specified.
Because this feature uses the average bitrate of the song, it may be
off by a second or two when streaming variable bitrate MP3s.
=back
=head2 Configuration Variables Affecting Paths and Directories
=over 4
=item BaseDir I
The B variable sets the URL in which Apache::MP3 will look
for its icons and stylesheet. You may use any absolute local or
remote URL. Relative URLs are not accepted.
The default is "/apache_mp3."
=item CacheDir I
This variable sets the directory path for Apache::MP3's cache of MP3
file information. This must be an absolute path in the physical file
system and be writable by Apache. If not specified, Apache::MP3 will
not cache the file information, resulting in slower performance on
large directories.
=item HelpImgURL I
The URL of the image that's inlined on the page that appears
when the user presses the "Quick Help
Summary" link at the bottom of the page. You can declare the
size of this image
by adding ":WxH" to the end of the URL, where W and H are the width
and height, respectively.
Default: apache_mp3_help.gif:614x498
Note: I prepared this image on an airplane, so it isn't as clean as I
would like. Volunteers to make a better help page are welcomed!
=item StreamBase I
A URL to use as the base for streaming. The default is to use the
same host for both directory listings and streaming. This may be of
use when running behind a firewall and the web server can't figure out
the correct address for the playlist automatically.
Example:
If the song requested is http://www.foobar.com/Songs/Madonna_live.m3u?stream=1
and B is set to I, then the URL
placed in the playlist will be
http://streamer.myhost.net/Songs/Madonna_live.m3u?stream=1
The path part of the URL is simply appended to StreamBase. If you
want to do more sophisticated URL processing, use I or
equivalent.
=item LocalNet I
This configuration variable is used in conjunction with B
to disable B for clients on the local network. This is
needed for firewall configurations in which the web server is accessed
by one address & port by hosts behind the firewall, and by another
address & port by hosts outside the firewall.
The argument is a dotted subnet address, or a space-delimited list of
subnets. For example:
PerlSetVar LocalNet "192.168.1 192.168.2 127.0.0.1"
Address matching is done by matching the address from left to right,
with an implied dot added to the end of the subnet address. More
complex subnet matching using netmasks is desirable, but not
implemented.
=back
=head2 Configuration Variables Affecting the Visual Display
=over 4
=item ArrowIcon I
Set the icon used for the arrows displayed between the components of
the directory path at the top of the directory listing.
=item CoverImage I
Before displaying a directory, Apache::MP3 will look inside the
directory for an image file. This feature allows you to display
digitized album covers or other customized icons. The default is
"cover.jpg", but the image file name can be changed with
I. If the file does not exist, the image specified by
I will be displayed instead.
=item CoverImageSmall I
Before displaying the list of subdirectories, Apache::MP3 will check
inside of each for an image file of this name. If one is present, the
image will displayed rather than the generic I. The
default is "cover_small.jpg".
=item DescriptionFormat I
The "Description" field, which is used both in the Description column
of the directory index and in the metadata sent to the player during
streaming, has a default format of I-I-I. The
description is constructed in such a way that the hyphen is omitted if
the corresponding field of the song's MP3 tag is empty.
You can customize this behavior by providing a I
string. These strings combine constant characters with %x format
codes in much the way that sprintf() does. For example, the directive
shown below will create descriptions similar to I<[Madonna] Like a
Virgin (1980)>.
PerlSetVar DescriptionFormat "[%a] %t (%y)"
The full list of format codes follows:
Table 2: I Field Codes
Code Description
---- -----------
%a Artist name
%c Comment
%d Duration, in format 00:00 (like 15:20 for 15 mins 20 sec)
%f Name of physical file (minus path)
%g Genre
%l Album name
%m Minutes portion of duration, usually used with %s
%n Track number
%q Sample rate, in kHz
%r Bitrate, in kbps
%s Seconds portion of duration, usually used with %m
%S Duration, expressed as total seconds
%t Title
%y Year
=item DirectoryIcon I
Set the icon displayed next to subdirectories in directory listings,
"cd_icon_small.gif" by default. This can be overridden on a
directory-by-directory basis by placing a I image
into the directory that you want to customize.
=item PlaylistIcon I
Set the icon displayed next to playlists in the playlist listings,
"playlist.gif" by default. You can change this icon on a
directory-by-directory basis by placing a file with this name in the
current directory.
=item PlaylistImage I
Before displaying a playlist, the module will check inside the current
directory for an image file named "playlist.jpg" to use as its icon.
This directive changes the name of the playlist image file. If no
image is found, the icon specified by I is used instead.
=item Fields I
Specify what MP3 information fields to display in the song listing.
This should be a list delimited by commas, "|" symbols, or any other
non-word character.
The following are valid fields:
Table 3: Field Names For use with the I Configuration Variable
Field Description
----- -----------
album The album
artist The artist
bitrate The bitrate, expressed in kbps
comment The comment field
duration Duration of the song in hour, minute, second format
description Description as specified by DescriptionFormat
filename The physical name of the .mp3 file
genre The genre
min The minutes portion of the duration
seconds Total duration of the song in seconds
sec The seconds portion of the duration
samplerate The sampling rate, in KHz
title The title of the song
track The track number
year The album year
Note that MP3 rip and encoding software differ in what fields they
capture and the exact format of such fields as the title and album.
Field names are case insensitive.
Previous versions of this module used "kbps" instead of "bitrate".
This has been changed.
=item HomeLabel I
This is the label for the link used to return to the site's home
page. You may use plain text or any fragment of HTML, such as an
tag.
=item LongList I
The number of lines in the list of MP3 files after which it is
considered "long". In long lists, the control buttons are placed at
the top as well as at the bottom of the table. Defaults to 10.
=item MissingComment I
This is the text string to use when an MP3 or Vorbis comment is missing;
it is "unknown" (or its translation) by default. For example, if the
module is configured to display the artist name, but a music file is
missing this field, "unknown" (or its translation) will be printed
instead. To turn this feature off, use an argument of "off"; missing
fields will then be blank.
PerlSetVar MissingComment off
=item PathStyle I
Controls the style with which the parent directories are displayed.
The options are "Staircase" (the default), which creates a
staircase-style display (each child directory is on a new line and
offset by 0.3 em). The other is "Arrows", in which the entire
directory list is on a single line and separated by graphic arrows.
Try them both and choose the one you prefer.
=item SongIcon I
Set the icon displayed at the beginning of each line of the MP3 file
list, "sound.gif" by default.
=item SubdirColumns I
The number of columns in which to display subdirectories (the small
"CD icons"). Default 3.
=item PlaylistColumns I
The number of columns in which to display playlists. Default 3.
=item Stylesheet I
Set the URL of the cascading stylesheet to use, "apache_mp3.css" by
default. If the URL begins with a slash it is treated as an absolute
URL. Otherwise it is interpreted as relative to the BaseDir
directory.
=item TitleIcon I
Set the icon displayed next to the current directory's name in the
absence of a coverimage, "cd_icon.gif" by default. In this, and the
other icon-related directives, relative URLs are treated as relative
to I.
=item DefaultLanguage I
This determines what language the interface should try appearing in,
if none of the languages from the browser's Accept-Language header
can be supported. For information on language tags, see
L. Example value: "zh-cn" for
PRC-style Chinese.
=back
=head2 Stylesheet-Based Configuration
You can change the appearance of the page by changing the cascading
stylesheet that accompanies this module, I. The
following table describes the tags that can be customized:
Table 4: Stylesheet Class Names
Class Name Description
---------- ----------
BODY General defaults
H1 Current directory path
H2 "CD Directories" and "Song List" headings
TR.title Style for the top line of the song listing
TR.normal Style for odd-numbered song listing lines
TR.highlight Style for even-numbered song listing lines
.directory Style for the title of the current directory
.subdirectory Style for the title of subdirectories
P Ordinary paragraphs
A Links
INPUT Fill-out form fields
=head2 Subclassing this Module
For more extensive customization, you can subclass this module. The
Apache::MP3::Sorted module illustrates how to do this.
Briefly, your module should inherit from Apache::MP3 (or
Apache::MP3::Sorted) either by setting the C<@ISA> package global or,
in Perl 5.6 and higher, with the C