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