-
Notifications
You must be signed in to change notification settings - Fork 0
/
par.pl
executable file
·399 lines (305 loc) · 12.7 KB
/
par.pl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
#!/usr/bin/perl
# Script to download and store BBC 'listen again' radio content, transcoding
# that recording into any format you care to mention (well, any format
# supported by the Perl Audio Converter at any rate!).
#
# Combine this script with cron for your very own Personal Audio Recorder
# (hence the name of this script).
use warnings;
use strict;
use POSIX qw(strftime tmpnam);
use File::Path;
use Pod::Usage;
# Check and show an error if required modules can't be found.
eval 'use File::Which'; die 'Missing required component \'dev-perl/File-Which\'' if $@;
eval 'use Getopt::Long'; die 'Missing required component \'perl-core/Getopt-Long\'' if $@;
eval 'use Mail::Sendmail'; die 'Missing required component \'dev-perl/Mail-Sendmail\'' if $@;
eval 'use LWP'; die 'Missing required component \'dev-perl/libwww-perl\'' if $@;
eval 'use LWP::UserAgent'; die 'Missing required component \'dev-perl/libwww-perl\'' if $@;
eval 'use Audio::Wav'; die 'Missing required component \'dev-perl/Audio-Wav\'' if $@;
use constant DEFAULT_ATTEMPTS => 5;
use constant DEFAULT_RETRY => 300;
use constant DEFAULT_FORMAT => 'mp3';
use constant DEFAULT_BITRATE => 128;
use constant PACPL => 'pacpl';
use constant MPLAYER => 'mplayer';
sub download_and_convert($$$$$$$$$$$$);
# We rely on the Perl Audio Converter for the heavy lifting, so ensure it's
# present on the path.
my $pacpl = which(PACPL);
if (not defined($pacpl)) {
printf STDERR "par: The Perl Audio Converter is required to support this script\n";
printf STDERR "par: Download it from http://sourceforge.net/projects/pacpl/\n";
printf STDERR "par: It doesn't require compilation, but use the following command to install\n";
printf STDERR "par: ./pacpl-install --install base\n";
exit 1;
}
# We rely on mplayer to do the stream downloading and dumping, so ensure it's
# present on the path.
my $mplayer = which(MPLAYER);
if (not defined($mplayer)) {
printf STDERR "par: mplayer (and appropriate Real Audio codecs) is required to support\n";
printf STDERR "par: this script.\n";
printf STDERR "par: Gentoo users can get them with the following command:\n";
printf STDERR "par: emerge media-video/mplayer media-video/mplayer\n";
exit 1;
}
# Process command-line arguments
my $stream;
my $basedir;
my $artist;
my $basename;
my $min_length;
my $max_attempts;
my $retry_pause;
my $bitrate;
my $format;
GetOptions("stream=s" => \$stream,
"outputdir=s" => \$basedir,
"artist=s" => \$artist,
"title=s" => \$basename,
"minlength=i" => \$min_length,
"maxtries=i" => \$max_attempts,
"retrypause=i" => \$retry_pause,
"bitrate=i" => \$bitrate,
"format=s" => \$format,
"help|?" => sub { pod2usage(-verbose => 1) })
or die "Failed to understand command options";
# Some options are mandatory.
die "par.pl: Mandatory options missing - try 'perldoc par.pl'" unless ($stream || $basedir || $artist || $basename || $min_length);
print 'par: $Id$' . "\n";
# Some options default.
$max_attempts = $max_attempts || DEFAULT_ATTEMPTS;
$retry_pause = $retry_pause || DEFAULT_RETRY;
$format = $format || DEFAULT_FORMAT;
$bitrate = $bitrate || DEFAULT_BITRATE;
# Display what we're using:
print "par: stream: $stream\n";
print "par: format: $format\n";
print "par: bitrate: $bitrate\n";
print "par: base directory: $basedir\n";
print "par: artist: $artist\n";
print "par: title: $basename\n";
print "par: minlength: $min_length\n";
print "par: maxtries: $max_attempts\n";
print "par: retrypause: $retry_pause\n";
print "par: mplayer: $mplayer\n";
print "par: pacpl: $pacpl\n";
# If this is a RAM file, need to download that to discover the embedded stream.
if ($stream =~ /^http:\/\//) {
print "Downloading '$stream'\n";
my $ua = LWP::UserAgent->new;
$ua->agent("par/0.1 ");
# Create a request
my $req = HTTP::Request->new(GET => $stream);
my $res = $ua->request($req);
if ($res->is_success) {
$stream= $res->content;
}
else {
die "Could not download '$stream'";
}
}
# Crack the current time.
my @time_now = gmtime;
my $date = strftime "%G-%m-%d", @time_now;
my $week = strftime "%U", @time_now;
my $year = strftime "%G", @time_now;
# Make the output directory (if necessary)
( -d "$basedir" ) or mkpath($basedir)
or die "par: can't make output directory $basedir";
# Filenames
my $wav_filename = tmpnam() . '.wav';
my $output_filename = "$date $basename";
my $output_dir = "$basedir";
# File tags
my $album = "$basename";
my $title = "$date $basename";
print "par: recording to $output_dir/$output_filename\n";
# Try a few times to download and convert - sometimes the download is
# truncated early.
my $converted_ok = 0;
my $length = 0;
for (my $attempt = 1; ($attempt <= $max_attempts) && !$converted_ok; $attempt++) {
print "par: download attempt $attempt of $max_attempts\n";
($converted_ok, $length) = download_and_convert($mplayer, $pacpl, $wav_filename, $output_filename, $output_dir, $stream, $bitrate,
$artist, $album, $title, $year, $min_length);
if (!$converted_ok && ($attempt != $max_attempts)) {
print "par: waiting $retry_pause seconds before retrying\n";
sleep $retry_pause;
}
}
# If still not big enough after a few goes, make the problem known.
if (!$converted_ok) {
my %mail = (To => 'stuart@hickinbottom.com',
From => 'par@hickinbottom.com',
Message => "Size was $length mins, minimum length is $min_length mins (tried $max_attempts times)",
Subject => "ERROR: Recording of '$output_filename' cancelled because download was too short"
);
if (!sendmail(%mail)) {
# This is purposefully done twice to avoid the warning from perl
# that the symbol is only used once (an artifact, I suspect, of the
# eval trick used at the head of this script).
my $error = $Mail::Sendmail::error;
$error = $Mail::Sendmail::error;
die $error;
}
}
print "par: all done\n";
exit 0;
sub download_and_convert($$$$$$$$$$$$) {
my ($mplayer, $pacpl, $wav_filename, $output_filename, $output_dir, $stream,
$bitrate, $artist, $album, $title, $year, $min_length) = @_;
my $converted_ok = 1;
# OK, now do the mplayer dump to the intermediate wav.
system("$mplayer -vo null -ao pcm:file=\"$wav_filename\" \"$stream\" 2>/dev/null");
my $length = length_minutes($wav_filename);
if ($length < $min_length) {
print "par: short output file - $length minutes, min length $min_length minutes\n";
$converted_ok = 0;
}
# If that worked, we're going to use pacpl to get to our destination
# format and apply the required tags.
if ($converted_ok) {
my $cmd = "$pacpl --to $format --overwrite --outfile \"$output_filename\" --outdir \"$output_dir\" \"$wav_filename\"";
print "par: $cmd\n";
chdir($output_dir);
system($cmd);
if (! -f "$output_dir/$output_filename.$format") {
die "par: Perl Audio Converter failed to convert WAV file";
}
$cmd = "$pacpl --genre=Speech --artist=\"$artist\" --title=\"$title\" --album=\"$album\" --year=\"$year\" \"$output_dir/$output_filename.$format\"";
print "par: $cmd\n";
system($cmd);
}
unlink $wav_filename;
return ( $converted_ok, $length );
}
sub length_minutes($) {
my $file = shift;
my $wav = new Audio::Wav;
my $read = $wav->read($file);
return int($read->length_seconds() / 60);
}
__END__
# The help text
=head1 NAME
par.pl [options]
=head1 SYNOPSIS
Script to download streams in Real Audio format and transcode them to
a format of the users' choice. The output audio file is also appropriately
tagged.
The main use of this script (and its original purpose) was to implement a
"poor man's personal audio recorded" by coupling this script to cron. In
particular, this is very useful for the BBC's "listen again" content since
that allows you to record each programme as they come out (if you schedule
your cron job correctly).
Each recorded programme is given a filename that includes the date of the
recorded programme, hence if you use this correctly you'll end up with
folder that contains all the recorded episodes of the programme you were
interested in.
=head1 OPTIONS
=over 15
=item --stream
The URL of the Real Audio stream to record. This can end in either F<.ra>
or F<.ram> - both will be correctly handled.
=item --outputdir
The directory used for the output files - this script will place each
recorded programme into this folder with a filename that includes the
date that the recording was made.
=item --format
The required output format. This can be any format that is recognised as
a 'convertto' type by the Perl Audio Converter script (pacpl), but the most
common types to use here would be "ogg" or "mp3".
This parameter is optional; if it is not present then MP3 format is the
default.
=item --artist
The artist name to tag the output file with. For radio stations, this is
often going to be the station name the programme has been recorded from
(eg "BBC Radio 4").
=item --title
The title that will be written into the track tag of the recording (with
the date of the recording as a prefix). For radio stations, this is often
going to be the name of the programme itself (eg "The Archers Omnibus").
=item --minlength
Specifies the minimum length, in minutes, that the programme is expected to
be. This is useful since a remote steam can be prematurely closed and would
otherwise result in a part-recorded programme. If the recording is less
than this number of minutes then the script will try to download the
programme again, up until the number of tries in the next option.
=item --maxtries
This specifies the maximum number of tries that will be used to download the
programme. This value is optional; if not specified then the maximum
number of tries will be 5.
=item --retrypause
If the download has to be retried, this value specifies the amount of time,
in seconds, that the script will pause before retrying the download. This
value is optional; if not specified then the pause will be 300s (five
minutes).
=item --bitrate
Specifies the required bitrate (in kbps) of the resulting file. This value
is optional and if not supplied the default of 128kbps will be used instead.
=item --help
Displays usage information for the script.
=back
=head1 EXAMPLE
A simple example is probably all you'll need to actually use this script
effectively. The following command will download a programme that is published
weekly and transcode it to Ogg format:
par.pl \
--stream "rtsp://rmv8.bbc.net.uk/radio4/archers/archers_omnibus.ra" \
--outputdir "/mnt/media/Radio/archers" \
--artist "BBC Radio 4" \
--title "The Archers Omnibus" \
--minlength 73 \
--format ogg
Over time (eg if this command were scheduled weekly with cron), this will
populate the folder F</mnt/media/Radio/archers> as follows:
2006-08-27 The Archers Omnibus.ogg
2006-09-03 The Archers Omnibus.ogg
2006-09-10 The Archers Omnibus.ogg
An example cron entry that scheduled this command to be run weekly would
be as follows (note that this would be all on one line in the crontab):
0 17 * * sun par.pl
--stream "rtsp://rmv8.bbc.net.uk/radio4/archers/archers_omnibus.ra"
--outputdir "/mnt/media/Radio/archers"
--artist "BBC Radio 4"
--title "The Archers Omnibus"
--minlength 73
--format ogg >/dev/null 2>&1
This will download and transcode the programme at 5pm every Sunday evening.
Remember that you can add and amend crontab entries with the
C<crontab -e> command. Note the redirection at the end of the entry to
prevent cron mailing you the verbose output of the script every time it
runs.
=head1 DEPENDENCIES
As well as the main dependencies of this script (which should be detected and
a meaningful error produced if they are not present), there are a number of
other dependencies that must be installed for this script to work:
=over 5
=item mplayer
This does the main work of downloading and dumping the radio stream. It must be
configured with the appropriate codecs to allow the decoding of Real Audio
streams. The Gentoo ebuilds media-video/mplayer and media-libs/win32codecs can
be used to install this.
=item Perl Audio Converter
This can be obtained from L<http://sourceforge.net/projects/pacpl> - there is
no Gentoo ebuild available. Follow the installation instructions with the
package (note, though, that only a "base" installation is necessary for
this script's purposes).
=item Ogg::Vorbis::Header
This is required by the Perl Audio Converter and must be installed. Gentoo
ebuild dev-perl/ogg-vorbis-header can be used to install this.
=item MP3::Tag
Only necessary if you intend to produce MP3 files with "--format mp3" (or
leave the default format, which is MP3). Gentoo ebuild dev-perl/MP3-Tag can
be used to install this.
=back
Plus, any other dependencies of the Perl Audio Converter for your chosen
output format - determine those by running "pacpl-install -c" in the
installation package (the required Perl modules are listed at the end).
=head1 AUTHOR
Stuart Hickinbottom
=head1 HOMEPAGE
L<http://hickinbottom.demon.co.uk/development/personal-audio-recorder>
=cut