Files
handbrake-cluster/handbrake-cluster-client.pl

480 lines
15 KiB
Perl
Executable File

#!/usr/bin/perl
package HandbrakeCluster::Client;
use warnings;
use strict;
use Data::Dumper;
use Gearman::Client;
use Getopt::Long;
use Log::Log4perl;
use MIME::Lite::TT::HTML;
use Pod::Usage;
use Storable qw/freeze thaw/;
use YAML::Any;
# Globals
my $default_options = {
log_config => 'logging.conf',
help => 0,
pretend => 0,
config_file => '',
job_servers => ['build0.sihnon.net', 'build1.sihnon.net', 'build2.sihnon.net'],
gearman_prefix => '',
limit => [],
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 $default_rip_options = {
nice => 15,
input_dir => '',
input_filename => '/dev/sr0',
output_dir => '',
output_filename => 'rip-output.mkv',
title => 0,
format => 'mkv',
video_codec => 'x264',
video_width => 0,
video_height => 0,
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,
};
my $rip_options = { map { $_ => undef } keys %$default_rip_options };
Getopt::Long::Configure( qw(bundling no_getopt_compat) );
GetOptions(
'log-config|l=s' => \$options->{log_config},
'help|h' => \$options->{help},
'pretend|n' => \$options->{pretend},
'job-servers|j=s@' => \$options->{job_servers},
'gearman-prefix=s' => \$options->{gearman_prefix},
'config|c=s' => \$options->{config_file},
'limit|L=s' => sub { my ($name, $value) = @_; $options->{limit} = [split(/,/, $value)]; },
'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}, $default_rip_options);
($config, $options) = process_config($config, $options, $default_options);
# Create a list of jobs from the configuration file, and command line options
my %jobs = %{ $config->{jobs} } if defined $config->{jobs};
$jobs{__commandline} = $rip_options if $options->{title};
die "No rips specified" unless %jobs;
# Setup logging
Log::Log4perl->init($options->{log_config});
my $client_log = Log::Log4perl->get_logger('HandbrakeCluster::Client');
$client_log->debug("Logging started");
# Setup the distribution client
my $client = Gearman::Client->new;
$client->prefix($options->{gearman_prefix}) if $options->{gearman_prefix};
$client->job_servers($options->{job_servers});
# Add new ripping task for each job to run
my %progress;
my $taskset = $client->new_task_set;
foreach my $job_id (keys %jobs) {
my $job = $jobs{$job_id};
# Check that the job hasn't been restricted by the limit option
if (@{ $options->{limit} } && !$options->{job_permitted}->{$job_id}) {
$client_log->info("Skipped job $job_id due to limit restrictions");
next;
}
# Create a new database record for this job, and grab the new id
$job->{db_job_id} = 43;
$progress{$job_id} = 0;
$taskset->add_task('handbrake_rip', freeze($job),
{
on_status => sub {
my $numerator = shift;
my $denominator = shift or die;
$progress{$job_id} = $numerator / $denominator;
display_progress(%progress);
},
on_complete => \&on_complete_handler,
on_retry => sub {
my $attempt = shift or die;
$client_log->warning("Retrying '$job_id' rip");
},
on_fail => sub {
$client_log->warning("Rip '$job_id' failed");
},
}
);
$client_log->info("Enqueued '$job_id' rip");
}
$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;
}
$client_log->info("Completed rip to $response->{real_output_filename}");
}
sub display_progress {
my %progress = @_;
local $|;
$| = 1;
print "Completion: " . join(' ', map { "$_($progress{$_}%)" } keys %progress) . "\r";
}
# Reads configuration options from a config file, expands the internal references, and returns the expanded form.
sub parse_config {
my $config_file = shift;
my $default_rip_options = shift or die;
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_id (keys %{ $config->{jobs} }) {
my $job = $config->{jobs}->{$job_id};
$job = $config->{jobs}->{$job_id} = {} unless $job;
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};
}
}
}
# Inject any default variables that haven't been defined by the job or preset
foreach my $rip_option (keys %$default_rip_options) {
$job->{$rip_option} = $default_rip_options->{$rip_option} unless $job->{$rip_option};
}
}
}
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};
}
}
}
# Flag jobs that may be run if any limits are specified
my %job_permitted = map { $_ => 0 } keys %{ $config->{jobs} };
foreach my $job_id (@{ $options->{limit} }) {
$job_permitted{$job_id} = 1;
}
$options->{job_permitted} = \%job_permitted;
# 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