diff --git a/HandBrakeCluster/Job.class.php b/HandBrakeCluster/Job.class.php index e10c99f..8417d17 100644 --- a/HandBrakeCluster/Job.class.php +++ b/HandBrakeCluster/Job.class.php @@ -2,10 +2,12 @@ class HandBrakeCluster_Job { + protected $source; + private $id; private $name; - private $source; - private $destination; + private $source_filename; + private $destination_filename; private $title; private $format; private $video_codec; @@ -20,27 +22,31 @@ class HandBrakeCluster_Job { private $statuses = null; + private static $cache = array(); - public function __construct($id, $name, $source, $destination, $title, $format, $video_codec, $video_width, $video_height, $quantizer, $deinterlace, $audio_tracks, $audio_codecs, $audio_names, $subtitle_tracks) { - $this->id = $id; - $this->name = $name; - $this->source = $source; - $this->destination = $destination; - $this->title = $title; - $this->format = $format; - $this->video_codec = $video_codec; - $this->video_width = $video_width; - $this->video_height = $video_height; - $this->quantizer = $quantizer; - $this->deinterlace = $deinterlace; - $this->audio_tracks = $audio_tracks; - $this->audio_codecs = $audio_codecs; - $this->audio_names = $audio_names; - $this->subtitle_tracks = $subtitle_tracks; + protected function __construct($source, $id, $name, $source_filename, $destination_filename, $title, $format, $video_codec, $video_width, $video_height, $quantizer, $deinterlace, + $audio_tracks, $audio_codecs, $audio_names, $subtitle_tracks) { + $this->source = $source; + $this->id = $id; + $this->name = $name; + $this->source_filename = $source_filename; + $this->destination_filename = $destination_filename; + $this->title = $title; + $this->format = $format; + $this->video_codec = $video_codec; + $this->video_width = $video_width; + $this->video_height = $video_height; + $this->quantizer = $quantizer; + $this->deinterlace = $deinterlace; + $this->audio_tracks = $audio_tracks; + $this->audio_codecs = $audio_codecs; + $this->audio_names = $audio_names; + $this->subtitle_tracks = $subtitle_tracks; } public static function fromDatabaseRow($row) { return new HandBrakeCluster_Job( + HandBrakeCluster_Rips_Source::load($rips['source']), $row['id'], $row['name'], $row['source'], @@ -59,14 +65,28 @@ class HandBrakeCluster_Job { ); } + /** + * + * @todo Implement cache of previously loaded jobs + * + * @param int $id + * @return HandBrakeCluster_Job + */ public static function fromId($id) { $database = HandBrakeCluster_Main::instance()->database(); - return HandBrakeCluster_Job::fromDatabaseRow( + + if (isset(self::$cache[$id])) { + return self::$cache[$id]; + } + + $job = HandBrakeCluster_Job::fromDatabaseRow( $database->selectOne('SELECT * FROM jobs WHERE id=:id', array( array('name' => 'id', 'value' => $id, 'type' => PDO::PARAM_INT) ) ) ); + + self::$cache[$job->id] = $job; } public static function all() { @@ -74,7 +94,10 @@ class HandBrakeCluster_Job { $database = HandBrakeCluster_Main::instance()->database(); foreach ($database->selectList('SELECT * FROM jobs') as $row) { - $jobs[] = self::fromDatabaseRow($row); + $job = self::fromDatabaseRow($row); + + self::$cache[$job->id] = $job; + $jobs[] = $job; } return $jobs; @@ -84,7 +107,7 @@ class HandBrakeCluster_Job { $jobs = array(); $database = HandBrakeCluster_Main::instance()->database(); - foreach ($database->selectList('SELECT * FROM jobs WHERE id IN (SELECT id FROM job_status_current WHERE status=:status)', array( + foreach ($database->selectList('SELECT * FROM jobs WHERE id IN (SELECT job_id FROM job_status_current WHERE status=:status)', array( array('name' => 'status', 'value' => $status, 'type' => PDO::PARAM_INT) )) as $row) { $jobs[] = self::fromDatabaseRow($row); @@ -92,17 +115,124 @@ class HandBrakeCluster_Job { return $jobs; } + + public static function fromPostRequest($source_id, $config) { + $source_filename = base64_decode(str_replace('-', '/', HandBrakeCluster_Main::issetelse($source_id, HandBrakeCluster_Exception_InvalidParameters))); + $source = HandBrakeCluster_Rips_Source::load($source_filename); - protected function loadStatuses() { - if ($this->statuses == null) { - $this->statuses = HandBrakeCluster_JobStatus::allForJob($this->id); + $jobs = array(); + foreach ($config as $title => $details) { + if (HandBrakeCluster_Main::issetelse($details['queue'])) { + $job = new HandBrakeCluster_Job( + $source, + null, + HandBrakeCluster_Main::issetelse($details['name'], 'unnamed job'), + $source->filename(), + HandBrakeCluster_Main::issetelse($details['output_filename'], HandBrakeCluster_Exception_InvalidParameters), + $title, + 'mkv', // @todo Make this configurable + 'x264', // @todo Make this configurable + 0, // @todo Make this configurable + 0, // @todo Make this configurable + 0.61, // @todo Make this configurable + HandBrakeCluster_Main::issetelse($details['deinterlace'], 2), + implode(',', HandBrakeCluster_Main::issetelse($details['audio'], array())), + implode(',', array_pad(array(), count($details['audio']), 'ac3')), // @todo Make this configurable + implode(',', array_pad(array(), count($details['audio']), 'Unknown')), // @todo Make this configurable + implode(',', HandBrakeCluster_Main::issetelse($details['subtitles'], array())) + ); + $job->create(); + + $jobs[] = $job; + } + } + + return $jobs; + } + + protected function create() { + $database = HandBrakeCluster_Main::instance()->database(); + $database->insert( + 'INSERT INTO jobs + (id,name,source,destination,title,format,video_codec,video_width,video_height,quantizer,deinterlace,audio_tracks,audio_codecs,audio_names,subtitle_tracks) + VALUES(NULL,:name,:source,:destination,:title,:format,:video_codec,:video_width,:video_height,:quantizer,:deinterlace,:audio_tracks,:audio_codecs,:audio_names,:subtitle_tracks)', + array( + array(name => 'name', value => $this->name, type => PDO::PARAM_STR), + array(name => 'source', value => $this->source_filename, type => PDO::PARAM_STR), + array(name => 'destination', value => $this->destination_filename, type => PDO::PARAM_STR), + array(name => 'title', value => $this->title, type => PDO::PARAM_INT), + array(name => 'format', value => $this->format, type => PDO::PARAM_STR), + array(name => 'video_codec', value => $this->video_codec, type => PDO::PARAM_STR), + array(name => 'video_width', value => $this->video_width, type => PDO::PARAM_INT), + array(name => 'video_height', value => $this->video_height, type => PDO::PARAM_INT), + array(name => 'quantizer', value => $this->quantizer, type => PDO::PARAM_INT), + array(name => 'deinterlace', value => $this->deinterlace, type => PDO::PARAM_INT), + array(name => 'audio_tracks', value => $this->audio_tracks, type => PDO::PARAM_STR), + array(name => 'audio_codecs', value => $this->audio_codecs, type => PDO::PARAM_STR), + array(name => 'audio_names', value => $this->audio_names, type => PDO::PARAM_STR), + array(name => 'subtitle_tracks', value => $this->subtitle_tracks, type => PDO::PARAM_STR), + ) + ); + + $this->id = $database->lastInsertId(); + $status = HandBrakeCluster_JobStatus::updateStatusForJob($this, HandBrakeCluster_JobStatus::CREATED); + } + + public function queue($gearman) { + $main = HandBrakeCluster_Main::instance(); + $config = $main->config(); + $log = $main->log(); + $log->info('Starting job', $this->id); + + // Construct the rip options + $rip_options = array( + 'nice' => $config->get('rips.nice', 15), + 'input_dir' => dirname($this->source_filename) . DIRECTORY_SEPARATOR, + 'input_filename' => basename($this->source_filename), + 'output_dir' => dirname($this->destination_filename) . DIRECTORY_SEPARATOR, + 'output_filename' => basename($this->destination_filename), + 'title' => $this->title, + 'format' => $this->format, + 'video_codec' => $this->video_codec, + 'video_width' => $this->video_width, + 'video_height' => $this->video_height, + 'quantizer' => $this->quantizer, + 'deinterlace' => $this->deinterlace, + 'audio_tracks' => $this->audio_tracks, + 'audio_codec' => $this->audio_codecs, + 'audio_names' => $this->audio_names, + 'subtitle_tracks' => $this->subtitle_tracks, + ); + + // Enqueue this rip + $task = $gearman->addTask('handbrake_rip', serialize($rip_options), $config->get('rips.context'), $this->id); + if ($task) { + $log->debug("Queued job", $this->id); + $this->updateStatus(HandBrakeCluster_JobStatus::QUEUED); + } else { + $log->warning("Failed to queue job", $this->id); + $this->updateStatus(HandBrakeCluster_JobStatus::FAILED); } } + protected function loadStatuses() { + if ($this->statuses == null) { + $this->statuses = HandBrakeCluster_JobStatus::allForJob($this); + } + } + + /** + * + * @return HandBrakeCluster_JobStatus + */ public function currentStatus() { $this->loadStatuses(); return $this->statuses[count($this->statuses) - 1]; } + + public function updateStatus($new_status, $rip_progress = null) { + return HandBrakeCluster_JobStatus::updateStatusForJob($this, $new_status, $rip_progress); + } public function id() { return $this->id; @@ -112,18 +242,22 @@ class HandBrakeCluster_Job { return $this->name; } - public function source() { - return $this->source; + public function sourceFilename() { + return $this->source_filename; } - public function destination() { - return $this->destination; + public function destinationFilename() { + return $this->destination_filename; } public function title() { return $this->title; } + public static function runAllJobs() { + HandBrakeCluster_BackgroundTask::run('/usr/bin/php run-jobs.php'); + } + }; ?> diff --git a/HandBrakeCluster/JobStatus.class.php b/HandBrakeCluster/JobStatus.class.php index 29b9adc..2478e4c 100644 --- a/HandBrakeCluster/JobStatus.class.php +++ b/HandBrakeCluster/JobStatus.class.php @@ -2,28 +2,32 @@ class HandBrakeCluster_JobStatus { - const QUEUED = 0; - const FAILED = 1; - const RUNNING = 2; - const COMPLETE = 3; + const CREATED = 0; + const QUEUED = 1; + const FAILED = 2; + const RUNNING = 3; + const COMPLETE = 4; private static $status_names = array( - self::QUEUED => 'Queued', - self::FAILED => 'Failed', - self::RUNNING => 'Running', - self::COMPLETE => 'Complete' + self::CREATED => 'Created', + self::QUEUED => 'Queued', + self::FAILED => 'Failed', + self::RUNNING => 'Running', + self::COMPLETE => 'Complete', ); protected $id; protected $job_id; protected $status; protected $ctime; + protected $rip_progress; - protected function __construct($id, $job_id, $status, $ctime) { + protected function __construct($id, $job_id, $status, $ctime, $rip_progress) { $this->id = $id; $this->job_id = $job_id; $this->status = $status; $this->ctime = $ctime; + $this->rip_progress = $rip_progress; } public static function fromDatabaseRow($row) { @@ -31,22 +35,68 @@ class HandBrakeCluster_JobStatus { $row['id'], $row['job_id'], $row['status'], - $row['ctime'] + $row['ctime'], + $row['rip_progress'] ); } + + public static function updateStatusForJob($job, $status, $rip_progress = null) { + $status = new HandBrakeCluster_JobStatus(null, $job->id(), $status, time(), $rip_progress); + $status->create(); + + return $status; + } + + public function updateRipProgress($rip_progress) { + $this->rip_progress = $rip_progress; + $this->save(); + } - public static function allForJob($job_id) { + public static function allForJob(HandBrakeCluster_Job $job) { $statuses = array(); $database = HandBrakeCluster_Main::instance()->database(); foreach ($database->selectList('SELECT * FROM job_status WHERE job_id=:job_id ORDER BY ctime ASC', array( - array('name' => 'job_id', 'value' => $job_id, 'type' => PDO::PARAM_INT), + array('name' => 'job_id', 'value' => $job->id(), 'type' => PDO::PARAM_INT), )) as $row) { $statuses[] = HandBrakeCluster_JobStatus::fromDatabaseRow($row); } return $statuses; } + + protected function create() { + $database = HandBrakeCluster_Main::instance()->database(); + $database->insert( + 'INSERT INTO job_status + (id, job_id, status, ctime, rip_progress) + VALUES(NULL,:job_id,:status,:ctime,:rip_progress)', + array( + array(name => 'job_id', value => $this->job_id, type => PDO::PARAM_INT), + array(name => 'status', value => $this->status, type => PDO::PARAM_INT), + array(name => 'ctime', value => $this->ctime, type => PDO::PARAM_INT), + array(name => 'rip_progress', value => $this->rip_progress), + ) + ); + + $this->id = $database->lastInsertId(); + } + + public function save() { + $database = HandBrakeCluster_Main::instance()->database(); + $database->update( + 'UPDATE job_status SET + job_id=:job_id, status=:status, ctime=:ctime, rip_progress=:rip_progress + WHERE id=:id', + array( + array(name => 'id', value => $this->id, type => PDO::PARAM_INT), + array(name => 'job_id', value => $this->job_id, type => PDO::PARAM_INT), + array(name => 'status', value => $this->status, type => PDO::PARAM_INT), + array(name => 'ctime', value => $this->ctime, type => PDO::PARAM_INT), + array(name => 'rip_progress', value => $this->rip_progress), + ) + ); + } public function id() { return $this->id; @@ -67,6 +117,10 @@ class HandBrakeCluster_JobStatus { public function ctime() { return $this->ctime; } + + public function ripProgress() { + return $this->rip_progress; + } }; diff --git a/HandBrakeCluster/Rips/Source.class.php b/HandBrakeCluster/Rips/Source.class.php index efe7ea4..024ae59 100644 --- a/HandBrakeCluster/Rips/Source.class.php +++ b/HandBrakeCluster/Rips/Source.class.php @@ -11,29 +11,50 @@ class HandBrakeCluster_Rips_Source { protected $output; protected $titles = array(); - public function __construct($source_filename, $use_cache) { + protected function __construct($source_filename, $scan_dir, $use_cache) { $this->source = $source_filename; - - $this->scan(); + + if ($scan_dir) { + $this->scan(); + } $main = HandBrakeCluster_Main::instance(); $cache = $main->cache(); $config = $main->config(); - if ($use_cache) { + if ($scan_dir && $use_cache) { $cache->store($this->source, serialize($this), $config->get('rips.cache_ttl')); } } - public static function load($source_filename, $use_cache = true) { + public static function load($source_filename, $scan_dir = true, $use_cache = true) { $cache = HandBrakeCluster_Main::instance()->cache(); if ($use_cache && $cache->exists($source_filename)) { return unserialize($cache->fetch($source_filename)); } else { - return new HandBrakeCluster_Rips_Source($source_filename, $use_cache); + return new HandBrakeCluster_Rips_Source($source_filename, $scan_dir, $use_cache); } } + + public static function loadEncoded($encoded_filename, $scan_dir = true, $use_cache = true) { + // Decode the filename + $source_filename = base64_decode(str_replace('-', '/', $encoded_filename)); + + // Ensure the source is a valid directory, and lies below the configured source_dir + $real_source_filename = realpath($source_filename); + if (!is_dir($source_filename)) { + throw new HandBrakeCluster_Exception_InvalidSourceDirectory($source_filename); + } + + $config = HandBrakeCluster_Main::instance()->config(); + $real_source_basedir = realpath($config->get('rips.source_dir')); + if (substr($real_source_filename, 0, strlen($real_source_basedir)) != $real_source_basedir) { + throw new HandBrakeCluster_Exception_InvalidSourceDirectory($source_filename); + } + + return self::load($source_filename, $scan_dir, $use_cache); + } protected function scan() { $source_shell = escapeshellarg($this->source); @@ -150,6 +171,10 @@ class HandBrakeCluster_Rips_Source { return $cache->exists($source_filename, $config->get('rips.cache_ttl')); } + public static function encodeFilename($filename) { + return str_replace("/", "-", base64_encode($filename)); + } + public function addTitle(HandBrakeCluster_Rips_SourceTitle $title) { $this->titles[] = $title; } @@ -173,7 +198,13 @@ class HandBrakeCluster_Rips_Source { return $longest_title; } + public function filename() { + return $this->source; + } + public function filenameEncoded() { + return self::encodeFilename($this->source); + } public function output() { return $this->output; diff --git a/HandBrakeCluster/Rips/SourceLister.class.php b/HandBrakeCluster/Rips/SourceLister.class.php index cc9cb86..a4b48a9 100644 --- a/HandBrakeCluster/Rips/SourceLister.class.php +++ b/HandBrakeCluster/Rips/SourceLister.class.php @@ -37,7 +37,7 @@ class HandBrakeCluster_Rips_SourceLister { // otherwise add the dir to the queue to scan deeper $source_vts = $source . DIRECTORY_SEPARATOR . 'VIDEO_TS'; if (is_dir($source_vts)) { - $this->sources[] = $source_vts; + $this->sources[] = HandBrakeCluster_Rips_Source::load($source_vts, false); } else { $scan_directories[] = $source; }