#!/usr/bin/perl use warnings; use strict; use Data::Dumper; use Gearman::Worker; use Getopt::Long; use IPC::Open3; use Log::Handler; use Pod::Usage; use String::Random qw/random_string/; use Storable qw/freeze thaw/; use Switch; # Globals our %options = ( verbose => 0, quiet => 0, log_file => '/tmp/handbrake-cluster-worker.log', silent => 0, help => 0, pretend => 0, job_servers => ['build0.sihnon.net', 'build1.sihnon.net', 'build2.sihnon.net'], handbrake => '/usr/bin/HandBrakeCLI', ); Getopt::Long::Configure( qw(bundling no_getopt_compat) ); GetOptions( 'help|h' => \$options{help}, 'verbose|v' => \$options{verbose}, 'debug|d' => \$options{debug}, 'quiet|q' => \$options{quiet}, 'silent|s' => \$options{silent}, 'log|l=s' => \$options{log_file}, 'pretend|n' => \$options{pretend}, 'job-servers|j=s@' => \$options{job_servers}, 'handbrake' => \$options{handbrake}, ) or pod2usage(-verbose => 0); pod2usage(-verbose => 1) if ($options{help}); # 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 worker, and listen for jobs my $worker = Gearman::Worker->new(job_servers => $options{job_servers}); $worker->register_function(handbrake_rip => \&handbrake_rip); $worker->work while 1; sub handbrake_rip { my $job = shift; my $rip_options = thaw($job->arg); my $response = {}; $log->notice("Beginning new rip to $rip_options->{output_filename}"); # Generate a unique filename based on the output filename to prevent clashes from previous runs my $uuid = random_string('cccccc'); $rip_options->{real_output_filename} = $rip_options->{output_filename}; $rip_options->{real_output_filename} =~ s/\.([^\.]+)$/\.$uuid\.$1/; $response->{real_output_filename} = $rip_options->{real_output_filename}; # Generate the command line for handbrake my @handbrake_cmd = ( # Reduce the priority of the ripping process, since it is very processor intensive 'nice', '-n', $rip_options->{nice}, # Construct the handbrake command line $options{handbrake}, get_options($rip_options, 'input_filename', '-i'), get_options($rip_options, 'real_output_filename', '-o'), get_options($rip_options, 'title'), get_options($rip_options, 'format', '-f'), get_options($rip_options, 'video_codec', '-e'), get_options($rip_options, 'quantizer', '-q'), get_options($rip_options, 'video_width', '-w'), get_options($rip_options, 'video_height', '-l'), get_options($rip_options, 'deinterlace'), get_options($rip_options, 'audio_tracks', '-a'), get_options($rip_options, 'audio_codec', '-E'), get_options($rip_options, 'audio_names', '-A'), get_options($rip_options, 'subtitle_tracks', '-s'), ); # Return a copy of the command used to rip the title $response->{handbrake_cmd} = @handbrake_cmd; # flag the start of the job $job->set_status(0, 100); # Execute the ripping process $log->debug("Beginning ripping process with command:\n" . join(' ', @handbrake_cmd)); my ($child_in, $child_out, $child_err); my $child_pid = open3($child_in, $child_out, $child_err, @handbrake_cmd); # No need to write to the child process close($child_in); # Log the output from handbrake, and return it back to the client $response->{log} = (); my $line; while ($line = <$child_out>) { # If the line is a progress report, record the current status # otherwise, log the line # Encoding: task 1 of 1, 0.87 % (34.71 fps, avg 62.95 fps, ETA 00h07m56s) if ($line =~ m/Encoding: task \d+ of \d+, (\d+\.\d+) %/) { my $numerator = $1 * 100; $job->set_status($numerator, 100); $log->notice("Task is $numerator% complete"); } else { push @{ $response->{log} }, $line; $log->notice($line); } } close($child_out); $job->set_status(100, 100); # If the rip process failed, report an error status here waitpid($child_pid, 0); $response->{status} = $? >> 8; $response->{success} = $response->{status} == 0; if ($response->{success}) { $log->notice("Finished rip to $response->{real_output_filename}"); } else { $log->warning("Ripping process returned error status: $response->{status}"); } return freeze($response); } sub get_options { my $rip_options = shift or die; my $option_name = shift or die; my $option = shift; switch ($option_name) { case 'title' { return ('-L') if ! defined($rip_options->{$option_name}) || $rip_options->{$option_name} < 0; return ('-t', $rip_options->{$option_name}); } case 'deinterlace' { switch ($rip_options->{$option_name}) { case 1 { return ('-d') } case 2 { return ('-5') } return (); } } return (defined $rip_options->{$option_name} ? ($option, $rip_options->{$option_name}) : ()); } } __END__; =head1 NAME handbrake-cluster-worker - Processes DVD rips farmed out by gearman =head1 SYNOPSIS handbrake-cluster-worker.pl -h|--help handbrake-cluster-worker.pl [[-v|--verbose] | [-d|--debug] | [-q|--quiet] | [-s|--silent]] [-l|--log ] [-n|--pretend] [-j|--job-servers ] [--handbrake ] =head1 DESCRIPTION Processes ripping tasks as requested by a gearman job server. Logging and the job servers to use are configurable by command line arguments. =head1 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 Elog fileE> 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 Eserver listE> Specifies a comma separated list of alternate servers to use for job distribution. =item B<--handbrake Ehandbrake executableE> Specifies an alternate HandBrake executable to use. Default is C. =cut