~ubuntu-branches/ubuntu/feisty/icecream/feisty

« back to all changes in this revision

Viewing changes to icecream

  • Committer: Bazaar Package Importer
  • Author(s): Christoph Siess (CHS)
  • Date: 2004-01-31 14:20:33 UTC
  • Revision ID: james.westby@ubuntu.com-20040131142033-ctzv5bse5fnjjit6
Tags: upstream-0.8
ImportĀ upstreamĀ versionĀ 0.8

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/perl -w 
 
2
#
 
3
# icecream 0.8
 
4
# Copyright (c) 2003 Gil Megidish
 
5
#
 
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.
 
10
#
 
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.
 
15
#
 
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.
 
19
 
 
20
use strict; 
 
21
use IO::Socket; 
 
22
use Getopt::Long;
 
23
 
 
24
my $config = {};
 
25
 
 
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, */*";
 
29
 
 
30
my $def_timeout = 500;
 
31
                
 
32
sub check_stop_cond
 
33
{
 
34
        return if (! defined $config->{'stop-cond'});
 
35
 
 
36
        $config->{'stop-cond'} =~ /^(\d+)(\w+)$/;
 
37
        
 
38
        my $count = $1;
 
39
        my $units = $2;
 
40
        my $kb = $config->{'bytes-downloaded'} / 1024;
 
41
 
 
42
        if ($units eq 'kb') {
 
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);
 
51
        } else {
 
52
                # wadda hell ?
 
53
                die "unhandled unit $units\n";
 
54
        }
 
55
}
 
56
 
 
57
sub parse_m3u_playlist
 
58
{
 
59
        my ($playlist) = shift || return undef;
 
60
        my (@lines) = split('\n', $playlist);
 
61
        my (@queue) = ();
 
62
        my ($id) = 1;
 
63
 
 
64
        foreach my $s (@lines) {
 
65
 
 
66
                my ($entry) = {};
 
67
                $entry->{id} = $id++;
 
68
                $entry->{file} = $s;
 
69
                push @queue, $entry;
 
70
        }
 
71
 
 
72
        return @queue;
 
73
}
 
74
 
 
75
sub parse_pls_playlist
 
76
{
 
77
        my ($playlist) = shift || return undef;
 
78
        my (@lines) = split('\n', $playlist);
 
79
        my ($line);
 
80
        my ($entry, $dirty);
 
81
        my ($lastid);
 
82
        my (@queue) = ();
 
83
        
 
84
        # parse_pls_playlist parses a .pls playlist, and
 
85
        # returns a vector of all links in content
 
86
 
 
87
        $line = shift @lines;
 
88
        if (! defined $line || $line !~ /^\[playlist\]/i) {
 
89
                # not a valid playlist
 
90
                print STDERR "invalid playlist file\n"; 
 
91
                return undef;
 
92
        }
 
93
 
 
94
        $entry = {};
 
95
        $dirty = 0;
 
96
 
 
97
        $lastid = 1;    
 
98
        $line = shift @lines;
 
99
        while (defined $line) {
 
100
                
 
101
                my ($property, $id, $value);
 
102
                
 
103
                # now expecting FileX, TitleX and LengthX
 
104
                if ($line =~ /^(\w+)(\d+)=(.+)$/) {
 
105
                        
 
106
                        $property = $1;
 
107
                        $id = $2;
 
108
                        $value = $3;
 
109
 
 
110
                        $value =~ s/\s*$//s;
 
111
                        
 
112
                        if ($id < $lastid) {
 
113
                                # ids are supposed to go up
 
114
                                return undef;
 
115
                        }
 
116
                        
 
117
                        if ($id > $lastid) {
 
118
                                # different entry
 
119
                                push @queue, $entry;
 
120
                                $entry = {};
 
121
                                $dirty = 0;
 
122
                                $lastid = $id;
 
123
                        }
 
124
                        
 
125
                        # add property to hash
 
126
                        $property = lc $property;
 
127
                        $entry->{$property} = $value;
 
128
                        $dirty = 1;
 
129
                }
 
130
                
 
131
                $line = shift @lines;
 
132
        }
 
133
        
 
134
        push @queue, $entry if $dirty;
 
135
        return @queue;
 
136
}
 
137
 
 
138
sub slurp_file
 
139
{
 
140
        my ($filename) = shift || return undef;
 
141
        my ($data);
 
142
        
 
143
        open(SLURPEE, "<$filename") || return undef;
 
144
        
 
145
        # set delimiter to undef, next read will load the 
 
146
        # entire file into memory
 
147
        local $/ = undef;
 
148
        
 
149
        # read entire file
 
150
        $data = <SLURPEE>;
 
151
        
 
152
        close SLURPEE;
 
153
        return $data;
 
154
}
 
155
 
 
156
sub select_socket
 
157
{
 
158
        my ($handle) = shift || return 0;
 
159
        my ($timeout) = shift || return 0;
 
160
        my ($v) = '';
 
161
 
 
162
        vec($v, fileno($handle), 1) = 1;
 
163
        return select($v, $v, $v, $timeout / 1000.0);
 
164
}
 
165
 
 
166
sub recv_chunk
 
167
{
 
168
        my ($handle) = shift || return undef;
 
169
        my ($cnt) = shift || return undef;
 
170
        my ($data) = '';
 
171
        
 
172
        while ($cnt != 0) {
 
173
 
 
174
                my ($chunk, $chunksize);
 
175
                my ($next_chunk);
 
176
                
 
177
                $next_chunk = ($cnt > 0) ? $cnt : 1024;
 
178
 
 
179
                if (select_socket($handle, $def_timeout) <= 0) {
 
180
                        # timed out
 
181
                        print "Timedout!\n";
 
182
                        last;
 
183
                }
 
184
                
 
185
                $handle->recv($chunk, $next_chunk);
 
186
                $chunksize = length($chunk);            
 
187
                if ($chunksize == 0) {
 
188
                        # error occured, or end of stream
 
189
                        last;
 
190
                }       
 
191
                
 
192
                $data .= $chunk;
 
193
                $cnt -= $chunksize;
 
194
                
 
195
                # paranoia, what if a bigger chunk is received
 
196
                $cnt = 0 unless $cnt > 0;
 
197
        }
 
198
        
 
199
        return $data;
 
200
}
 
201
 
 
202
sub split_url
 
203
{
 
204
        my ($url) = shift || return undef;
 
205
        my ($host, $port, $path);
 
206
 
 
207
        $port = undef;
 
208
        
 
209
        if ($url =~ /^([\d\w\._\-]+)(:\d+)??(\/.*)??$/) {
 
210
                
 
211
                $host = $1;
 
212
                if (defined $2) {
 
213
                        # port includes the colon
 
214
                        $port = substr($2, 1);
 
215
                }
 
216
                
 
217
                $path = $3;
 
218
        } 
 
219
        else {
 
220
                # unparsable
 
221
                print "*** UNPARSABLE ***\n";
 
222
                return undef;
 
223
        }
 
224
 
 
225
        return ($host, $port, $path);   
 
226
}
 
227
 
 
228
sub slurp_http
 
229
{
 
230
        my ($location) = shift || return undef;
 
231
        my ($host, $port, $path);
 
232
        my ($sock);
 
233
        my ($data, $request);
 
234
 
 
235
        debug("slurping http resource at $location");
 
236
 
 
237
        # parse location        
 
238
        ($host, $port, $path) = split_url($location);
 
239
                
 
240
        # parsing errors?
 
241
        return undef unless defined $host;
 
242
        $port = 80 unless defined $port;
 
243
        $path = "/" unless defined $path;
 
244
 
 
245
        debug("retreiving from $host $port $path");
 
246
 
 
247
        $sock = IO::Socket::INET->new(PeerAddr => $host,
 
248
                                PeerPort => $port,
 
249
                                Proto => 'tcp');
 
250
        
 
251
        # error connecting?
 
252
        return undef unless defined $sock;
 
253
        
 
254
        $sock->autoflush(1);
 
255
        
 
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" .
 
261
                "\r\n";
 
262
 
 
263
        debug("sending request to server", $request);
 
264
        print $sock $request;
 
265
 
 
266
        $data = recv_chunk($sock, -1);
 
267
        $sock->shutdown(2);
 
268
 
 
269
        debug("data retreived from server", $data);
 
270
        return $data;
 
271
}
 
272
 
 
273
sub get_http_body
 
274
{
 
275
        my ($message) = shift || return undef;
 
276
        my ($header, $body);
 
277
        
 
278
        ($header, $body) = split("\r\n\r\n", $message, 2);
 
279
        return $body;
 
280
}
 
281
 
 
282
sub extract_status_code
 
283
{
 
284
        my ($message) = shift || return undef;
 
285
 
 
286
        if ($message !~ /^(.+)\s+(\d+)/) {
 
287
                return undef;
 
288
        }
 
289
        
 
290
        return $2;
 
291
}
 
292
 
 
293
sub get_302_location
 
294
{
 
295
        my ($message) = shift || return undef;
 
296
        my ($headers, $body);
 
297
 
 
298
        ($headers, $body) = split("\r\n\r\n", $message, 2);
 
299
        return undef unless defined $headers;
 
300
 
 
301
        if ($headers =~ /\nLocation:\s*(.+)\n/i) {
 
302
                return $1;
 
303
        }
 
304
 
 
305
        # uhm? where did it go?
 
306
        return undef;
 
307
}
 
308
 
 
309
sub retreive_http_playlist
 
310
{
 
311
        my ($location) = shift || return undef;
 
312
        my ($response);
 
313
        my ($status);
 
314
        
 
315
        while (1) {
 
316
 
 
317
                $response = slurp_http($location);
 
318
                return undef unless defined $response;
 
319
        
 
320
                $status = extract_status_code($response);
 
321
                if (! defined $status) {
 
322
                        # problems parsing
 
323
                        return undef;
 
324
                }
 
325
        
 
326
                if ($status == 200) {
 
327
                        # 200 OK
 
328
                        return get_http_body($response);
 
329
                }
 
330
        
 
331
                if ($status == 302) {
 
332
 
 
333
                        # location moved
 
334
                        $location = get_302_location($response);
 
335
                        print "new location $location\n";
 
336
                        next;
 
337
                }
 
338
 
 
339
                # 404, 5XX and anything else
 
340
                return undef;
 
341
        }
 
342
}
 
343
 
 
344
sub retreive_playlist
 
345
{
 
346
        my ($location) = shift || return undef;
 
347
        
 
348
        if ($location =~ /^(\w+):\/\/(.+)$/) {
 
349
                
 
350
                my $protocol = $1;
 
351
                my $url = $2;
 
352
                
 
353
                if ($protocol eq "file") {
 
354
                        # local file requested
 
355
                        return slurp_file($url);
 
356
                }
 
357
                
 
358
                if ($protocol eq "http") {
 
359
                        # remote http file
 
360
                        return retreive_http_playlist($url);
 
361
                }
 
362
                
 
363
                # unknown protocol
 
364
                return undef;
 
365
        }
 
366
        
 
367
        # no protocol specified, assuming local file
 
368
        return slurp_file($location);
 
369
}
 
370
 
 
371
sub slurp_headers
 
372
{
 
373
        my ($sock) = shift || return undef;
 
374
        my ($max_length) = shift || -1;
 
375
        my ($data);
 
376
        my ($headers) = '';
 
377
        
 
378
        return "" if ($max_length == 0);
 
379
        
 
380
        $data = recv_chunk($sock, 1);
 
381
        while (defined $data) {
 
382
                
 
383
                $headers .= $data;
 
384
                last if $headers =~ /\r\n\r\n/;
 
385
                
 
386
                if ($max_length != -1 && length($headers) >= $max_length) {
 
387
                        # just enough (we're reading one byte at a time)
 
388
                        last;
 
389
                }
 
390
                        
 
391
                $data = recv_chunk($sock, 1);
 
392
        }
 
393
        
 
394
        return $headers;
 
395
}
 
396
 
 
397
sub trim
 
398
{
 
399
        my ($str) = shift || return undef;
 
400
        
 
401
        $str =~ s/^[\s\t]//g;
 
402
        $str =~ s/[\s\t]$//g;
 
403
        return $str;
 
404
}
 
405
 
 
406
sub parse_stream_headers
 
407
{
 
408
        my ($headers) = shift || return undef;
 
409
        my (@lines) = split('\n', $headers);
 
410
        my ($server) = {};
 
411
        
 
412
        foreach my $line (@lines) {
 
413
                
 
414
                my ($key, $value);
 
415
                
 
416
                if ($line =~ /^\s*([\w\-]+)\s*\:\s*(.+)\s*$/) {
 
417
                        
 
418
                        $key = $1;
 
419
                        $value = $2;
 
420
                        
 
421
                        $key = trim($key);
 
422
                        $value = trim($value);
 
423
                        
 
424
                        $server->{$key} = $value;
 
425
                }
 
426
                
 
427
        }
 
428
        
 
429
        return $server;
 
430
}
 
431
 
 
432
sub parse_meta
 
433
{
 
434
        my ($meta) = shift || return undef;
 
435
        
 
436
        if ($meta =~ /StreamTitle='(.+){1}'/) {
 
437
                
 
438
                my $title = $1;
 
439
                $title =~ s/\';(.*)$//;
 
440
                return $title;
 
441
        }
 
442
        
 
443
        return undef;
 
444
}
 
445
 
 
446
sub recv_metablock
 
447
{
 
448
        my ($sock) = shift || return undef;
 
449
        my ($block_size);
 
450
        my ($data);
 
451
        
 
452
        $block_size = recv_chunk($sock, 1);
 
453
        $block_size = ord($block_size) * 16;
 
454
        return "" if ($block_size == 0);
 
455
        
 
456
        $data = recv_chunk($sock, $block_size);
 
457
        return $data;
 
458
}
 
459
 
 
460
sub fix_filename
 
461
{
 
462
        my ($fn) = shift || return undef;
 
463
 
 
464
        # remove all characters that cause problems
 
465
        # on unices and on windows
 
466
        $fn =~ s/[\\\/\?\*\:\t\n\r]//g;
 
467
        return $fn;
 
468
}
 
469
 
 
470
sub open_output
 
471
{
 
472
        my ($context) = shift || return 0;
 
473
        my ($fn) = shift || return 0;
 
474
 
 
475
        $fn = fix_filename($fn);
 
476
        open(OUTPUT, ">$fn") || die "FIXME: ";
 
477
        OUTPUT->autoflush(1);
 
478
        binmode OUTPUT;
 
479
        $context->{output_open} = 1;
 
480
        return 1;
 
481
}
 
482
 
 
483
sub write_block
 
484
{
 
485
        my ($chunk) = shift || return;
 
486
        my ($context) = shift || return;
 
487
 
 
488
        if ($config->{stdout} == 1) {
 
489
                print $chunk;
 
490
                return;
 
491
        }
 
492
 
 
493
        if ($context->{output_open} == 0 && $context->{title} ne '') {
 
494
 
 
495
                my $trackid = '';
 
496
                if ($context->{id} != 0) {
 
497
                        $trackid = sprintf "%02d - ", $context->{id};
 
498
                }
 
499
 
 
500
                my $fn = $trackid . $context->{title} . '.mp3';
 
501
                return unless open_output($context, $fn);
 
502
        }
 
503
 
 
504
        if ($context->{output_open} == 1) {
 
505
                print OUTPUT $chunk;
 
506
        }
 
507
}
 
508
 
 
509
sub print_title
 
510
{
 
511
        my ($context) = shift;
 
512
        
 
513
        if ($config->{quiet} == 1) {
 
514
                # quiet!
 
515
                return;
 
516
        }
 
517
 
 
518
        if (defined $context) { 
 
519
                if ($context->{length} > 0) {
 
520
 
 
521
                        my $trackid = '';
 
522
                        if ($context->{id} != 0) {
 
523
                                $trackid = sprintf "%02d - ", $context->{id};
 
524
                        }
 
525
 
 
526
                        my ($kb) = int(($context->{length} + 1023) / 1024);
 
527
                        print "\r${trackid}$context->{title} [$kb K]";
 
528
                }
 
529
 
 
530
        } 
 
531
        else {
 
532
                print "\n";
 
533
        }
 
534
}
 
535
 
 
536
sub close_output_stream
 
537
{
 
538
        my $context = shift || return;
 
539
 
 
540
        # close old output stream
 
541
        $context->{output_open} = 0;
 
542
        close OUTPUT;
 
543
}
 
544
 
 
545
sub set_title
 
546
{
 
547
        my ($context) = shift || return 0;
 
548
        my ($newtitle) = shift || return 0;
 
549
        
 
550
        if ($newtitle eq $context->{title}) {
 
551
                # still playing the same track
 
552
                return 0;
 
553
        }
 
554
 
 
555
        if ($context->{title} ne '') {
 
556
                # new track
 
557
                print_title();
 
558
                $config->{'played-tracks'}++;
 
559
        }
 
560
 
 
561
        $context->{title} = $newtitle;  
 
562
        
 
563
        # track has changed
 
564
        if ($config->{tracks} == 0) {
 
565
                # there is no need to switch output stream
 
566
                return 1;
 
567
        }
 
568
 
 
569
        # reset track information
 
570
        $context->{length} = 0;
 
571
        $context->{id} = $context->{id} + 1;
 
572
 
 
573
        close_output_stream($context);
 
574
        return 1;
 
575
}
 
576
 
 
577
sub loop_named_stream
 
578
{
 
579
        my ($sock) = shift || return 0;
 
580
        my ($stream) = shift || return 0;
 
581
        my ($context) = {};
 
582
 
 
583
        $context->{id} = find_latest_index(".");
 
584
        $context->{title} = '';
 
585
        $context->{length} = 0;
 
586
        $context->{output_open} = 0;
 
587
 
 
588
        if ($config->{tracks} == 0) {
 
589
                # single audio track of whatever is received
 
590
                $context->{title} = $stream->{'name'};
 
591
        }
 
592
 
 
593
        # load all data upto the first metaint if -t is set
 
594
        if ($config->{tracks} && $config->{stdout} == 0) {
 
595
 
 
596
                my $huge = "";
 
597
                my $metablock;
 
598
                my $title;
 
599
 
 
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";
 
604
                                return 0;
 
605
                        }
 
606
 
 
607
                        $huge .= $chunk;
 
608
                        $metablock = recv_metablock($sock);
 
609
                        $title = parse_meta($metablock);
 
610
                        last if defined $title;
 
611
                }
 
612
 
 
613
                set_title($context, $title);
 
614
                $context->{length} += length($huge);            
 
615
                write_block($huge, $context);
 
616
                print_title($context);
 
617
        }
 
618
        
 
619
        while (1) {
 
620
                check_stop_cond();
 
621
                last if ($config->{stop} != 0);
 
622
                        
 
623
                my $chunk = recv_chunk($sock, $stream->{'metaint'});
 
624
                if (length($chunk) < $stream->{'metaint'}) {
 
625
                        print "got a problem here..\n";
 
626
                        return 0;
 
627
                }
 
628
 
 
629
                # update statistics
 
630
                $config->{'bytes-downloaded'} += length($chunk);
 
631
                
 
632
                write_block($chunk, $context);
 
633
                print_title($context);
 
634
                
 
635
                $context->{length} += length($chunk);
 
636
                
 
637
                my $metablock = recv_metablock($sock);
 
638
                
 
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;
 
643
        }
 
644
        
 
645
        return 1;
 
646
}
 
647
 
 
648
sub loop_anonymous_stream
 
649
{
 
650
        my ($sock) = shift || return 0;
 
651
        my ($stream) = shift || return 0;
 
652
        my ($context) = {};
 
653
 
 
654
        debug("loop_anonymous_stream()");
 
655
 
 
656
        $context->{id} = 0;
 
657
        $context->{title} = $stream->{name};
 
658
        $context->{length} = 0;
 
659
        $context->{output_open} = 0;
 
660
 
 
661
        while (1) {
 
662
                check_stop_cond();
 
663
                last if ($config->{stop} != 0);
 
664
 
 
665
                my $chunk = recv_chunk($sock, 1024);
 
666
                last unless length($chunk) > 0;
 
667
 
 
668
                # update statistics
 
669
                $config->{'bytes-downloaded'} += length($chunk);
 
670
 
 
671
                $context->{length} += length($chunk);
 
672
                write_block($chunk, $context);
 
673
                print_title($context);
 
674
        }
 
675
        
 
676
        debug("loop_anonymous_stream ended");
 
677
        return 1;
 
678
}
 
679
 
 
680
sub strip_protocol
 
681
{
 
682
        my ($url) = shift || return undef;
 
683
 
 
684
        if ($url =~ /^\w+:\/\/(.+)$/) {
 
685
                return $1;
 
686
        }
 
687
 
 
688
        return $url;
 
689
}
 
690
                
 
691
sub split_protocol
 
692
{
 
693
        my ($url) = shift || return undef;
 
694
 
 
695
        if ($url =~ /^(\w+):\/\//) {
 
696
                return $1;
 
697
        }
 
698
 
 
699
        return undef;
 
700
}
 
701
 
 
702
sub prepare_stream_data
 
703
{
 
704
        my ($raw) = shift || return undef;
 
705
        my ($out) = ();
 
706
 
 
707
        # ICY protocol
 
708
        if (defined $raw->{'icy-name'}) {
 
709
                $out->{name} = $raw->{'icy-name'};
 
710
        }
 
711
 
 
712
        if (defined $raw->{'icy-metaint'}) {
 
713
                $out->{metaint} = $raw->{'icy-metaint'};
 
714
        }
 
715
 
 
716
        if (defined $raw->{'icy-genre'}) {
 
717
                $out->{genre} = $raw->{'icy-genre'};
 
718
        }
 
719
 
 
720
        # Shoutcast protocol
 
721
        if (defined $raw->{'x-audiocast-genre'}) {
 
722
                $out->{genre} = $raw->{'x-audiocast-genre'};
 
723
        }
 
724
 
 
725
        if (defined $raw->{'x-audiocast-name'}) {
 
726
                $out->{name} = $raw->{'x-audiocast-name'};
 
727
        }
 
728
 
 
729
        return $out;
 
730
}
 
731
 
 
732
sub start_stream
 
733
{
 
734
        my ($location) = shift || return 0;
 
735
        my ($host, $port, $path);
 
736
        my ($sock, $headers);
 
737
        my ($status);
 
738
 
 
739
        do
 
740
        {       
 
741
                if (split_protocol($location) ne "http") {
 
742
                        print STDERR "error: not an http location $location\n";
 
743
                        return 0;
 
744
                }
 
745
 
 
746
                $location = strip_protocol($location);
 
747
 
 
748
                # XXX: note: can clean this (too much code)
 
749
        
 
750
                # parse location
 
751
                ($host, $port, $path) = split_url($location);
 
752
        
 
753
                # parsing errors?
 
754
                if (! defined $host) {
 
755
                        print STDERR "error parsing url $location\n";
 
756
                        return 0;
 
757
                }
 
758
 
 
759
                $port = 80 unless defined $port;
 
760
                $path = "/" unless defined $path;
 
761
        
 
762
                $sock = IO::Socket::INET->new(PeerAddr => $host,
 
763
                                        PeerPort => $port,
 
764
                                        Proto => 'tcp');        
 
765
                if (! defined $sock) {
 
766
                        print STDERR "error connecting to $host:$port\n";
 
767
                        return 0;
 
768
                }
 
769
 
 
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" .
 
774
                        "\r\n";
 
775
 
 
776
                debug("sending request to server", $request);
 
777
                print $sock $request;
 
778
 
 
779
                $headers = slurp_headers($sock);
 
780
                if (! defined $headers) {
 
781
                        print STDERR "error retreiving response from server\n";
 
782
                        return 0;
 
783
                }
 
784
 
 
785
                debug("data retreived from server", $headers);
 
786
 
 
787
                $status = extract_status_code($headers);
 
788
                if (! defined $status) {
 
789
                        print STDERR "error parsing server response (use --debug)\n";
 
790
                        return 0;
 
791
                }
 
792
 
 
793
                if ($status == 302) {
 
794
                        # relocated
 
795
                        $location = get_302_location($headers);
 
796
                        next;
 
797
                }
 
798
 
 
799
                if ($status == 400) {
 
800
                        # server full
 
801
                        print STDERR "error: server is full (use --debug for complete response)\n";
 
802
                        return 0;
 
803
                }
 
804
 
 
805
                if ($status != 200) {
 
806
                        # nothing works fine these days
 
807
                        print STDERR "error: server error $status (use --debug for complete response)\n";
 
808
                        return 0;
 
809
                }
 
810
 
 
811
        } while ($status != 200);
 
812
 
 
813
        if ($headers =~ /^HTTP/) {
 
814
                # ICY is embedded inside an HTTP
 
815
                $headers = slurp_headers($sock);
 
816
                return 0 unless defined $headers;
 
817
        }
 
818
 
 
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";
 
822
                return 0;
 
823
        }
 
824
 
 
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";
 
828
                return 0;
 
829
        }
 
830
 
 
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);
 
836
        }
 
837
        
 
838
        if (defined $stream_data->{'metaint'}) {
 
839
                # server periodically sends stream title
 
840
                loop_named_stream($sock, $stream_data);
 
841
        } 
 
842
        else {
 
843
                # no titles for tracks
 
844
                loop_anonymous_stream($sock, $stream_data);
 
845
        }
 
846
        
 
847
        return 1;
 
848
}
 
849
 
 
850
sub banner()
 
851
{
 
852
        print "$version\n";
 
853
}
 
854
 
 
855
sub help()
 
856
{
 
857
        banner();
 
858
        
 
859
        print "usage: icecream [options] URL [URL...]\n";
 
860
        print "\n";
 
861
        print "options:\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";
 
870
        exit 0;
 
871
}
 
872
 
 
873
sub parse_options
 
874
{
 
875
        my (%options) = ();
 
876
        my ($config) = {};
 
877
        
 
878
        GetOptions(\%options, "--help", "--quiet", "--verbose", "--stdout", "--tracks", "--debug", "--user-agent=s", "--stop=s");
 
879
 
 
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;
 
888
 
 
889
        # stdout implies quiet
 
890
        if ($config->{stdout} == 1) {
 
891
                # stdout implies quiet
 
892
                $config->{quiet} = 1;
 
893
                $config->{debug} = 0;
 
894
        }
 
895
 
 
896
        # validate stop condition
 
897
        if (defined $config->{'stop-cond'}) {
 
898
                my $cond_valid = 0;
 
899
                if ($config->{'stop-cond'} =~ /^(\d+)(\w+)$/) {
 
900
                        my $cond = $2;
 
901
                        if ($cond eq 'min' || $cond eq 'songs' || 
 
902
                        $cond eq 'kb' || $cond eq 'mb') {
 
903
                                $cond_valid = 1;
 
904
                        }
 
905
                }
 
906
 
 
907
                if ($cond_valid == 0)
 
908
                {
 
909
                        print STDERR "error parsing stop condition $config->{'stop-cond'}\n";
 
910
                        return undef;
 
911
                }
 
912
        }
 
913
 
 
914
        $config->{urls} = join("\n", @ARGV);
 
915
        return $config;
 
916
}
 
917
 
 
918
sub verbose
 
919
{
 
920
        my $s = shift || return;
 
921
 
 
922
        if ($config->{verbose} == 1) {
 
923
                print STDERR "$s";
 
924
        }
 
925
}
 
926
 
 
927
sub debug
 
928
{
 
929
        my $title = shift || return;
 
930
        my $additional = shift;
 
931
 
 
932
        if ($config->{debug}) {
 
933
        print "[ $title ]\n";
 
934
                if (defined $additional) {
 
935
                        my @ar = split("\n", $additional);
 
936
                        foreach my $s (@ar) {
 
937
                                print "\t$s\n";
 
938
                        }
 
939
                }
 
940
        }
 
941
}
 
942
 
 
943
sub process
 
944
{
 
945
        my ($url) = shift || return undef;
 
946
        my ($config) = shift || return undef;
 
947
        
 
948
        my $raw = retreive_playlist($url);
 
949
        if (! defined $raw) {
 
950
                print STDERR "error: failed to retreive playlist from $url\n";
 
951
                return undef;
 
952
        }
 
953
 
 
954
        my @pls = ();
 
955
 
 
956
        if ($url =~ /\.m3u$/) {
 
957
                @pls = parse_m3u_playlist($raw);
 
958
        } else {
 
959
                @pls = parse_pls_playlist($raw);
 
960
        }
 
961
 
 
962
        debug("play list parsed");
 
963
 
 
964
        $config->{'stop'} = 0;
 
965
        $config->{'played-tracks'} = 0;
 
966
        $config->{'bytes-downloaded'} = 0;
 
967
        $config->{'start-time'} = time();
 
968
 
 
969
        my $entry = ();
 
970
        foreach $entry (@pls) {
 
971
        
 
972
                if ($config->{verbose}) {
 
973
                        print "[ playing $entry->{file} ]\n";
 
974
                }
 
975
 
 
976
                start_stream($entry->{file});
 
977
                last if ($config->{stop} != 0);
 
978
        }
 
979
        
 
980
        return 1;
 
981
}
 
982
 
 
983
sub main { 
 
984
        
 
985
        my (@queue, $url);
 
986
        my ($played) = 0;
 
987
        
 
988
        $config = parse_options();
 
989
        if (! defined $config) {
 
990
                # there was an error parsing parameters
 
991
                help();
 
992
        }
 
993
 
 
994
        help() if ($config->{help} == 1);
 
995
        
 
996
        @queue = split("\n", $config->{urls});
 
997
        help() unless @queue > 0;
 
998
        
 
999
        foreach $url (@queue) {
 
1000
                process($url, $config);
 
1001
                $played++;
 
1002
        }
 
1003
 
 
1004
        print "\n";
 
1005
 
 
1006
        if ($played == 0) {
 
1007
                print STDERR "nothing was played";
 
1008
        }
 
1009
}               
 
1010
 
 
1011
sub find_latest_index
 
1012
{
 
1013
        my ($location) = shift || return 0;
 
1014
        my ($id) = 0;
 
1015
        my ($fn);
 
1016
 
 
1017
        opendir(DIR, $location) || return $id;
 
1018
        while ($fn = readdir(DIR)) {
 
1019
 
 
1020
                if ($fn =~ /^(\d+)\s+.*\.mp3$/) {
 
1021
                        $id = $1 if ($id < $1);
 
1022
                }
 
1023
        }
 
1024
                                
 
1025
        closedir(DIR);
 
1026
        return $id;
 
1027
}
 
1028
 
 
1029
# great C habit
 
1030
binmode STDOUT;
 
1031
$| = 1;
 
1032
 
 
1033
main();
 
1034
 
 
1035
__END__
 
1036
 
 
1037
=head1 NAME
 
1038
 
 
1039
icecream - listen to, or download icecast streams
 
1040
 
 
1041
=head1 SYNOPSIS
 
1042
 
 
1043
icecream [OPTIONS] URL [URL..]
 
1044
 
 
1045
=head1 DESCRIPTION
 
1046
 
 
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.
 
1053
 
 
1054
=head1 OPTIONS
 
1055
 
 
1056
=over 8
 
1057
 
 
1058
=item B<-h>, B<--help>
 
1059
 
 
1060
Print a help message describing all options
 
1061
 
 
1062
=item B<-q>, B<--quiet>
 
1063
 
 
1064
Turn off output
 
1065
 
 
1066
=item B<-v>, B<--verbose>
 
1067
 
 
1068
Be verbose
 
1069
 
 
1070
=item B<-s>, B<--stdout>
 
1071
 
 
1072
Output stream to stdout (implies -q)
 
1073
 
 
1074
=item B<-t>, B<--tracks>
 
1075
 
 
1076
Split stream into tracks (if possible)
 
1077
 
 
1078
=item B<--stop=N[units]>
 
1079
 
 
1080
Stop stream after N min(minutes), songs or KB/MB transferred
 
1081
 
 
1082
=item B<--debug>
 
1083
 
 
1084
Turn on debugging outputs
 
1085
 
 
1086
=item B<--useragent=AGENT>
 
1087
 
 
1088
Set useragent header to AGENT
 
1089
 
 
1090
=back
 
1091
 
 
1092
=head1 EXAMPLES
 
1093
 
 
1094
=over 8
 
1095
 
 
1096
=item Streaming to mpg123
 
1097
 
 
1098
icecream -s http://radio.com/playlist.pls | mpg123 -
 
1099
 
 
1100
=item Split stream into different tracks
 
1101
 
 
1102
icecream -t http://metal.org/radio.pls
 
1103
 
 
1104
=item Prepare a 74 minute CD
 
1105
 
 
1106
icecream -t --stop 74min http://trance.net/playlist.m3u 
 
1107
 
 
1108
=back
 
1109
 
 
1110
=head1 BUGS
 
1111
 
 
1112
You are welcome to send bug reports about icecream to our mailing
 
1113
list. Feel free to visit http://icecream.sourceforge.net
 
1114
 
 
1115
=head1 AUTHOR
 
1116
 
 
1117
Written by Gil Megidish <gmegidis@ort.org.il>
 
1118
 
 
1119
=cut
 
1120