Using anonymous subs for the simple callbacks instead of named subs Using reference rather than direct hash access to construct the response
458 lines
14 KiB
Perl
Executable File
458 lines
14 KiB
Perl
Executable File
#!/usr/bin/perl
|
|
|
|
use warnings;
|
|
use strict;
|
|
|
|
use Data::Dumper;
|
|
use Gearman::Client;
|
|
use Getopt::Long;
|
|
use Log::Handler;
|
|
use MIME::Lite::TT::HTML;
|
|
use Pod::Usage;
|
|
use Storable qw/freeze thaw/;
|
|
use YAML::Any;
|
|
|
|
# Globals
|
|
my $default_options = {
|
|
verbose => 0,
|
|
debug => 0,
|
|
quiet => 0,
|
|
log_file => '/tmp/handbrake-cluster-client.log',
|
|
silent => 0,
|
|
help => 0,
|
|
pretend => 0,
|
|
config_file => '',
|
|
job_servers => ['build0.sihnon.net', 'build1.sihnon.net', 'build2.sihnon.net'],
|
|
email_target => '',
|
|
email_sender => '',
|
|
email_subject => 'Rip completed',
|
|
email_timezone => 'Europe/London',
|
|
email_html => 'rip-completed.html',
|
|
email_text => 'rip-completed.txt',
|
|
};
|
|
my $options = { map { $_ => undef } keys %$default_options };
|
|
|
|
my $rip_options = {
|
|
nice => 15,
|
|
input_filename => '/dev/sr0',
|
|
output_filename => 'rip-output.mkv',
|
|
title => 0,
|
|
format => 'mkv',
|
|
video_codec => 'x264',
|
|
video_width => 720, # DVD resolution
|
|
video_height => 576, # DVD resolution
|
|
quantizer => 0.61,# x264 quantizer = 20
|
|
deinterlace => 0, # 0 = off, 1 = on, 2 = selective
|
|
audio_tracks => 1,
|
|
audio_codec => 'ac3',
|
|
audio_names => 'English',
|
|
subtitle_tracks => 1,
|
|
};
|
|
|
|
Getopt::Long::Configure( qw(bundling no_getopt_compat) );
|
|
GetOptions(
|
|
'verbose|v+' => \$options->{verbose},
|
|
'debug|d' => \$options->{debug},
|
|
'quiet|q' => \$options->{quiet},
|
|
'silent|s' => \$options->{silent},
|
|
'log|l=s' => \$options->{log_file},
|
|
'help|h' => \$options->{help},
|
|
'pretend|n' => \$options->{pretend},
|
|
'job-servers|j=s@' => \$options->{job_servers},
|
|
'config|c=s' => \$options->{config_file},
|
|
'nice|N=i' => \$rip_options->{nice},
|
|
'input|i=s' => \$rip_options->{input_filename},
|
|
'output|o=s' => \$rip_options->{output_filename},
|
|
'title|t=s' => \$rip_options->{title},
|
|
'format|f=s' => \$rip_options->{format},
|
|
'video-encoder|e=s' => \$rip_options->{video_codec},
|
|
'width|w=i' => \$rip_options->{video_width},
|
|
'height|l=i' => \$rip_options->{video_height},
|
|
'quantizer|Q=f' => \$rip_options->{quantizer},
|
|
'deinterlate|D=i' => \$rip_options->{deinterlace},
|
|
'audio-tracks|a=s' => \$rip_options->{audio_tracks},
|
|
'audio-encoder|E=s' => \$rip_options->{audio_codec},
|
|
'audio-name|A=s' => \$rip_options->{audio_names},
|
|
'subtitle-tracks|S=s' => \$rip_options->{subtitle_tracks},
|
|
'email-target=s' => \$options->{email_target},
|
|
'email-sender=s' => \$options->{email_sender},
|
|
'email-subject=s' => \$options->{email_subject},
|
|
) or pod2usage(-verbose => 0);
|
|
pod2usage(-verbose => 1) if ($options->{help});
|
|
|
|
# Parse the configuration file (if any), and merge/validate the options
|
|
my $config = parse_config($options->{config_file});
|
|
($config, $options) = process_config($config, $options, $default_options);
|
|
|
|
# Create a list of jobs from the configuration file, and command line options
|
|
my @jobs;
|
|
push @jobs, @{ $config->{jobs} } if defined $config->{jobs};
|
|
push @jobs, $rip_options if $options->{title};
|
|
die "No rips specified" unless @jobs;
|
|
|
|
# Setup logging
|
|
my $log = Log::Handler->new();
|
|
# default to logging warning+, unless quiet in which case log error+, or debug in which case log everything
|
|
my $maxLogLevel = ($options->{debug} ? 'debug' : ($options->{quiet} ? 3 : $options->{verbose} + 4));
|
|
# Ignore the quiet option when logging to disk
|
|
my $maxFileLogLevel = ($options->{debug} ? 'debug' : 'info');
|
|
|
|
if ( ! $options->{silent}) {
|
|
$log->add(
|
|
screen => {
|
|
log_to => 'STDOUT',
|
|
minlevel => 'emergency',
|
|
maxlevel => $maxLogLevel,
|
|
},
|
|
);
|
|
}
|
|
if ( $options->{log_file}) {
|
|
$log->add(
|
|
file => {
|
|
filename => $options->{log_file},
|
|
minlevel => 'emergency',
|
|
maxlevel => $maxFileLogLevel,
|
|
},
|
|
);
|
|
}
|
|
|
|
# Setup the distribution client
|
|
my $client = Gearman::Client->new;
|
|
$client->job_servers($options->{job_servers});
|
|
|
|
# Add new ripping task for each job to run
|
|
my @running_tasks;
|
|
my $taskset = $client->new_task_set;
|
|
foreach my $job (@jobs) {
|
|
$taskset->add_task('handbrake_rip', freeze($job),
|
|
{
|
|
on_status => sub {
|
|
my $numerator = shift;
|
|
my $denominator = shift or die;
|
|
|
|
$log->notice("Ripping task at ", ($numerator/$denominator), "% complete");
|
|
},
|
|
on_complete => \&on_complete_handler,
|
|
on_retry => sub {
|
|
my $attempt = shift or die;
|
|
$log->warning("Retrying rip");
|
|
},
|
|
on_fail => sub {
|
|
$log->warning("Rip failed");
|
|
},
|
|
}
|
|
);
|
|
}
|
|
$taskset->wait;
|
|
|
|
sub on_complete_handler {
|
|
my $result_ref = shift or die;
|
|
my $response = thaw($$result_ref);
|
|
|
|
if ($options->{email_target}) {
|
|
my $email = MIME::Lite::TT::HTML->new(
|
|
From => $options->{email_sender},
|
|
To => $options->{email_target},
|
|
Subject => $options->{email_subject},
|
|
TimeZone => $options->{email_timezone},
|
|
Encoding => 'quoted-printable',
|
|
Template => {
|
|
html => $options->{email_html},
|
|
text => $options->{email_text},
|
|
},
|
|
Charset => 'utf8',
|
|
TmplOptions => {
|
|
INCLUDE_PATH => '.',
|
|
},
|
|
TmplParams => {
|
|
success => $response->{success},
|
|
filename => $response->{output_filename},
|
|
real_filename => $response->{real_output_filename},
|
|
log => join '\n', @{ $response->{log} },
|
|
},
|
|
);
|
|
$email->send;
|
|
}
|
|
|
|
$log->notice("Completed rip to $response->{real_output_filename}");
|
|
}
|
|
|
|
# Reads configuration options from a config file, expands the internal references, and returns the expanded form.
|
|
sub parse_config {
|
|
my $config_file = shift;
|
|
|
|
return {} unless defined $config_file;
|
|
my $config = YAML::Any::LoadFile($options->{config_file}) or die 'Unable to load configuration file: ' . $!;
|
|
|
|
# Iterate through each job, and inject any preset variables that haven't been redefined by the job
|
|
if (defined $config->{jobs}) {
|
|
foreach my $job (@{ $config->{jobs} }) {
|
|
if ($job->{presets}) {
|
|
foreach my $preset_name (@{ $job->{presets} }) {
|
|
foreach my $preset_key (keys %{$config->{presets}->{$preset_name}}) {
|
|
$job->{$preset_key} = $config->{presets}->{$preset_name}->{$preset_key} unless $job->{$preset_key};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return $config;
|
|
}
|
|
|
|
sub process_config {
|
|
my $config = shift or die;
|
|
my $options = shift or die;
|
|
my $default_options = shift or die;
|
|
|
|
# Update any unset options with values from config file
|
|
foreach my $option (keys %$options) {
|
|
# Update the value of any option which has not been set by a command line argument
|
|
if (!$options->{$option}) {
|
|
# Try the config file first, otherwise use the default value
|
|
if (defined $config->{options}->{$option}) {
|
|
$options->{$option} = $config->{options}->{$option};
|
|
} else {
|
|
$options->{$option} = $default_options->{$option};
|
|
}
|
|
}
|
|
}
|
|
|
|
# Validate the email options
|
|
if ($options->{email_target}) {
|
|
$options->{email_sender} = $options->{email_target} unless $options->{email_sender};
|
|
}
|
|
|
|
return ($config, $options);
|
|
}
|
|
|
|
|
|
__END__;
|
|
=head1 NAME
|
|
|
|
handbrake-cluster-client - Sets up DVD ripping tasks to be distributed
|
|
|
|
=head1 SYNOPSIS
|
|
|
|
handbrake-cluster-client.pl -h|--help
|
|
handbrake-cluster-client.pl [[-v|--verbose] | [-d|--debug] |
|
|
[-q|--quiet]] | [-s|--silent]]
|
|
[-l|--log <log file>]
|
|
[-n|--pretend]
|
|
[-c|--config <config file>]
|
|
[-N|--nice <nice level>]
|
|
[-i|--input <input filename>]
|
|
[-o|--output <output filename>]
|
|
[-f|--format <mkv|mp4>]
|
|
[-t|--title <title>]
|
|
[-e|--video-encoder <ffmpeg|theora|x264>]
|
|
[-w|--width <width>]
|
|
[-l|--height <height>]
|
|
[-Q|--quantizer <quality>]
|
|
[-D|--deinterlace <0|1|2>]
|
|
[-a|--audio-tracks <track list>]
|
|
[-E|--audio-encoder
|
|
<faac/lame/vorbis/ac3/dts>[,...]]
|
|
[-A|--audio-name <audio name list>]
|
|
[-s|--subtitle-tracks <subtitle track list>]
|
|
[--email-target <email address>]
|
|
[--email-sender <email address>]
|
|
[--email-subject <subject line>]
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
Sets up one or more DVD ripping tasks, and hands them off to gearman for
|
|
distribution. Logging and ripping configuration can be controlled with
|
|
command line arguments.
|
|
|
|
=head1 OPTIONS
|
|
|
|
=head2 Administrative Options
|
|
|
|
=over 4
|
|
|
|
=item B<-h|--help>
|
|
|
|
Displays this help information and quits.
|
|
|
|
=item B<-v|--verbose>
|
|
|
|
Displays verbose logging information.
|
|
|
|
=item B<-d|--debug>
|
|
|
|
Displays full debugging information, including all output from HandBrake.
|
|
|
|
=item B<-q|--quiet>
|
|
|
|
Displays only errors.
|
|
|
|
=item B<-s|--silent>
|
|
|
|
Displays no output whatsoever, useful for scripting. Output will still be
|
|
logged to disk if a file is specified.
|
|
|
|
=item B<-l|--log E<lt>log fileE<gt>>
|
|
|
|
Specifies the name of a file to write logging information to.
|
|
|
|
=item B<-n|--pretend>
|
|
|
|
Only shows what action would be taken, no rips are actually performed.
|
|
|
|
=item B<-j|--job-servers E<lt>server listE<gt>>
|
|
|
|
Specifies a comma separated list of alternate servers to use for job
|
|
distribution.
|
|
|
|
=item B<-c|--config E<lt>config fileE<gt>>
|
|
|
|
Specifies the name of a configuration file which contains additional options,
|
|
and a list of ripping jobs.
|
|
|
|
=item B<-n|--nice E<lt>nice levelE<gt>>
|
|
|
|
Specifies the priority to give the ripping process, using nice.
|
|
Defaults to 15.
|
|
|
|
=back
|
|
|
|
=head2 Ripping Options
|
|
|
|
=over 4
|
|
|
|
=item B<-i|--input E<lt>input filenameE<gt>>
|
|
|
|
Specifies the DVD drive device, or path to a VIDEO_TS folder to use as input
|
|
for the rip.
|
|
|
|
=item B<-o|--output E<lt>output filenameE<gt>>
|
|
|
|
Specifies the name of the file the rip will be saved to. This is only used
|
|
as a template; the actual rip will be saved to a filename with a unique
|
|
component at the end to avoid accidental overwrites.
|
|
|
|
=item B<-f|--format E<lt>mkv|mp4E<gt>>
|
|
|
|
Selects the container used for the rip. Defaults to mkv.
|
|
|
|
=item B<-t|--title E<lt>titleE<gt>>
|
|
|
|
Specifies the DVD title to rip. By default, the longest title will be used.
|
|
|
|
=item B<-e|--video-encoder E<lt>ffmpeg|theora|x264E<gt>>
|
|
|
|
Specifies the video encoder to use for the rip. Defaults to x264.
|
|
|
|
=item B<-w|--width E<lt>widthE<gt>>
|
|
|
|
Specifies the width of the output rip in pixels.
|
|
|
|
=item B<-l|--height E<lt>heightE<gt>>
|
|
|
|
Specifies the height of the output rip in pixels.
|
|
|
|
=item B<-Q|--quantizer E<lt>qualityE<gt>>
|
|
|
|
Specifies the x264 quantizer property, in the form of a real between 0 and 1.
|
|
Defaults to 0.61, aka "20".
|
|
|
|
=item B<D|--deinterlace E<lt>0|1|2E<gt>>
|
|
|
|
Specifies the type of deinterlacing to use. Use 0 to disable completely.
|
|
Use 1 for full deinterlacing, or 2 for selective deinterlacing (recommended).
|
|
Defaults to 0.
|
|
|
|
=item B<-a|--audio-tracks E<lt>audio track listE<gt>>
|
|
|
|
Specifies a comma separated list of audio tracks to include in the rip.
|
|
Defaults to 1.
|
|
|
|
=item B<-E|--audio-encoder E<lt>faac/lame/vorbis/ac3/dts>E<gt>>
|
|
|
|
Specifies the audio encoder to use for the rip. Defaults to ac3 pass-through.
|
|
|
|
=item B<-A|--audio-name E<lt>audio track namesE<gt>>
|
|
|
|
Specifies a comma separated list of names for the selected audio tracks.
|
|
Defaults to 'English'.
|
|
|
|
=back
|
|
|
|
=head2 Reporting Options
|
|
|
|
=over 4
|
|
|
|
=item B<--email-target E<lt>email addressE<gt>>
|
|
|
|
Specifies an email address to send a completion report to after each rip
|
|
finishes.
|
|
|
|
=item B<--email-sender E<lt>email addressE<gt>>
|
|
|
|
Specifies an email address to use as the sender of the rip completion email
|
|
reports. Defaults to the same as the target address.
|
|
|
|
=item B<--email-subject E<lt>subject lineE<gt>>
|
|
|
|
Overrides the subject line to use in the rip completion email reports.
|
|
|
|
=back
|
|
|
|
=head1 CONFIGURATION
|
|
|
|
This script accepts a configuration file in YAML format to specify multiple
|
|
ripping tasks, or provide values for commonly used options.
|
|
|
|
Command line arguments take precedence to options specified in config files.
|
|
|
|
This file is split into three sections:
|
|
|
|
=over 4
|
|
|
|
=item B<options>
|
|
|
|
Values for command-line options from the I<Administrative Options> section
|
|
above can be specified here, using variables with the same name as the long
|
|
options (with hyphens replaced with underscores).
|
|
|
|
options:
|
|
job_servers: build0.example.com
|
|
log_file: /tmp/handbrake.log
|
|
|
|
=item B<presets>
|
|
|
|
Presets allow the grouping of common job configuration options, for example
|
|
the input filename when ripping multiple episodes from a single DVD image.
|
|
Any option from the I<Ripping Options> section can be specified in a preset,
|
|
and options specified in a job take precedence in case of a clash.
|
|
|
|
preset:
|
|
disk1:
|
|
input_filename: /tmp/disk1/VIDEO_TS
|
|
tvseries:
|
|
format: mkv
|
|
video_codec: x264
|
|
|
|
=item B<jobs>
|
|
|
|
Specifies a list of ripping tasks to perform. Variables from the
|
|
I<Ripping Options> section above can be used here.
|
|
|
|
Additionally, you can include all of the options from one or more presets.
|
|
|
|
jobs:
|
|
- presets:
|
|
- disk1
|
|
- tvseries
|
|
output_filename: episode1.mkv
|
|
|
|
=back
|
|
|
|
=head2 EXAMPLE CONFIGURATION
|
|
|
|
An example configuration file is included in the distribution as C<config.yml>.
|
|
Refer to this when writing your own files.
|
|
|
|
=cut
|