4
# Copyright (c) 2003 Gil Megidish
6
# This program is free software; you can redistribute it and/or
7
# modify it under the terms of the GNU General Public License
8
# as published by the Free Software Foundation; either version 2
9
# of the License, or (at your option) any later version.
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
# GNU General Public License for more details.
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
26
my $def_agent = "icecream/0.8";
27
my $version = "icecream/0.8";
28
my $accept_header = "audio/mpeg, audio/x-mpegurl, audio/x-scpls, */*";
30
my $def_timeout = 500;
34
return if (! defined $config->{'stop-cond'});
36
$config->{'stop-cond'} =~ /^(\d+)(\w+)$/;
40
my $kb = $config->{'bytes-downloaded'} / 1024;
43
$config->{stop} = ($kb >= $count);
44
} elsif ($units eq 'mb') {
45
$config->{stop} = ($kb >= ($count * 1024));
46
} elsif ($units eq 'min') {
47
my $elapsed = (time() - $config->{'start-time'}) / 60;
48
$config->{stop} = ($elapsed >= $count);
49
} elsif ($units eq 'songs') {
50
$config->{stop} = ($config->{'played-tracks'} >= $count);
53
die "unhandled unit $units\n";
57
sub parse_m3u_playlist
59
my ($playlist) = shift || return undef;
60
my (@lines) = split('\n', $playlist);
64
foreach my $s (@lines) {
75
sub parse_pls_playlist
77
my ($playlist) = shift || return undef;
78
my (@lines) = split('\n', $playlist);
84
# parse_pls_playlist parses a .pls playlist, and
85
# returns a vector of all links in content
88
if (! defined $line || $line !~ /^\[playlist\]/i) {
89
# not a valid playlist
90
print STDERR "invalid playlist file\n";
99
while (defined $line) {
101
my ($property, $id, $value);
103
# now expecting FileX, TitleX and LengthX
104
if ($line =~ /^(\w+)(\d+)=(.+)$/) {
113
# ids are supposed to go up
125
# add property to hash
126
$property = lc $property;
127
$entry->{$property} = $value;
131
$line = shift @lines;
134
push @queue, $entry if $dirty;
140
my ($filename) = shift || return undef;
143
open(SLURPEE, "<$filename") || return undef;
145
# set delimiter to undef, next read will load the
146
# entire file into memory
158
my ($handle) = shift || return 0;
159
my ($timeout) = shift || return 0;
162
vec($v, fileno($handle), 1) = 1;
163
return select($v, $v, $v, $timeout / 1000.0);
168
my ($handle) = shift || return undef;
169
my ($cnt) = shift || return undef;
174
my ($chunk, $chunksize);
177
$next_chunk = ($cnt > 0) ? $cnt : 1024;
179
if (select_socket($handle, $def_timeout) <= 0) {
185
$handle->recv($chunk, $next_chunk);
186
$chunksize = length($chunk);
187
if ($chunksize == 0) {
188
# error occured, or end of stream
195
# paranoia, what if a bigger chunk is received
196
$cnt = 0 unless $cnt > 0;
204
my ($url) = shift || return undef;
205
my ($host, $port, $path);
209
if ($url =~ /^([\d\w\._\-]+)(:\d+)??(\/.*)??$/) {
213
# port includes the colon
214
$port = substr($2, 1);
221
print "*** UNPARSABLE ***\n";
225
return ($host, $port, $path);
230
my ($location) = shift || return undef;
231
my ($host, $port, $path);
233
my ($data, $request);
235
debug("slurping http resource at $location");
238
($host, $port, $path) = split_url($location);
241
return undef unless defined $host;
242
$port = 80 unless defined $port;
243
$path = "/" unless defined $path;
245
debug("retreiving from $host $port $path");
247
$sock = IO::Socket::INET->new(PeerAddr => $host,
252
return undef unless defined $sock;
256
my $agent = $config->{'user-agent'};
257
$request = "GET $path HTTP/1.0\r\n" .
258
"Host: $host:$port\r\n" .
259
"Accept: ${accept_header}\r\n" .
260
"User-Agent: $agent\r\n" .
263
debug("sending request to server", $request);
264
print $sock $request;
266
$data = recv_chunk($sock, -1);
269
debug("data retreived from server", $data);
275
my ($message) = shift || return undef;
278
($header, $body) = split("\r\n\r\n", $message, 2);
282
sub extract_status_code
284
my ($message) = shift || return undef;
286
if ($message !~ /^(.+)\s+(\d+)/) {
295
my ($message) = shift || return undef;
296
my ($headers, $body);
298
($headers, $body) = split("\r\n\r\n", $message, 2);
299
return undef unless defined $headers;
301
if ($headers =~ /\nLocation:\s*(.+)\n/i) {
305
# uhm? where did it go?
309
sub retreive_http_playlist
311
my ($location) = shift || return undef;
317
$response = slurp_http($location);
318
return undef unless defined $response;
320
$status = extract_status_code($response);
321
if (! defined $status) {
326
if ($status == 200) {
328
return get_http_body($response);
331
if ($status == 302) {
334
$location = get_302_location($response);
335
print "new location $location\n";
339
# 404, 5XX and anything else
344
sub retreive_playlist
346
my ($location) = shift || return undef;
348
if ($location =~ /^(\w+):\/\/(.+)$/) {
353
if ($protocol eq "file") {
354
# local file requested
355
return slurp_file($url);
358
if ($protocol eq "http") {
360
return retreive_http_playlist($url);
367
# no protocol specified, assuming local file
368
return slurp_file($location);
373
my ($sock) = shift || return undef;
374
my ($max_length) = shift || -1;
378
return "" if ($max_length == 0);
380
$data = recv_chunk($sock, 1);
381
while (defined $data) {
384
last if $headers =~ /\r\n\r\n/;
386
if ($max_length != -1 && length($headers) >= $max_length) {
387
# just enough (we're reading one byte at a time)
391
$data = recv_chunk($sock, 1);
399
my ($str) = shift || return undef;
401
$str =~ s/^[\s\t]//g;
402
$str =~ s/[\s\t]$//g;
406
sub parse_stream_headers
408
my ($headers) = shift || return undef;
409
my (@lines) = split('\n', $headers);
412
foreach my $line (@lines) {
416
if ($line =~ /^\s*([\w\-]+)\s*\:\s*(.+)\s*$/) {
422
$value = trim($value);
424
$server->{$key} = $value;
434
my ($meta) = shift || return undef;
436
if ($meta =~ /StreamTitle='(.+){1}'/) {
439
$title =~ s/\';(.*)$//;
448
my ($sock) = shift || return undef;
452
$block_size = recv_chunk($sock, 1);
453
$block_size = ord($block_size) * 16;
454
return "" if ($block_size == 0);
456
$data = recv_chunk($sock, $block_size);
462
my ($fn) = shift || return undef;
464
# remove all characters that cause problems
465
# on unices and on windows
466
$fn =~ s/[\\\/\?\*\:\t\n\r]//g;
472
my ($context) = shift || return 0;
473
my ($fn) = shift || return 0;
475
$fn = fix_filename($fn);
476
open(OUTPUT, ">$fn") || die "FIXME: ";
477
OUTPUT->autoflush(1);
479
$context->{output_open} = 1;
485
my ($chunk) = shift || return;
486
my ($context) = shift || return;
488
if ($config->{stdout} == 1) {
493
if ($context->{output_open} == 0 && $context->{title} ne '') {
496
if ($context->{id} != 0) {
497
$trackid = sprintf "%02d - ", $context->{id};
500
my $fn = $trackid . $context->{title} . '.mp3';
501
return unless open_output($context, $fn);
504
if ($context->{output_open} == 1) {
511
my ($context) = shift;
513
if ($config->{quiet} == 1) {
518
if (defined $context) {
519
if ($context->{length} > 0) {
522
if ($context->{id} != 0) {
523
$trackid = sprintf "%02d - ", $context->{id};
526
my ($kb) = int(($context->{length} + 1023) / 1024);
527
print "\r${trackid}$context->{title} [$kb K]";
536
sub close_output_stream
538
my $context = shift || return;
540
# close old output stream
541
$context->{output_open} = 0;
547
my ($context) = shift || return 0;
548
my ($newtitle) = shift || return 0;
550
if ($newtitle eq $context->{title}) {
551
# still playing the same track
555
if ($context->{title} ne '') {
558
$config->{'played-tracks'}++;
561
$context->{title} = $newtitle;
564
if ($config->{tracks} == 0) {
565
# there is no need to switch output stream
569
# reset track information
570
$context->{length} = 0;
571
$context->{id} = $context->{id} + 1;
573
close_output_stream($context);
577
sub loop_named_stream
579
my ($sock) = shift || return 0;
580
my ($stream) = shift || return 0;
583
$context->{id} = find_latest_index(".");
584
$context->{title} = '';
585
$context->{length} = 0;
586
$context->{output_open} = 0;
588
if ($config->{tracks} == 0) {
589
# single audio track of whatever is received
590
$context->{title} = $stream->{'name'};
593
# load all data upto the first metaint if -t is set
594
if ($config->{tracks} && $config->{stdout} == 0) {
600
while ($config->{stop} == 0) {
601
my $chunk = recv_chunk($sock, $stream->{'metaint'});
602
if (length($chunk) < $stream->{'metaint'}) {
603
print "got a problem here..\n";
608
$metablock = recv_metablock($sock);
609
$title = parse_meta($metablock);
610
last if defined $title;
613
set_title($context, $title);
614
$context->{length} += length($huge);
615
write_block($huge, $context);
616
print_title($context);
621
last if ($config->{stop} != 0);
623
my $chunk = recv_chunk($sock, $stream->{'metaint'});
624
if (length($chunk) < $stream->{'metaint'}) {
625
print "got a problem here..\n";
630
$config->{'bytes-downloaded'} += length($chunk);
632
write_block($chunk, $context);
633
print_title($context);
635
$context->{length} += length($chunk);
637
my $metablock = recv_metablock($sock);
639
# update current track; do whatever is needed if
640
# a track has changed
641
my $title = parse_meta($metablock);
642
set_title($context, $title) if defined $title;
648
sub loop_anonymous_stream
650
my ($sock) = shift || return 0;
651
my ($stream) = shift || return 0;
654
debug("loop_anonymous_stream()");
657
$context->{title} = $stream->{name};
658
$context->{length} = 0;
659
$context->{output_open} = 0;
663
last if ($config->{stop} != 0);
665
my $chunk = recv_chunk($sock, 1024);
666
last unless length($chunk) > 0;
669
$config->{'bytes-downloaded'} += length($chunk);
671
$context->{length} += length($chunk);
672
write_block($chunk, $context);
673
print_title($context);
676
debug("loop_anonymous_stream ended");
682
my ($url) = shift || return undef;
684
if ($url =~ /^\w+:\/\/(.+)$/) {
693
my ($url) = shift || return undef;
695
if ($url =~ /^(\w+):\/\//) {
702
sub prepare_stream_data
704
my ($raw) = shift || return undef;
708
if (defined $raw->{'icy-name'}) {
709
$out->{name} = $raw->{'icy-name'};
712
if (defined $raw->{'icy-metaint'}) {
713
$out->{metaint} = $raw->{'icy-metaint'};
716
if (defined $raw->{'icy-genre'}) {
717
$out->{genre} = $raw->{'icy-genre'};
721
if (defined $raw->{'x-audiocast-genre'}) {
722
$out->{genre} = $raw->{'x-audiocast-genre'};
725
if (defined $raw->{'x-audiocast-name'}) {
726
$out->{name} = $raw->{'x-audiocast-name'};
734
my ($location) = shift || return 0;
735
my ($host, $port, $path);
736
my ($sock, $headers);
741
if (split_protocol($location) ne "http") {
742
print STDERR "error: not an http location $location\n";
746
$location = strip_protocol($location);
748
# XXX: note: can clean this (too much code)
751
($host, $port, $path) = split_url($location);
754
if (! defined $host) {
755
print STDERR "error parsing url $location\n";
759
$port = 80 unless defined $port;
760
$path = "/" unless defined $path;
762
$sock = IO::Socket::INET->new(PeerAddr => $host,
765
if (! defined $sock) {
766
print STDERR "error connecting to $host:$port\n";
770
my $agent = $config->{'user-agent'};
771
my $request = "GET $path HTTP/1.0\r\n" .
772
"Icy-MetaData:1\r\n" .
773
"User-Agent:$agent\r\n" .
776
debug("sending request to server", $request);
777
print $sock $request;
779
$headers = slurp_headers($sock);
780
if (! defined $headers) {
781
print STDERR "error retreiving response from server\n";
785
debug("data retreived from server", $headers);
787
$status = extract_status_code($headers);
788
if (! defined $status) {
789
print STDERR "error parsing server response (use --debug)\n";
793
if ($status == 302) {
795
$location = get_302_location($headers);
799
if ($status == 400) {
801
print STDERR "error: server is full (use --debug for complete response)\n";
805
if ($status != 200) {
806
# nothing works fine these days
807
print STDERR "error: server error $status (use --debug for complete response)\n";
811
} while ($status != 200);
813
if ($headers =~ /^HTTP/) {
814
# ICY is embedded inside an HTTP
815
$headers = slurp_headers($sock);
816
return 0 unless defined $headers;
819
my $raw_stream_data = parse_stream_headers($headers);
820
if (! defined $raw_stream_data) {
821
print STDERR "error: problems parsing stream headers (please use --debug)\n";
825
my $stream_data = prepare_stream_data($raw_stream_data);
826
if (! defined $stream_data->{'name'}) {
827
print STDERR "error: not an icecast/shoutcast stream\n";
831
if ($config->{debug}) {
832
my $info = "name: $stream_data->{name}\n";
833
$info .= "genre: $stream_data->{genre}\n" if defined $stream_data->{genre};
834
$info .= "metaint: $stream_data->{metaint}\n" if defined $stream_data->{metaint};
835
debug("parsed stream headers", $info);
838
if (defined $stream_data->{'metaint'}) {
839
# server periodically sends stream title
840
loop_named_stream($sock, $stream_data);
843
# no titles for tracks
844
loop_anonymous_stream($sock, $stream_data);
859
print "usage: icecream [options] URL [URL...]\n";
862
print " -h, --help print this message\n";
863
print " -q, --quiet no printouts\n";
864
print " -v, --verbose be verbose\n";
865
print " -s, --stdout output tracks to stdout (implies quiet)\n";
866
print " -t, --tracks split into tracks when saving\n";
867
print " --stop=N[units] stop after N (kb, mb, min, songs)\n";
868
print " --debug turn on debugging\n";
869
print " --useragent=AGENT identify as AGENT stead of ${def_agent}\n";
878
GetOptions(\%options, "--help", "--quiet", "--verbose", "--stdout", "--tracks", "--debug", "--user-agent=s", "--stop=s");
880
$config->{help} = (defined $options{help}) ? 1 : 0;
881
$config->{quiet} = (defined $options{quiet}) ? 1 : 0;
882
$config->{verbose} = (defined $options{verbose}) ? 1 : 0;
883
$config->{debug} = (defined $options{debug}) ? 1 : 0;
884
$config->{stdout} = (defined $options{stdout}) ? 1 : 0;
885
$config->{tracks} = (defined $options{tracks}) ? 1 : 0;
886
$config->{'user-agent'} = (defined $options{'user-agent'}) ? $options{'user-agent'} : ${def_agent};
887
$config->{'stop-cond'} = (defined $options{stop}) ? lc $options{stop} : undef;
889
# stdout implies quiet
890
if ($config->{stdout} == 1) {
891
# stdout implies quiet
892
$config->{quiet} = 1;
893
$config->{debug} = 0;
896
# validate stop condition
897
if (defined $config->{'stop-cond'}) {
899
if ($config->{'stop-cond'} =~ /^(\d+)(\w+)$/) {
901
if ($cond eq 'min' || $cond eq 'songs' ||
902
$cond eq 'kb' || $cond eq 'mb') {
907
if ($cond_valid == 0)
909
print STDERR "error parsing stop condition $config->{'stop-cond'}\n";
914
$config->{urls} = join("\n", @ARGV);
920
my $s = shift || return;
922
if ($config->{verbose} == 1) {
929
my $title = shift || return;
930
my $additional = shift;
932
if ($config->{debug}) {
933
print "[ $title ]\n";
934
if (defined $additional) {
935
my @ar = split("\n", $additional);
936
foreach my $s (@ar) {
945
my ($url) = shift || return undef;
946
my ($config) = shift || return undef;
948
my $raw = retreive_playlist($url);
949
if (! defined $raw) {
950
print STDERR "error: failed to retreive playlist from $url\n";
956
if ($url =~ /\.m3u$/) {
957
@pls = parse_m3u_playlist($raw);
959
@pls = parse_pls_playlist($raw);
962
debug("play list parsed");
964
$config->{'stop'} = 0;
965
$config->{'played-tracks'} = 0;
966
$config->{'bytes-downloaded'} = 0;
967
$config->{'start-time'} = time();
970
foreach $entry (@pls) {
972
if ($config->{verbose}) {
973
print "[ playing $entry->{file} ]\n";
976
start_stream($entry->{file});
977
last if ($config->{stop} != 0);
988
$config = parse_options();
989
if (! defined $config) {
990
# there was an error parsing parameters
994
help() if ($config->{help} == 1);
996
@queue = split("\n", $config->{urls});
997
help() unless @queue > 0;
999
foreach $url (@queue) {
1000
process($url, $config);
1007
print STDERR "nothing was played";
1011
sub find_latest_index
1013
my ($location) = shift || return 0;
1017
opendir(DIR, $location) || return $id;
1018
while ($fn = readdir(DIR)) {
1020
if ($fn =~ /^(\d+)\s+.*\.mp3$/) {
1021
$id = $1 if ($id < $1);
1039
icecream - listen to, or download icecast streams
1043
icecream [OPTIONS] URL [URL..]
1047
icecream is a non-interactive stream download utility. It connects
1048
to icecast and shoutcast servers and redirects all fetched content
1049
to an stdin-capable player or to media files on your disk. With an
1050
option turned on, it can save the stream into different files, each
1051
representing the played track. It is also possible to tee the input
1052
to both disk and stdout.
1058
=item B<-h>, B<--help>
1060
Print a help message describing all options
1062
=item B<-q>, B<--quiet>
1066
=item B<-v>, B<--verbose>
1070
=item B<-s>, B<--stdout>
1072
Output stream to stdout (implies -q)
1074
=item B<-t>, B<--tracks>
1076
Split stream into tracks (if possible)
1078
=item B<--stop=N[units]>
1080
Stop stream after N min(minutes), songs or KB/MB transferred
1084
Turn on debugging outputs
1086
=item B<--useragent=AGENT>
1088
Set useragent header to AGENT
1096
=item Streaming to mpg123
1098
icecream -s http://radio.com/playlist.pls | mpg123 -
1100
=item Split stream into different tracks
1102
icecream -t http://metal.org/radio.pls
1104
=item Prepare a 74 minute CD
1106
icecream -t --stop 74min http://trance.net/playlist.m3u
1112
You are welcome to send bug reports about icecream to our mailing
1113
list. Feel free to visit http://icecream.sourceforge.net
1117
Written by Gil Megidish <gmegidis@ort.org.il>