#!/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; # Handle interrupts, and term signals $SIG{'INT'} = 'INT_handler'; $SIG{'TERM'} = 'TERM_handler'; # Globals our %default_options = ( verbose => 0, debug => 0, quiet => 0, log_file => '/tmp/handbrake-cluster-client.log', silent => 0, help => 0, pretend => 0, job_servers => ['build0.sihnon.net', 'build1.sihnon.net', 'build2.sihnon.net'], report_email => '', config_file => '', ); our %options = map { $_ => undef } keys %default_options; our %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}, 'report|r=s' => \$options{report_email}, '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}, ) or pod2usage(-verbose => 0); pod2usage(-verbose => 1) if ($options{help}); my @jobs; my $config; # Read a configuration file, if provided if ($options{config_file}) { $config = parse_config($options{config_file}); push @jobs, @{ $config->{jobs} }; } # A list of jobs to run, formed from command line options or config file if ($rip_options{title}) { push @jobs, \%rip_options; } # Setup logging my $log = Log::Handler->new(); my $maxLogLevel = ($options{debug} ? 'debug' : ($options{quiet} ? 3 : $options{verbose} + 4)); # default to logging warning+, unless quiet in which case log error+, or debug in which case log everything 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 $taskset = $client->new_task_set; foreach my $job (@jobs) { $taskset->add_task("rip", freeze($job), { on_complete => \&on_complete_handler, on_fail => \&on_fail_handler, } ); } $taskset->wait; sub on_complete_handler { my $result_ref = shift or die; if ($options{report_email}) { my $email = MIME::Lite::TT::HTML->new( From => $options{report_email},, To => $options{report_email}, Subject => 'Rip completed', TimeZone => 'Europe/London', Encoding => 'quoted-printable', Template => { html => 'rip-completed.html', text => 'rip-completed.txt', }, Charset => 'utf8', TmplOptions => {}, TmplParams => {}, ); $email->send; } $log->notice("Completed rip to $$result_ref"); } sub on_fail_handler { $log->error("Rip failed"); } sub parse_config { my $config_file = shift or die; my $config = YAML::Any::LoadFile($options{config_file}) or die 'Unable to load configuration file: ' . $!; # 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}; } } } # Iterate through each job, and inject any preset variables that haven't been redefined by the job foreach my $job (@{ $config->{jobs} }) { if ($job->{presets}) { foreach my $preset_name (@{ $job->{presets} }) { print "Copying values for preset $preset_name into job\n"; 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 INT_handler { $log->error("Caught interrupt, exiting."); exit 0; } sub TERM_handler { $log->error("Caught SIGTERM, exiting."); exit 0; } __END__; =head1 NAME handbrake-cluster-client - Sets up DVD ripping tasks to be distributed =head1 SYNOPSIS handbrake-cluster-client.pl -h handbrake-cluster-client.pl [-v [-d]|-q|-s] [-l logfile] [-n] =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 =cut