| 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, E |
||||||
| 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; |