Renamed the codebase to RippingCluster
Since the new design is engine agnostic, removed HandBrake from the class names.
This commit is contained in:
17
lib/RippingCluster/BackgroundTask.class.php
Normal file
17
lib/RippingCluster/BackgroundTask.class.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
class RippingCluster_BackgroundTask {
|
||||
|
||||
protected function __construct() {
|
||||
|
||||
}
|
||||
|
||||
public function run($command) {
|
||||
$pipes = array();
|
||||
$pid = proc_open($command . ' &', array(), $pipes);
|
||||
proc_close($pid);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
?>
|
||||
60
lib/RippingCluster/Cache.class.php
Normal file
60
lib/RippingCluster/Cache.class.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
class RippingCluster_Cache {
|
||||
|
||||
protected $config;
|
||||
protected $cache_dir;
|
||||
|
||||
public function __construct(RippingCluster_Config $config) {
|
||||
$this->config = $config;
|
||||
$this->cache_dir = $config->get('cache.base_dir');
|
||||
|
||||
if (is_dir($this->cache_dir)) {
|
||||
if ( ! is_writeable($this->cache_dir)) {
|
||||
throw new RippingCluster_Exception_InvalidCacheDir();
|
||||
}
|
||||
} else {
|
||||
if ( ! RippingCluster_Main::mkdir_recursive($this->cache_dir)) {
|
||||
throw new RippingCluster_Exception_InvalidCacheDir();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function cacheFilename($source_filename) {
|
||||
return $this->cache_dir . sha1($source_filename);
|
||||
}
|
||||
|
||||
public function exists($source_filename, $ttl = 3600) {
|
||||
$cache_filename = $this->cacheFilename($source_filename);
|
||||
|
||||
// Check to see if the file is cached
|
||||
if (!file_exists($cache_filename)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check to see if the cache has expired
|
||||
if (filemtime($cache_filename) + $ttl < time()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function store($source_filename, $content) {
|
||||
$cache_filename = $this->cacheFilename($source_filename);
|
||||
return file_put_contents($cache_filename, $content);
|
||||
}
|
||||
|
||||
public function fetch($source_filename, $ttl = 3600) {
|
||||
$cache_filename = $this->cacheFilename($source_filename);
|
||||
|
||||
if (!$this->exists($source_filename)) {
|
||||
throw new RippingCluster_Exception_CacheObjectNotFound($source_filename);
|
||||
}
|
||||
|
||||
return file_get_contents($cache_filename);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
?>
|
||||
13
lib/RippingCluster/ClientLogEntry.class.php
Normal file
13
lib/RippingCluster/ClientLogEntry.class.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
class RippingCluster_ClientLogEntry extends RippingCluster_LogEntry {
|
||||
|
||||
public static function initialise() {
|
||||
parent::$table_name = 'client_log';
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
RippingCluster_ClientLogEntry::initialise();
|
||||
|
||||
?>
|
||||
56
lib/RippingCluster/Config.class.php
Normal file
56
lib/RippingCluster/Config.class.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
class RippingCluster_Config {
|
||||
|
||||
private $dbconfig;
|
||||
private $database;
|
||||
|
||||
private $databaseConfig = array();
|
||||
private $settings = array();
|
||||
|
||||
public function __construct($dbconfig) {
|
||||
$this->dbconfig = $dbconfig;
|
||||
|
||||
$this->parseDatabaseConfig();
|
||||
}
|
||||
|
||||
public function parseDatabaseConfig() {
|
||||
$this->databaseConfig = parse_ini_file($this->dbconfig);
|
||||
}
|
||||
|
||||
public function getDatabase($key) {
|
||||
if (!isset($this->databaseConfig[$key])) {
|
||||
throw new RippingCluster_Exception_DatabaseConfigMissing($key);
|
||||
}
|
||||
|
||||
return $this->databaseConfig[$key];
|
||||
}
|
||||
|
||||
public function setDatabase(RippingCluster_Database $database) {
|
||||
$this->database = $database;
|
||||
$this->preload();
|
||||
}
|
||||
|
||||
public function preload() {
|
||||
if (!$this->database) {
|
||||
throw new RippingCluster_Exception_NoDatabaseConnection();
|
||||
}
|
||||
|
||||
$this->settings = $this->database->selectAssoc('SELECT name,value FROM settings', 'name', 'value');
|
||||
}
|
||||
|
||||
public function exists($key) {
|
||||
return isset($this->settings[$key]);
|
||||
}
|
||||
|
||||
public function get($key) {
|
||||
if (!isset($this->settings[$key])) {
|
||||
throw new RippingCluster_Exception_UnknownSetting($key);
|
||||
}
|
||||
|
||||
return $this->settings[$key];
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
?>
|
||||
132
lib/RippingCluster/Database.class.php
Normal file
132
lib/RippingCluster/Database.class.php
Normal file
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
class RippingCluster_Database {
|
||||
|
||||
private $config;
|
||||
private $dbh;
|
||||
|
||||
private $hostname;
|
||||
private $username;
|
||||
private $password;
|
||||
private $dbname;
|
||||
|
||||
private $prepared_statements = array();
|
||||
|
||||
public function __construct(RippingCluster_Config $config) {
|
||||
$this->config = $config;
|
||||
|
||||
$this->hostname = $this->config->getDatabase('hostname');
|
||||
$this->username = $this->config->getDatabase('username');
|
||||
$this->password = $this->config->getDatabase('password');
|
||||
$this->dbname = $this->config->getDatabase('dbname');
|
||||
|
||||
try {
|
||||
$this->dbh = new PDO("mysql:host={$this->hostname};dbname={$this->dbname}", $this->username, $this->password);
|
||||
} catch (PDOException $e) {
|
||||
throw new RippingCluster_Exception_DatabaseConnectionFailed($e->getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function __destruct() {
|
||||
$this->dbh = null;
|
||||
}
|
||||
|
||||
public function selectAssoc($sql, $key_col, $value_col) {
|
||||
$results = array();
|
||||
|
||||
foreach ($this->dbh->query($sql) as $row) {
|
||||
$results[$row[$key_col]] = $row[$value_col];
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
public function selectList($sql, $bind_params = null) {
|
||||
if ($bind_params) {
|
||||
$stmt = $this->dbh->prepare($sql);
|
||||
|
||||
foreach ($bind_params as $param) {
|
||||
$stmt->bindValue(':'.$param['name'], $param['value'], $param['type']);
|
||||
}
|
||||
|
||||
$result = $stmt->execute();
|
||||
if (!$result) {
|
||||
list($code, $dummy, $message) = $stmt->errorInfo();
|
||||
throw new RippingCluster_Exception_DatabaseQueryFailed($message, $code);
|
||||
}
|
||||
|
||||
return $stmt->fetchAll();
|
||||
|
||||
} else {
|
||||
$results = array();
|
||||
|
||||
$result = $this->dbh->query($sql);
|
||||
foreach ($result as $row) {
|
||||
$results[] = $row;
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
}
|
||||
|
||||
public function selectOne($sql, $bind_params = null) {
|
||||
$rows = $this->selectList($sql, $bind_params);
|
||||
if (count($rows) != 1) {
|
||||
throw new RippingCluster_Exception_ResultCountMismatch(count($rows));
|
||||
}
|
||||
|
||||
return $rows[0];
|
||||
}
|
||||
|
||||
public function insert($sql, $bind_params = null) {
|
||||
$stmt = $this->dbh->prepare($sql);
|
||||
|
||||
if ($bind_params) {
|
||||
foreach ($bind_params as $param) {
|
||||
if (isset($param['type'])) {
|
||||
$stmt->bindValue(':'.$param['name'], $param['value'], $param['type']);
|
||||
} else {
|
||||
$stmt->bindValue(':'.$param['name'], $param['value']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$result = $stmt->execute();
|
||||
if (!$result) {
|
||||
list($code, $dummy, $message) = $stmt->errorInfo();
|
||||
throw new RippingCluster_Exception_DatabaseQueryFailed($message, $code);
|
||||
}
|
||||
}
|
||||
|
||||
public function update($sql, $bind_params = null) {
|
||||
$stmt = $this->dbh->prepare($sql);
|
||||
|
||||
if ($bind_params) {
|
||||
foreach ($bind_params as $param) {
|
||||
if (isset($param['type'])) {
|
||||
$stmt->bindValue(':'.$param['name'], $param['value'], $param['type']);
|
||||
} else {
|
||||
$stmt->bindValue(':'.$param['name'], $param['value']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$result = $stmt->execute();
|
||||
if (!$result) {
|
||||
list($code, $dummy, $message) = $stmt->errorInfo();
|
||||
throw new RippingCluster_Exception_DatabaseQueryFailed($message, $code);
|
||||
}
|
||||
}
|
||||
|
||||
public function errorInfo() {
|
||||
return $this->dbh->errorInfo();
|
||||
}
|
||||
|
||||
public function lastInsertId() {
|
||||
return $this->dbh->lastInsertId();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
?>
|
||||
30
lib/RippingCluster/Exceptions.class.php
Normal file
30
lib/RippingCluster/Exceptions.class.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
class RippingCluster_Exception extends Exception {};
|
||||
|
||||
class RippingCluster_Exception_DatabaseException extends RippingCluster_Exception {};
|
||||
class RippingCluster_Exception_DatabaseConfigMissing extends RippingCluster_Exception_DatabaseException {};
|
||||
class RippingCluster_Exception_DatabaseConnectFailed extends RippingCluster_Exception_DatabaseException {};
|
||||
class RippingCluster_Exception_NoDatabaseConnection extends RippingCluster_Exception_DatabaseException {};
|
||||
class RippingCluster_Exception_DatabaseQueryFailed extends RippingCluster_Exception_DatabaseException {};
|
||||
class RippingCluster_Exception_ResultCountMismatch extends RippingCluster_Exception_DatabaseException {};
|
||||
|
||||
class RippingCluster_Exception_ConfigException extends RippingCluster_Exception {};
|
||||
class RippingCluster_Exception_UnknownSetting extends RippingCluster_Exception_ConfigException {};
|
||||
|
||||
class RippingCluster_Exception_TemplateException extends RippingCluster_Exception {};
|
||||
class RippingCluster_Exception_AbortEntirePage extends RippingCluster_Exception_TemplateException {};
|
||||
class RippingCluster_Exception_Unauthorized extends RippingCluster_Exception_TemplateException {};
|
||||
class RippingCluster_Exception_FileNotFound extends RippingCluster_Exception_TemplateException {};
|
||||
class RippingCluster_Exception_InvalidParameters extends RippingCluster_Exception_TemplateException {};
|
||||
|
||||
class RippingCluster_Exception_InvalidSourceDirectory extends RippingCluster_Exception {};
|
||||
|
||||
class RippingCluster_Exception_CacheException extends RippingCluster_Exception {};
|
||||
class RippingCluster_Exception_InvalidCacheDir extends RippingCluster_Exception_CacheException {};
|
||||
class RippingCluster_Exception_CacheObjectNotFound extends RippingCluster_Exception_CacheException {};
|
||||
|
||||
class RippingCluster_Exception_LogicException extends RippingCluster_Exception {};
|
||||
class RippingCluster_Exception_JobNotRunning extends RippingCluster_Exception_LogicException {};
|
||||
|
||||
?>
|
||||
111
lib/RippingCluster/ForegroundTask.class.php
Normal file
111
lib/RippingCluster/ForegroundTask.class.php
Normal file
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
class RippingCluster_ForegroundTask {
|
||||
|
||||
const PIPE_STDIN = 0;
|
||||
const PIPE_STDOUT = 1;
|
||||
const PIPE_STDERR = 2;
|
||||
|
||||
private function __construct() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Code largely taken from user submitted comment on http://php.sihnon.net/manual/en/function.proc-open.php
|
||||
* @param unknown_type $command
|
||||
* @param unknown_type $cwd
|
||||
* @param unknown_type $env
|
||||
* @param unknown_type $stdin
|
||||
* @param unknown_type $callback_stdout
|
||||
* @param unknown_type $callback_stderr
|
||||
*/
|
||||
public static function execute($command, $cwd = null, $env = null, $stdin = null, $callback_stdout = null, $callback_stderr = null, $identifier = null) {
|
||||
$txOff = 0;
|
||||
$txLen = strlen($stdin);
|
||||
$stdout = '';
|
||||
$stdoutDone = FALSE;
|
||||
$stderr = '';
|
||||
$stderrDone = FALSE;
|
||||
|
||||
$descriptors = array(
|
||||
self::PIPE_STDIN => array('pipe', 'r'),
|
||||
self::PIPE_STDOUT => array('pipe', 'w'),
|
||||
self::PIPE_STDERR => array('pipe', 'w'),
|
||||
);
|
||||
|
||||
$pipes = array();
|
||||
$process = proc_open($command, $descriptors, $pipes);
|
||||
|
||||
stream_set_blocking($pipes[self::PIPE_STDIN], 0); // Make stdin/stdout/stderr non-blocking
|
||||
stream_set_blocking($pipes[self::PIPE_STDOUT], 0);
|
||||
stream_set_blocking($pipes[self::PIPE_STDERR], 0);
|
||||
|
||||
if ($txLen == 0) {
|
||||
fclose($pipes[0]);
|
||||
}
|
||||
|
||||
while (true) {
|
||||
$rx = array(); // The program's stdout/stderr
|
||||
if (!$stdoutDone) {
|
||||
$rx[] = $pipes[self::PIPE_STDOUT];
|
||||
}
|
||||
if (!$stderrDone) {
|
||||
$rx[] = $pipes[self::PIPE_STDERR];
|
||||
}
|
||||
|
||||
$tx = array(); // The program's stdin
|
||||
if ($txOff < $txLen) {
|
||||
$tx[] = $pipes[self::PIPE_STDIN];
|
||||
}
|
||||
|
||||
stream_select($rx, $tx, $ex = null, null, null); // Block til r/w possible
|
||||
if (!empty($tx)) {
|
||||
$txRet = fwrite($pipes[self::PIPE_STDIN], substr($stdin, $txOff, 8192));
|
||||
if ($txRet !== false) {
|
||||
$txOff += $txRet;
|
||||
}
|
||||
if ($txOff >= $txLen) {
|
||||
fclose($pipes[self::PIPE_STDIN]);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($rx as $r) {
|
||||
if ($r == $pipes[self::PIPE_STDOUT]) {
|
||||
$chunk = fread($pipes[self::PIPE_STDOUT], 8192);
|
||||
if (feof($pipes[self::PIPE_STDOUT])) {
|
||||
fclose($pipes[self::PIPE_STDOUT]); $stdoutDone = true;
|
||||
}
|
||||
|
||||
if ($callback_stdout) {
|
||||
call_user_func($callback_stdout, $identifier, $chunk);
|
||||
} else {
|
||||
$stdout .= $chunk;
|
||||
}
|
||||
|
||||
} else if ($r == $pipes[self::PIPE_STDERR]) {
|
||||
$chunk = fread($pipes[self::PIPE_STDERR], 8192);
|
||||
if (feof($pipes[self::PIPE_STDERR])) {
|
||||
fclose($pipes[self::PIPE_STDERR]); $stderrDone = true;
|
||||
}
|
||||
|
||||
if ($callback_stderr) {
|
||||
call_user_func($callback_stderr, $identifier, $chunk);
|
||||
} else {
|
||||
$stderr .= $chunk;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_resource($process))
|
||||
break;
|
||||
|
||||
if ($txOff >= $txLen && $stdoutDone && $stderrDone)
|
||||
break;
|
||||
}
|
||||
|
||||
return array(proc_close($process), $stdout, $stderr);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
306
lib/RippingCluster/Job.class.php
Normal file
306
lib/RippingCluster/Job.class.php
Normal file
@@ -0,0 +1,306 @@
|
||||
<?php
|
||||
|
||||
class RippingCluster_Job {
|
||||
|
||||
protected $source;
|
||||
|
||||
private $id;
|
||||
private $name;
|
||||
private $source_filename;
|
||||
private $destination_filename;
|
||||
private $title;
|
||||
private $format;
|
||||
private $video_codec;
|
||||
private $video_width;
|
||||
private $video_height;
|
||||
private $quantizer;
|
||||
private $deinterlace;
|
||||
private $audio_tracks;
|
||||
private $audio_codecs;
|
||||
private $audio_names;
|
||||
private $subtitle_tracks;
|
||||
|
||||
private $statuses = null;
|
||||
|
||||
private static $cache = array();
|
||||
|
||||
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 function __clone() {
|
||||
$this->id = null;
|
||||
|
||||
$this->create();
|
||||
}
|
||||
|
||||
public static function fromDatabaseRow($row) {
|
||||
return new RippingCluster_Job(
|
||||
RippingCluster_Rips_Source::load($row['source'], false),
|
||||
$row['id'],
|
||||
$row['name'],
|
||||
$row['source'],
|
||||
$row['destination'],
|
||||
$row['title'],
|
||||
$row['format'],
|
||||
$row['video_codec'],
|
||||
$row['video_width'],
|
||||
$row['video_height'],
|
||||
$row['quantizer'],
|
||||
$row['deinterlace'],
|
||||
$row['audio_tracks'],
|
||||
$row['audio_codecs'],
|
||||
$row['audio_names'],
|
||||
$row['subtitle_tracks']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @todo Implement cache of previously loaded jobs
|
||||
*
|
||||
* @param int $id
|
||||
* @return RippingCluster_Job
|
||||
*/
|
||||
public static function fromId($id) {
|
||||
$database = RippingCluster_Main::instance()->database();
|
||||
|
||||
if (isset(self::$cache[$id])) {
|
||||
return self::$cache[$id];
|
||||
}
|
||||
|
||||
$job = RippingCluster_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;
|
||||
|
||||
return $job;
|
||||
}
|
||||
|
||||
public static function all() {
|
||||
$jobs = array();
|
||||
|
||||
$database = RippingCluster_Main::instance()->database();
|
||||
foreach ($database->selectList('SELECT * FROM jobs WHERE id > 0') as $row) {
|
||||
$job = self::fromDatabaseRow($row);
|
||||
|
||||
self::$cache[$job->id] = $job;
|
||||
$jobs[] = $job;
|
||||
}
|
||||
|
||||
return $jobs;
|
||||
}
|
||||
|
||||
public static function allWithStatus($status, $limit = null) {
|
||||
$jobs = array();
|
||||
$database = RippingCluster_Main::instance()->database();
|
||||
|
||||
$params = array(
|
||||
array('name' => 'status', 'value' => $status, 'type' => PDO::PARAM_INT),
|
||||
);
|
||||
|
||||
$limitSql = '';
|
||||
if ($limit) {
|
||||
$limitSql = 'LIMIT :limit';
|
||||
$params[] = array('name' => 'limit', 'value' => $limit, 'type' => PDO::PARAM_INT);
|
||||
}
|
||||
|
||||
foreach ($database->selectList("SELECT * FROM jobs WHERE id IN (SELECT job_id FROM job_status_current WHERE id > 0 AND status=:status) ORDER BY id {$limitSql}", $params) as $row) {
|
||||
$jobs[] = self::fromDatabaseRow($row);
|
||||
}
|
||||
|
||||
return $jobs;
|
||||
}
|
||||
|
||||
public static function fromPostRequest($source_id, $global_options, $titles) {
|
||||
$source = RippingCluster_Rips_Source::loadEncoded(RippingCluster_Main::issetelse($source_id, RippingCluster_Exception_InvalidParameters));
|
||||
|
||||
$jobs = array();
|
||||
foreach ($titles as $title => $details) {
|
||||
if (RippingCluster_Main::issetelse($details['queue'])) {
|
||||
RippingCluster_Main::issetelse($details['output_filename'], RippingCluster_Exception_InvalidParameters);
|
||||
|
||||
$job = new RippingCluster_Job(
|
||||
$source,
|
||||
null,
|
||||
RippingCluster_Main::issetelse($details['name'], 'unnamed job'),
|
||||
$source->filename(),
|
||||
$global_options['output-directory'] . DIRECTORY_SEPARATOR . $details['output_filename'],
|
||||
$title,
|
||||
$global_options['format'],
|
||||
$global_options['video-codec'],
|
||||
$global_options['video-width'],
|
||||
$global_options['video-height'],
|
||||
$global_options['quantizer'],
|
||||
RippingCluster_Main::issetelse($details['deinterlace'], 2),
|
||||
implode(',', RippingCluster_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(',', RippingCluster_Main::issetelse($details['subtitles'], array()))
|
||||
);
|
||||
$job->create();
|
||||
|
||||
$jobs[] = $job;
|
||||
}
|
||||
}
|
||||
|
||||
return $jobs;
|
||||
}
|
||||
|
||||
protected function create() {
|
||||
$database = RippingCluster_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 = RippingCluster_JobStatus::updateStatusForJob($this, RippingCluster_JobStatus::CREATED);
|
||||
}
|
||||
|
||||
public function delete() {
|
||||
$database = RippingCluster_Main::instance()->database();
|
||||
$database->update(
|
||||
'DELETE FROM jobs WHERE id=:job_id LIMIT 1',
|
||||
array(
|
||||
array(name => 'job_id', value => $this->id, type => PDO::PARAM_INT),
|
||||
)
|
||||
);
|
||||
|
||||
$this->id = null;
|
||||
}
|
||||
|
||||
public function queue($gearman) {
|
||||
$main = RippingCluster_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_filename' => $this->source_filename,
|
||||
'output_filename' => $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(RippingCluster_JobStatus::QUEUED);
|
||||
} else {
|
||||
$log->warning("Failed to queue job", $this->id);
|
||||
$this->updateStatus(RippingCluster_JobStatus::FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
protected function loadStatuses() {
|
||||
if ($this->statuses == null) {
|
||||
$this->statuses = RippingCluster_JobStatus::allForJob($this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return RippingCluster_JobStatus
|
||||
*/
|
||||
public function currentStatus() {
|
||||
$this->loadStatuses();
|
||||
return $this->statuses[count($this->statuses) - 1];
|
||||
}
|
||||
|
||||
public function updateStatus($new_status, $rip_progress = null) {
|
||||
return RippingCluster_JobStatus::updateStatusForJob($this, $new_status, $rip_progress);
|
||||
}
|
||||
|
||||
public function calculateETA() {
|
||||
$current_status = $this->currentStatus();
|
||||
if ($current_status->status() != RippingCluster_JobStatus::RUNNING) {
|
||||
throw new RippingCluster_Exception_JobNotRunning();
|
||||
}
|
||||
|
||||
$running_time = $current_status->mtime() - $current_status->ctime();
|
||||
$progress = $current_status->ripProgress();
|
||||
|
||||
if ($progress > 0) {
|
||||
$remaining_time = round((100 - $progress) * ($running_time / $progress));
|
||||
}
|
||||
|
||||
return $remaining_time;
|
||||
}
|
||||
|
||||
public function id() {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function name() {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function sourceFilename() {
|
||||
return $this->source_filename;
|
||||
}
|
||||
|
||||
public function destinationFilename() {
|
||||
return $this->destination_filename;
|
||||
}
|
||||
|
||||
public function title() {
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public static function runAllJobs() {
|
||||
RippingCluster_BackgroundTask::run('/usr/bin/php run-jobs.php');
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
?>
|
||||
142
lib/RippingCluster/JobStatus.class.php
Normal file
142
lib/RippingCluster/JobStatus.class.php
Normal file
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
class RippingCluster_JobStatus {
|
||||
|
||||
const CREATED = 0;
|
||||
const QUEUED = 1;
|
||||
const FAILED = 2;
|
||||
const RUNNING = 3;
|
||||
const COMPLETE = 4;
|
||||
|
||||
private static $status_names = array(
|
||||
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 $mtime;
|
||||
protected $rip_progress;
|
||||
|
||||
protected function __construct($id, $job_id, $status, $ctime, $mtime, $rip_progress) {
|
||||
$this->id = $id;
|
||||
$this->job_id = $job_id;
|
||||
$this->status = $status;
|
||||
$this->ctime = $ctime;
|
||||
$this->mtime = $mtime;
|
||||
$this->rip_progress = $rip_progress;
|
||||
}
|
||||
|
||||
public static function fromDatabaseRow($row) {
|
||||
return new RippingCluster_JobStatus(
|
||||
$row['id'],
|
||||
$row['job_id'],
|
||||
$row['status'],
|
||||
$row['ctime'],
|
||||
$row['mtime'],
|
||||
$row['rip_progress']
|
||||
);
|
||||
}
|
||||
|
||||
public static function updateStatusForJob($job, $status, $rip_progress = null) {
|
||||
$ctime = $mtime = time();
|
||||
$status = new RippingCluster_JobStatus(null, $job->id(), $status, $ctime, $mtime, $rip_progress);
|
||||
$status->create();
|
||||
|
||||
return $status;
|
||||
}
|
||||
|
||||
public function updateRipProgress($rip_progress) {
|
||||
$this->rip_progress = $rip_progress;
|
||||
$this->mtime = time();
|
||||
$this->save();
|
||||
}
|
||||
|
||||
public static function allForJob(RippingCluster_Job $job) {
|
||||
$statuses = array();
|
||||
|
||||
$database = RippingCluster_Main::instance()->database();
|
||||
foreach ($database->selectList('SELECT * FROM job_status WHERE job_id=:job_id ORDER BY mtime ASC', array(
|
||||
array('name' => 'job_id', 'value' => $job->id(), 'type' => PDO::PARAM_INT),
|
||||
)) as $row) {
|
||||
$statuses[] = RippingCluster_JobStatus::fromDatabaseRow($row);
|
||||
}
|
||||
|
||||
return $statuses;
|
||||
}
|
||||
|
||||
protected function create() {
|
||||
$database = RippingCluster_Main::instance()->database();
|
||||
$database->insert(
|
||||
'INSERT INTO job_status
|
||||
(id, job_id, status, ctime, mtime, rip_progress)
|
||||
VALUES(NULL,:job_id,:status,:ctime,:mtime,: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' => 'mtime', 'value' => $this->mtime, 'type' => PDO::PARAM_INT),
|
||||
array('name' => 'rip_progress', 'value' => $this->rip_progress),
|
||||
)
|
||||
);
|
||||
|
||||
$this->id = $database->lastInsertId();
|
||||
}
|
||||
|
||||
public function save() {
|
||||
$database = RippingCluster_Main::instance()->database();
|
||||
$database->update(
|
||||
'UPDATE job_status SET
|
||||
job_id=:job_id, status=:status, ctime=:ctime, mtime=:mtime, 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' => 'mtime', 'value' => $this->mtime, 'type' => PDO::PARAM_INT),
|
||||
array('name' => 'rip_progress', 'value' => $this->rip_progress),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function hasProgressInfo() {
|
||||
return ($this->status == self::RUNNING);
|
||||
}
|
||||
|
||||
public function id() {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function jobId() {
|
||||
return $this->job_id;
|
||||
}
|
||||
|
||||
public function status() {
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
public function statusName() {
|
||||
return self::$status_names[$this->status];
|
||||
}
|
||||
|
||||
public function ctime() {
|
||||
return $this->ctime;
|
||||
}
|
||||
|
||||
public function mtime() {
|
||||
return $this->mtime;
|
||||
}
|
||||
|
||||
public function ripProgress() {
|
||||
return $this->rip_progress;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
?>
|
||||
61
lib/RippingCluster/Log.class.php
Normal file
61
lib/RippingCluster/Log.class.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
class RippingCluster_Log {
|
||||
|
||||
private static $hostname = '';
|
||||
|
||||
private $database;
|
||||
private $config;
|
||||
private $table;
|
||||
|
||||
public function __construct(RippingCluster_Database $database, RippingCluster_Config $config, $table) {
|
||||
$this->database = $database;
|
||||
$this->config = $config;
|
||||
$this->table = $table;
|
||||
|
||||
}
|
||||
|
||||
public function log($severity, $message, $job_id = 0) {
|
||||
$this->database->insert("INSERT INTO {$this->table} (job_id,level,ctime,pid,hostname,progname,line,message) VALUES(:job_id, :level, :ctime, :pid, :hostname, :progname, :line, :message)",
|
||||
array(
|
||||
array('name' => 'job_id', 'value' => $job_id, 'type' => PDO::PARAM_INT),
|
||||
array('name' => 'level', 'value' => $severity, 'type' => PDO::PARAM_STR),
|
||||
array('name' => 'ctime', 'value' => time(), 'type' => PDO::PARAM_INT),
|
||||
array('name' => 'pid', 'value' => 0, 'type' => PDO::PARAM_INT),
|
||||
array('name' => 'hostname', 'value' => self::$hostname, 'type' => PDO::PARAM_STR),
|
||||
array('name' => 'progname', 'value' => 'webui', 'type' => PDO::PARAM_STR),
|
||||
array('name' => 'line', 'value' => 0, 'type' => PDO::PARAM_INT),
|
||||
array('name' => 'message', 'value' => $message, 'type' => PDO::PARAM_STR)
|
||||
)
|
||||
);
|
||||
|
||||
if (HBC_File == 'worker') {
|
||||
echo date("r") . ' ' . $message . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
public function debug($message, $job_id = 0) {
|
||||
return $this->log('DEBUG', $message, $job_id);
|
||||
}
|
||||
|
||||
public function info($message, $job_id = 0) {
|
||||
return $this->log('INFO', $message, $job_id);
|
||||
}
|
||||
|
||||
public function warning($message, $job_id = 0) {
|
||||
return $this->log('WARNING', $message, $job_id);
|
||||
}
|
||||
|
||||
public function error($message, $job_id = 0) {
|
||||
return $this->log('ERROR', $message, $job_id);
|
||||
}
|
||||
|
||||
public static function initialise() {
|
||||
self::$hostname = trim(`hostname`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
RippingCluster_Log::initialise();
|
||||
|
||||
?>
|
||||
122
lib/RippingCluster/LogEntry.class.php
Normal file
122
lib/RippingCluster/LogEntry.class.php
Normal file
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
abstract class RippingCluster_LogEntry {
|
||||
|
||||
protected static $table_name = "";
|
||||
|
||||
protected $id;
|
||||
protected $job_id;
|
||||
protected $level;
|
||||
protected $ctime;
|
||||
protected $pid;
|
||||
protected $hostname;
|
||||
protected $progname;
|
||||
protected $line;
|
||||
protected $message;
|
||||
|
||||
protected function __construct($id, $job_id, $level, $ctime, $pid, $hostname, $progname, $line, $message) {
|
||||
$this->id = $id;
|
||||
$this->job_id = $job_id;
|
||||
$this->level = $level;
|
||||
$this->ctime = $ctime;
|
||||
$this->pid = $pid;
|
||||
$this->hostname = $hostname;
|
||||
$this->progname = $progname;
|
||||
$this->line = $line;
|
||||
$this->message = $message;
|
||||
}
|
||||
|
||||
public static function fromDatabaseRow($row) {
|
||||
return new RippingCluster_ClientLogEntry(
|
||||
$row['id'],
|
||||
$row['job_id'],
|
||||
$row['level'],
|
||||
$row['ctime'],
|
||||
$row['pid'],
|
||||
$row['hostname'],
|
||||
$row['progname'],
|
||||
$row['line'],
|
||||
$row['message']
|
||||
);
|
||||
}
|
||||
|
||||
public static function fromId($id) {
|
||||
$database = RippingCluster_Main::instance()->database();
|
||||
return RippingCluster_ClientLogEntry::fromDatabaseRow(
|
||||
$database->selectOne('SELECT * FROM '.self::$table_name.' WHERE id=:id', array(
|
||||
array('name' => 'id', 'value' => $id, 'type' => PDO::PARAM_INT)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public static function recent($limit = 100) {
|
||||
$entries = array();
|
||||
|
||||
$database = RippingCluster_Main::instance()->database();
|
||||
foreach ($database->selectList('SELECT * FROM '.self::$table_name.' ORDER BY ctime DESC LIMIT :limit', array(
|
||||
array('name' => 'limit', 'value' => $limit, 'type' => PDO::PARAM_INT)
|
||||
)) as $row) {
|
||||
$entries[] = self::fromDatabaseRow($row);
|
||||
}
|
||||
|
||||
return $entries;
|
||||
}
|
||||
|
||||
public static function recentForJob($job_id, $limit = 100) {
|
||||
$entries = array();
|
||||
|
||||
$database = RippingCluster_Main::instance()->database();
|
||||
foreach ($database->selectList('SELECT * FROM '.self::$table_name.' WHERE job_id=:job_id ORDER BY ctime DESC LIMIT :limit', array(
|
||||
array('name' => 'job_id', 'value' => $job_id, 'type' => PDO::PARAM_INT),
|
||||
array('name' => 'limit', 'value' => $limit, 'type' => PDO::PARAM_INT)
|
||||
)) as $row) {
|
||||
$entries[] = self::fromDatabaseRow($row);
|
||||
}
|
||||
|
||||
return $entries;
|
||||
}
|
||||
|
||||
public static function allForNoJob() {
|
||||
return self::allForJob(0);
|
||||
}
|
||||
|
||||
public function id() {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function jobId() {
|
||||
return $this->job_id;
|
||||
}
|
||||
|
||||
public function level() {
|
||||
return $this->level;
|
||||
}
|
||||
|
||||
public function ctime() {
|
||||
return $this->ctime;
|
||||
}
|
||||
|
||||
public function pid() {
|
||||
return $this->pid;
|
||||
}
|
||||
|
||||
public function hostname() {
|
||||
return $this->hostname;
|
||||
}
|
||||
|
||||
public function progname() {
|
||||
return $this->progname;
|
||||
}
|
||||
|
||||
public function line() {
|
||||
return $this->line;
|
||||
}
|
||||
|
||||
public function message() {
|
||||
return $this->message;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
?>
|
||||
215
lib/RippingCluster/Main.class.php
Normal file
215
lib/RippingCluster/Main.class.php
Normal file
@@ -0,0 +1,215 @@
|
||||
<?php
|
||||
|
||||
require 'smarty/Smarty.class.php';
|
||||
|
||||
class RippingCluster_Main {
|
||||
|
||||
private static $instance;
|
||||
|
||||
private $smarty;
|
||||
private $config;
|
||||
private $database;
|
||||
private $log;
|
||||
private $request;
|
||||
private $cache;
|
||||
|
||||
private $base_uri;
|
||||
|
||||
private function __construct() {
|
||||
$request_string = isset($_GET['l']) ? $_GET['l'] : '';
|
||||
|
||||
$log_table = null;
|
||||
switch(HBC_File) {
|
||||
case 'index':
|
||||
case 'run-jobs': {
|
||||
$log_table = 'client_log';
|
||||
} break;
|
||||
|
||||
case 'worker': {
|
||||
$log_table = 'worker_log';
|
||||
}
|
||||
}
|
||||
|
||||
$this->config = new RippingCluster_Config(RippingCluster_DBConfig);
|
||||
$this->database = new RippingCluster_Database($this->config);
|
||||
$this->config->setDatabase($this->database);
|
||||
|
||||
$this->log = new RippingCluster_Log($this->database, $this->config, $log_table);
|
||||
$this->request = new RippingCluster_RequestParser($request_string);
|
||||
$this->cache = new RippingCluster_Cache($this->config);
|
||||
|
||||
$this->smarty = new Smarty();
|
||||
$this->smarty->template_dir = './templates';
|
||||
$this->smarty->compile_dir = './tmp/templates';
|
||||
$this->smarty->cache_dir = './tmp/cache';
|
||||
$this->smarty->config_fir = './config';
|
||||
|
||||
$this->smarty->register_modifier('formatDuration', array('RippingCluster_Main', 'formatDuration'));
|
||||
|
||||
$this->smarty->assign('version', '0.1');
|
||||
|
||||
$this->base_uri = dirname($_SERVER['SCRIPT_NAME']) . '/';
|
||||
$this->smarty->assign('base_uri', $this->base_uri);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return RippingCluster_Main
|
||||
*/
|
||||
public static function instance() {
|
||||
if (!self::$instance) {
|
||||
self::$instance = new RippingCluster_Main();
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function smarty() {
|
||||
return $this->smarty;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return RippingCluster_Config
|
||||
*/
|
||||
public function config() {
|
||||
return $this->config;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return RippingCluster_Database
|
||||
*/
|
||||
public function database() {
|
||||
return $this->database;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return RippingCluster_Log
|
||||
*/
|
||||
public function log() {
|
||||
return $this->log;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return RippingCluster_RequestParser
|
||||
*/
|
||||
public function request() {
|
||||
return $this->request;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return RippingCluster_Cache
|
||||
*/
|
||||
public function cache() {
|
||||
return $this->cache;
|
||||
}
|
||||
|
||||
public function baseUri() {
|
||||
return $this->base_uri;
|
||||
}
|
||||
|
||||
public function absoluteUrl($relative_url) {
|
||||
$secure = isset($_SERVER['secure']);
|
||||
$port = $_SERVER['HTTP_PORT'];
|
||||
return 'http' . ($secure ? 's' : '') . '://'
|
||||
. $_SERVER['HTTP_HOST'] . (($port == 80 || ($secure && $port == 443)) ? '' : ':' . $port)
|
||||
. '/' . $this->base_uri . $relative_url;
|
||||
}
|
||||
|
||||
public static function initialise() {
|
||||
spl_autoload_register(array('RippingCluster_Main','autoload'));
|
||||
}
|
||||
|
||||
public static function autoload($classname) {
|
||||
// Ensure the classname contains only valid class name characters
|
||||
if (!preg_match('/^[A-Z][a-zA-Z0-9_]*$/', $classname)) {
|
||||
throw new Exception('Illegal characters in classname'); // TODO Subclass this exception
|
||||
}
|
||||
|
||||
// Ensure the class to load begins with our prefix
|
||||
if (!preg_match('/^RippingCluster_/', $classname)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Special case: All exceptions are stored in the same file
|
||||
if (preg_match('/^RippingCluster_Exception/', $classname)) {
|
||||
require_once(RippingCluster_Lib . 'RippingCluster/Exceptions.class.php');
|
||||
return;
|
||||
}
|
||||
|
||||
// Replace any underscores with directory separators
|
||||
$filename = RippingCluster_Lib . preg_replace('/_/', '/', $classname);
|
||||
|
||||
// Tack on the class file suffix
|
||||
$filename .= '.class.php';
|
||||
|
||||
// If this file exists, load it
|
||||
if (file_exists($filename)) {
|
||||
require_once $filename;
|
||||
}
|
||||
}
|
||||
|
||||
public static function mkdir_recursive($directory, $permissions=0777) {
|
||||
$parts = explode('/', $directory);
|
||||
$path = '';
|
||||
for ($i=1,$l=count($parts); $i<=$l; $i++) {
|
||||
$iPath = $parts;
|
||||
$path = join('/', array_slice($iPath, 0, $i));
|
||||
if (empty($path)) continue;
|
||||
if (!file_exists($path)) {
|
||||
if (!mkdir($path)) return false;
|
||||
if (!chmod($path, $permissions)) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function issetelse($var, $default = null) {
|
||||
if (isset($var)) {
|
||||
return $var;
|
||||
}
|
||||
|
||||
if (is_string($default) && preg_match('/^RippingCluster_Exception/', $default) && class_exists($default) && is_subclass_of($default, RippingCluster_Exception)) {
|
||||
throw new $default();
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
|
||||
public static function formatDuration($time) {
|
||||
if (is_null($time)) {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
$labels = array('seconds', 'minutes', 'hours', 'days', 'weeks', 'months', 'years');
|
||||
$limits = array(60, 3600, 86400, 604800, 2592000, 31556926, PHP_INT_MAX);
|
||||
|
||||
$working_time = $time;
|
||||
|
||||
$result = "";
|
||||
$ptr = count($labels) - 1;
|
||||
|
||||
while ($ptr >= 0 && $working_time < $limits[$ptr]) {
|
||||
--$ptr;
|
||||
}
|
||||
|
||||
while ($ptr >= 0) {
|
||||
$unit_time = floor($working_time / $limits[$ptr]);
|
||||
$working_time -= $unit_time * $limits[$ptr];
|
||||
$result = $result . ' ' . $unit_time . ' ' . $labels[$ptr];
|
||||
--$ptr;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
RippingCluster_Main::initialise();
|
||||
|
||||
?>
|
||||
74
lib/RippingCluster/Page.class.php
Normal file
74
lib/RippingCluster/Page.class.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
class RippingCluster_Page {
|
||||
|
||||
private $smarty;
|
||||
private $request;
|
||||
|
||||
private $page;
|
||||
|
||||
public function __construct(Smarty $smarty, RippingCluster_RequestParser $request) {
|
||||
$this->smarty = $smarty;
|
||||
$this->request = $request;
|
||||
$this->page = $request->page();
|
||||
}
|
||||
|
||||
public function page() {
|
||||
return $this->page;
|
||||
}
|
||||
|
||||
public function template_filename() {
|
||||
return $this->page . '.tpl';
|
||||
}
|
||||
|
||||
public function evaluate($template_variables = array()) {
|
||||
$code_filename = $this->page . '.php';
|
||||
$template_filename = $this->template_filename();
|
||||
|
||||
try {
|
||||
$this->render($template_filename, $code_filename, $template_variables);
|
||||
} catch (RippingCluster_Exception_AbortEntirePage $e) {
|
||||
return false;
|
||||
} catch (RippingCluster_Exception_FileNotFound $e) {
|
||||
$this->render('errors/404.tpl', 'errors/404.php');
|
||||
} catch (RippingCluster_Exception $e) {
|
||||
$this->render('errors/unhandled-exception.tpl', 'errors/unhandled-exception.php', array(
|
||||
'exception' => $e,
|
||||
));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function render($template_filename, $code_filename = null, $template_variables = array()) {
|
||||
if ( ! $this->smarty->template_exists($template_filename)) {
|
||||
throw new RippingCluster_Exception_FileNotFound($template_filename);
|
||||
}
|
||||
|
||||
// Copy all the template variables into the namespace for this function,
|
||||
// so that they are readily available to the template
|
||||
foreach ($template_variables as $__k => $__v) {
|
||||
$$__k = $__v;
|
||||
}
|
||||
|
||||
// Include the template code file, which will do all the work for this page
|
||||
$real_code_filename = 'pages/' . $code_filename;
|
||||
if ($code_filename && file_exists($real_code_filename)) {
|
||||
include $real_code_filename;
|
||||
}
|
||||
|
||||
// Now execute the template itself, which will render the results of the code file
|
||||
$this->smarty->assign('page_content', $this->smarty->fetch($template_filename));
|
||||
}
|
||||
|
||||
public static function redirect($relative_url) {
|
||||
$absolute_url = RippingCluster_Main::instance()->absoluteUrl($relative_url);
|
||||
|
||||
header("Location: $absolute_url");
|
||||
|
||||
throw new RippingCluster_Exception_AbortEntirePage();
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
?>
|
||||
50
lib/RippingCluster/PluginFactory.class.php
Normal file
50
lib/RippingCluster/PluginFactory.class.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
abstract class RippingCluster_PluginFactory {
|
||||
|
||||
static protected $validPlugins;
|
||||
|
||||
abstract public static function init();
|
||||
|
||||
public static function getValidPlugins() {
|
||||
return array_keys(self::$validPlugins);
|
||||
}
|
||||
|
||||
protected static function findPlugins($directory) {
|
||||
$plugins = array();
|
||||
|
||||
$iterator = new RippingCluster_Utility_ClassFilesIterator(new RippingCluster_Utility_VisibleFilesIterator(new DirectoryIterator(RippingCluster_Lib . $directory)));
|
||||
|
||||
foreach ($iterator as /** @var SplFileInfo */ $file) {
|
||||
$plugin = preg_replace('/.class.php$/', '', $file->getFilename());
|
||||
$plugins[] = $plugin;
|
||||
}
|
||||
|
||||
return $plugins;
|
||||
}
|
||||
|
||||
protected static function loadPlugins($plugins, $prefix) {
|
||||
self::$validPlugins = array();
|
||||
|
||||
foreach ($plugins as $plugin) {
|
||||
$fullClassname = $prefix . $plugin;
|
||||
if ( ! class_exists($fullClassname, true)) {
|
||||
echo "Cannot load $fullClassname\n";
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( ! in_array('RippingCluster_Worker_IPlugin', class_implements($fullClassname))) {
|
||||
echo "$plugin does not implement the necessary interfaces\n";
|
||||
continue;
|
||||
}
|
||||
|
||||
// Initialise the plugin
|
||||
call_user_func(array($fullClassname, 'init'));
|
||||
|
||||
self::$validPlugins[$plugin] = $fullClassname;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
?>
|
||||
97
lib/RippingCluster/RequestParser.class.php
Normal file
97
lib/RippingCluster/RequestParser.class.php
Normal file
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
class RippingCluster_RequestParser {
|
||||
|
||||
private $request_string;
|
||||
private $page = array();
|
||||
private $vars = array();
|
||||
|
||||
public function __construct($request_string) {
|
||||
$this->request_string = $request_string;
|
||||
|
||||
$this->parse();
|
||||
}
|
||||
|
||||
public function parse() {
|
||||
if (!$this->request_string) {
|
||||
$this->page = array('home');
|
||||
return;
|
||||
}
|
||||
|
||||
$components = explode('/', $this->request_string);
|
||||
if (!$components) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Read through the components list looking for elements matching known directories and files
|
||||
// to determine which page this request is for
|
||||
$base_dir = 'templates';
|
||||
while (true) {
|
||||
if ($components && ! $components[0]) {
|
||||
// Skip over any empty components before we find a page
|
||||
array_shift($components);
|
||||
}
|
||||
|
||||
if ($components && is_dir($base_dir . '/' . $components[0])) {
|
||||
$base_dir .= '/' . $components[0];
|
||||
array_push($this->page, array_shift($components));
|
||||
} elseif ($components && is_file($base_dir . '/' . $components[0] . '.tpl')) {
|
||||
// We have found a valid page, so break the loop here,
|
||||
// leaving the remaining components as key/value pairs
|
||||
array_push($this->page, array_shift($components));
|
||||
break;
|
||||
} else {
|
||||
// See if we've already seen a component and assumed it referred to a dir when a file of the same name exists
|
||||
if ($this->page && is_file($base_dir . '.tpl')) {
|
||||
break;
|
||||
} elseif ( ! $components && is_file($base_dir . '/index.tpl')) {
|
||||
// The last component in the path was a valid directory, and a directory index exists
|
||||
array_push($this->page, 'index');
|
||||
break;
|
||||
} else {
|
||||
// No valid page was found, so display an error page
|
||||
$this->page = array('404');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The subsequent tokens are parameters for this page in key value pairs
|
||||
while ($components) {
|
||||
// If the url ended with a trailing slash, the last element will be null
|
||||
$last_element = $components[count($components) - 1];
|
||||
if ($last_element == "") {
|
||||
array_pop($components);
|
||||
}
|
||||
|
||||
$this->vars[array_shift($components)] = $components ? array_shift($components) : true;
|
||||
}
|
||||
}
|
||||
|
||||
public function page() {
|
||||
return join('/', $this->page);
|
||||
}
|
||||
|
||||
public function exists($key) {
|
||||
return isset($this->vars[$key]);
|
||||
}
|
||||
|
||||
public function get($key, $default = null) {
|
||||
if (isset($this->vars[$key])) {
|
||||
return $this->vars[$key];
|
||||
}
|
||||
|
||||
if (is_string($default) && preg_match('/^RippingCluster_Exception/', $default) && class_exists($default) && is_subclass_of($default, RippingCluster_Exception)) {
|
||||
throw new $default();
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
|
||||
public function request_string() {
|
||||
return $this->request_string;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
?>
|
||||
233
lib/RippingCluster/Rips/Source.class.php
Normal file
233
lib/RippingCluster/Rips/Source.class.php
Normal file
@@ -0,0 +1,233 @@
|
||||
<?php
|
||||
|
||||
class RippingCluster_Rips_Source {
|
||||
|
||||
const PM_TITLE = 0;
|
||||
const PM_CHAPTER = 1;
|
||||
const PM_AUDIO = 2;
|
||||
const PM_SUBTITLE = 3;
|
||||
|
||||
protected $source;
|
||||
protected $output;
|
||||
protected $titles = array();
|
||||
|
||||
protected function __construct($source_filename, $scan_dir, $use_cache) {
|
||||
$this->source = $source_filename;
|
||||
|
||||
if ($scan_dir) {
|
||||
$this->scan();
|
||||
}
|
||||
|
||||
$main = RippingCluster_Main::instance();
|
||||
$cache = $main->cache();
|
||||
$config = $main->config();
|
||||
|
||||
if ($scan_dir && $use_cache) {
|
||||
$cache->store($this->source, serialize($this), $config->get('rips.cache_ttl'));
|
||||
}
|
||||
}
|
||||
|
||||
public static function load($source_filename, $scan_dir = true, $use_cache = true) {
|
||||
$cache = RippingCluster_Main::instance()->cache();
|
||||
|
||||
if ($use_cache && $cache->exists($source_filename)) {
|
||||
return unserialize($cache->fetch($source_filename));
|
||||
} else {
|
||||
return new RippingCluster_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 RippingCluster_Exception_InvalidSourceDirectory($source_filename);
|
||||
}
|
||||
|
||||
$config = RippingCluster_Main::instance()->config();
|
||||
$source_basedir = $config->get('rips.source_dir');
|
||||
$real_source_basedir = realpath($source_basedir);
|
||||
if (substr($real_source_filename, 0, strlen($real_source_basedir)) != $real_source_basedir) {
|
||||
throw new RippingCluster_Exception_InvalidSourceDirectory($source_filename);
|
||||
}
|
||||
|
||||
return self::load($source_filename, $scan_dir, $use_cache);
|
||||
}
|
||||
|
||||
protected function scan() {
|
||||
$source_shell = escapeshellarg($this->source);
|
||||
$handbrake_cmd = "HandBrakeCLI -i {$source_shell} -t 0";
|
||||
list($retval, $handbrake_output, $handbrake_error) = RippingCluster_ForegroundTask::execute($handbrake_cmd);
|
||||
|
||||
// Process the output
|
||||
$lines = explode("\n", $handbrake_error);
|
||||
$title = null;
|
||||
$mode = self::PM_TITLE;
|
||||
|
||||
foreach ($lines as $line) {
|
||||
// Skip any line that doesn't begin with a + (with optional leading whitespace)
|
||||
if ( ! preg_match('/\s*\+/', $line)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$matches = array();
|
||||
switch (true) {
|
||||
case preg_match('/^\+ title (?P<id>\d+):$/', $line, $matches): {
|
||||
if ($title) {
|
||||
$this->addTitle($title);
|
||||
}
|
||||
|
||||
$mode = self::PM_TITLE;
|
||||
$title = new RippingCluster_Rips_SourceTitle($matches['id']);
|
||||
} break;
|
||||
|
||||
case $title && preg_match('/^ \+ chapters:$/', $line): {
|
||||
$mode = self::PM_CHAPTER;
|
||||
} break;
|
||||
|
||||
case $title && preg_match('/^ \+ audio tracks:$/', $line): {
|
||||
$mode = self::PM_AUDIO;
|
||||
|
||||
} break;
|
||||
|
||||
case $title && preg_match('/^ \+ subtitle tracks:$/', $line): {
|
||||
$mode = self::PM_SUBTITLE;
|
||||
} break;
|
||||
|
||||
case $title && $mode == self::PM_TITLE && preg_match('/^ \+ duration: (?P<duration>\d+:\d+:\d+)$/', $line, $matches): {
|
||||
$title->setDuration($matches['duration']);
|
||||
} break;
|
||||
|
||||
case $title && $mode == self::PM_TITLE && preg_match('/^ \+ angle\(s\) (?P<angles>\d+)$/', $line, $matches): {
|
||||
$title->setAngles($matches['angles']);
|
||||
} break;
|
||||
|
||||
//" + size: 720x576, pixel aspect: 64/45, display aspect: 1.78, 25.000 fps"
|
||||
case $title && $mode == self::PM_TITLE && preg_match('/^ \+ size: (?P<width>\d+)x(?P<height>\d+), pixel aspect: (?P<pixel_aspect>\d+\/\d+), display aspect: (?P<display_aspect>[\d\.]+), (?<framerate>[\d\.]+) fps$/', $line, $matches): {
|
||||
$title->setDisplayInfo(
|
||||
$matches['width'], $matches['height'], $matches['pixel_aspect'],
|
||||
$matches['display_aspect'], $matches['framerate']
|
||||
);
|
||||
} break;
|
||||
|
||||
case $title && $mode == self::PM_TITLE && preg_match('/^ \+ autocrop: (?P<autocrop>(?:\d+\/?){4})$/', $line, $matches): {
|
||||
$title->setAutocrop($matches['autocrop']);
|
||||
} break;
|
||||
|
||||
case $title && $mode == self::PM_CHAPTER && preg_match('/^ \+ (?P<id>\d+): cells \d+->\d+, \d+ blocks, duration (?P<duration>\d+:\d+:\d+)$/', $line, $matches): {
|
||||
$title->addChapter($matches['id'], $matches['duration']);
|
||||
} break;
|
||||
|
||||
case $title && $mode == self::PM_AUDIO && preg_match('/^ \+ (?P<id>\d+), (?P<name>.+) \((?P<format>.+)\) \((?P<channels>(.+ ch|Dolby Surround))\) \((?P<language>.+)\), (?P<samplerate>\d+)Hz, (?P<bitrate>\d+)bps$/', $line, $matches): {
|
||||
$title->addAudioTrack(
|
||||
new RippingCluster_Rips_SourceAudioTrack(
|
||||
$matches['id'], $matches['name'], $matches['format'], $matches['channels'],
|
||||
$matches['language'], $matches['samplerate'], $matches['bitrate']
|
||||
)
|
||||
);
|
||||
} break;
|
||||
|
||||
case $title && $mode == self::PM_SUBTITLE && preg_match('/^ \+ (?P<id>\d+), (?P<name>.+) \((?P<language>.+)\) \((?P<format>.+)\)$/', $line, $matches): {
|
||||
$title->addSubtitleTrack(
|
||||
new RippingCluster_Rips_SourceSubtitleTrack(
|
||||
$matches['id'], $matches['name'], $matches['language'], $matches['format']
|
||||
)
|
||||
);
|
||||
} break;
|
||||
|
||||
default: {
|
||||
// Ignore this unmatched line
|
||||
} break;
|
||||
|
||||
}
|
||||
|
||||
$this->output .= $line . "\n";
|
||||
}
|
||||
|
||||
// Handle the last title found as a special case
|
||||
if ($title) {
|
||||
$this->addTitle($title);
|
||||
}
|
||||
}
|
||||
|
||||
public static function isCached($source_filename) {
|
||||
$main = RippingCluster_Main::instance();
|
||||
$cache = $main->cache();
|
||||
$config = $main->config();
|
||||
|
||||
return $cache->exists($source_filename, $config->get('rips.cache_ttl'));
|
||||
}
|
||||
|
||||
public static function encodeFilename($filename) {
|
||||
return str_replace("/", "-", base64_encode($filename));
|
||||
}
|
||||
|
||||
public function addTitle(RippingCluster_Rips_SourceTitle $title) {
|
||||
$this->titles[] = $title;
|
||||
}
|
||||
|
||||
public function longestTitle() {
|
||||
$longest_title = null;
|
||||
$maximum_duration = 0;
|
||||
|
||||
if ( ! $this->titles) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ($this->titles as $title) {
|
||||
$duration = $title->durationInSeconds();
|
||||
if ($duration > $maximum_duration) {
|
||||
$longest_title = $title;
|
||||
$maximum_duration = $duration;
|
||||
}
|
||||
}
|
||||
|
||||
return $longest_title;
|
||||
}
|
||||
|
||||
public function longestTitleIndex() {
|
||||
$longest_index = null;
|
||||
$maximmum_duration = 0;
|
||||
|
||||
if ( ! $this->titles) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for ($i = 0, $l = count($this->titles); $i < $l; ++$i) {
|
||||
$title = $this->titles[$i];
|
||||
$duration = $title->durationInSeconds();
|
||||
if ($duration > $maximum_duration) {
|
||||
$longest_index = $i;
|
||||
$maximum_duration = $duration;
|
||||
}
|
||||
}
|
||||
|
||||
return $longest_index;
|
||||
}
|
||||
|
||||
public function filename() {
|
||||
return $this->source;
|
||||
}
|
||||
|
||||
public function filenameEncoded() {
|
||||
return self::encodeFilename($this->source);
|
||||
}
|
||||
|
||||
public function output() {
|
||||
return $this->output;
|
||||
}
|
||||
|
||||
public function titleCount() {
|
||||
return count($this->titles);
|
||||
}
|
||||
|
||||
public function titles() {
|
||||
return $this->titles;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
?>
|
||||
53
lib/RippingCluster/Rips/SourceAudioTrack.class.php
Normal file
53
lib/RippingCluster/Rips/SourceAudioTrack.class.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
class RippingCluster_Rips_SourceAudioTrack {
|
||||
|
||||
protected $id;
|
||||
protected $name;
|
||||
protected $format;
|
||||
protected $channels;
|
||||
protected $language;
|
||||
protected $samplerate;
|
||||
protected $bitrate;
|
||||
|
||||
public function __construct($id, $name, $format, $channels, $language, $samplerate, $bitrate) {
|
||||
$this->id = $id;
|
||||
$this->name = $name;
|
||||
$this->format = $format;
|
||||
$this->channels = $channels;
|
||||
$this->language = $language;
|
||||
$this->samplerate = $samplerate;
|
||||
$this->bitrate = $bitrate;
|
||||
}
|
||||
|
||||
public function id() {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function name() {
|
||||
return $name;
|
||||
}
|
||||
|
||||
public function format() {
|
||||
return $this->format;
|
||||
}
|
||||
|
||||
public function channels() {
|
||||
return $this->channels;
|
||||
}
|
||||
|
||||
public function language() {
|
||||
return $this->language;
|
||||
}
|
||||
|
||||
public function samplerate() {
|
||||
return $this->samplerate;
|
||||
}
|
||||
|
||||
public function bitrate() {
|
||||
return $this->bitrate;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
?>
|
||||
31
lib/RippingCluster/Rips/SourceLister.class.php
Normal file
31
lib/RippingCluster/Rips/SourceLister.class.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
class RippingCluster_Rips_SourceLister {
|
||||
|
||||
protected $base_directory;
|
||||
protected $sources = array();
|
||||
|
||||
public function __construct($base_directory) {
|
||||
$this->base_directory = $base_directory;
|
||||
|
||||
$this->scan();
|
||||
}
|
||||
|
||||
public function scan() {
|
||||
if (!is_dir($this->base_directory)) {
|
||||
throw new RippingCluster_Exception_InvalidSourceDirectory($this->base_directory);
|
||||
}
|
||||
|
||||
$iterator = new RippingCluster_Utility_DvdDirectoryIterator(new RippingCluster_Utility_VisibleFilesIterator(new DirectoryIterator($this->base_directory)));
|
||||
foreach ($iterator as /** @var SplFileInfo */ $source_vts) {
|
||||
$this->sources[] = RippingCluster_Rips_Source::load($source_vts->getPathname(), false);
|
||||
}
|
||||
}
|
||||
|
||||
public function sources() {
|
||||
return $this->sources;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
?>
|
||||
35
lib/RippingCluster/Rips/SourceSubtitleTrack.class.php
Normal file
35
lib/RippingCluster/Rips/SourceSubtitleTrack.class.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
class RippingCluster_Rips_SourceSubtitleTrack {
|
||||
|
||||
protected $id;
|
||||
protected $name;
|
||||
protected $language;
|
||||
protected $format;
|
||||
|
||||
public function __construct($id, $name, $language, $format) {
|
||||
$this->id = $id;
|
||||
$this->name = $name;
|
||||
$this->language = $language;
|
||||
$this->format = $format;
|
||||
}
|
||||
|
||||
public function id() {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function name() {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function language() {
|
||||
return $this->language;
|
||||
}
|
||||
|
||||
public function format() {
|
||||
return $this->format;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
?>
|
||||
127
lib/RippingCluster/Rips/SourceTitle.class.php
Normal file
127
lib/RippingCluster/Rips/SourceTitle.class.php
Normal file
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
class RippingCluster_Rips_SourceTitle {
|
||||
|
||||
protected $id;
|
||||
//protected $vts;
|
||||
//protected $ttn;
|
||||
//protected $cell_count;
|
||||
//protected $blocks;
|
||||
|
||||
protected $angles;
|
||||
protected $duration;
|
||||
protected $width;
|
||||
protected $height;
|
||||
protected $pixel_aspect;
|
||||
protected $display_aspect;
|
||||
protected $framerate;
|
||||
protected $autocrop;
|
||||
|
||||
protected $chapters = array();
|
||||
protected $audio = array();
|
||||
protected $subtitles = array();
|
||||
|
||||
public function __construct($id) {
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
public function id() {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function angles() {
|
||||
return $this->angles;
|
||||
}
|
||||
|
||||
public function setAngles($angles) {
|
||||
$this->angles = $angles;
|
||||
}
|
||||
|
||||
public function duration() {
|
||||
return $this->duration;
|
||||
}
|
||||
|
||||
public function durationInSeconds() {
|
||||
$time = explode(":", $this->duration);
|
||||
return ($time[0] * 3600) + ($time[1] * 60) + $time[2];
|
||||
}
|
||||
|
||||
public function setDuration($duration) {
|
||||
$this->duration = $duration;
|
||||
}
|
||||
|
||||
public function width() {
|
||||
return $this->width;
|
||||
}
|
||||
|
||||
public function height() {
|
||||
return $this->height;
|
||||
}
|
||||
|
||||
public function displayAspect() {
|
||||
return $this->display_aspect;
|
||||
}
|
||||
|
||||
public function pixelAspect() {
|
||||
return $this->pixel_aspect;
|
||||
}
|
||||
|
||||
public function framerate() {
|
||||
return $this->framerate;
|
||||
}
|
||||
|
||||
public function setDisplayInfo($width, $height, $display_aspect, $pixel_aspect, $framerate) {
|
||||
$this->width = $width;
|
||||
$this->height = $height;
|
||||
$this->pixel_aspect = $pixel_aspect;
|
||||
$this->display_aspect = $display_aspect;
|
||||
$this->framerate = $framerate;
|
||||
}
|
||||
|
||||
public function autocrop() {
|
||||
return $this->autocrop;
|
||||
}
|
||||
|
||||
public function setAutocrop($autocrop) {
|
||||
$this->autocrop = $autocrop;
|
||||
}
|
||||
|
||||
public function chapterCount() {
|
||||
return count($this->chapters);
|
||||
}
|
||||
|
||||
public function chapters() {
|
||||
return $this->chapters;
|
||||
}
|
||||
|
||||
public function addChapter($chapter_id, $duration) {
|
||||
$this->chapters[$chapter_id] = $duration;
|
||||
}
|
||||
|
||||
public function audioTrackCount() {
|
||||
return count($this->audio);
|
||||
}
|
||||
|
||||
public function audioTracks() {
|
||||
return $this->audio;
|
||||
}
|
||||
|
||||
public function addAudioTrack(RippingCluster_Rips_SourceAudioTrack $audio_track) {
|
||||
$this->audio[] = $audio_track;
|
||||
}
|
||||
|
||||
public function subtitleTrackCount() {
|
||||
return count($this->subtitles);
|
||||
}
|
||||
|
||||
public function subtitleTracks() {
|
||||
return $this->subtitles;
|
||||
}
|
||||
|
||||
public function addSubtitleTrack(RippingCluster_Rips_SourceSubtitleTrack $subtitle_track) {
|
||||
$this->subtitles[] = $subtitle_track;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
?>
|
||||
9
lib/RippingCluster/Utility/ClassFilesIterator.class.php
Normal file
9
lib/RippingCluster/Utility/ClassFilesIterator.class.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
class RippingCluster_Utility_ClassFilesIterator extends FilterIterator {
|
||||
public function accept() {
|
||||
return preg_match('/.class.php$/i', $this->current()->getFilename());
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
class RippingCluster_Utility_DvdDirectoryIterator extends FilterIterator {
|
||||
public function accept() {
|
||||
return is_dir($this->current()->getPathname() . DIRECTORY_SEPARATOR . 'VIDEO_TS');
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
class RippingCluster_Utility_VisibleFilesIterator extends FilterIterator {
|
||||
public function accept() {
|
||||
return !(substr($this->current()->getFilename(), 0, 1) == '.');
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
48
lib/RippingCluster/Worker.class.php
Normal file
48
lib/RippingCluster/Worker.class.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
class RippingCluster_Worker {
|
||||
|
||||
protected $gearman;
|
||||
|
||||
public function __construct() {
|
||||
$this->init();
|
||||
|
||||
}
|
||||
|
||||
private function init() {
|
||||
if ($this->gearman) {
|
||||
return;
|
||||
}
|
||||
|
||||
$config = RippingCluster_Main::instance()->config();
|
||||
|
||||
$this->gearman = new GearmanWorker();
|
||||
$this->gearman->addServers($config->get('rips.job_servers'));
|
||||
|
||||
// Load all the plugin classes
|
||||
echo "Loading Plugins\n";
|
||||
RippingCluster_Worker_PluginFactory::scan();
|
||||
foreach (RippingCluster_Worker_PluginFactory::getValidPlugins() as $plugin) {
|
||||
echo "Grabbing worker functions provided by {$plugin}\n";
|
||||
$workerFunctions = RippingCluster_Worker_PluginFactory::getPluginWorkerFunctions($plugin);
|
||||
|
||||
foreach ($workerFunctions as $function => $callback) {
|
||||
echo "Adding {$plugin}::{$callback[1]} as {$function}\n";
|
||||
$this->gearman->addFunction($function, $callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function start() {
|
||||
while($this->gearman->work()) {
|
||||
if ($this->gearman->returnCode() != GEARMAN_SUCCESS) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
?>
|
||||
16
lib/RippingCluster/Worker/IPlugin.class.php
Normal file
16
lib/RippingCluster/Worker/IPlugin.class.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
interface RippingCluster_Worker_IPlugin {
|
||||
|
||||
public static function workerFunctions();
|
||||
|
||||
public static function rip(GearmanJob $job);
|
||||
|
||||
public function evaluateOption($name, $option = null);
|
||||
|
||||
public function callbackStdout($id, $data);
|
||||
|
||||
public function callbackStderr($id, $data);
|
||||
}
|
||||
|
||||
?>
|
||||
130
lib/RippingCluster/Worker/Plugin/HandBrake.class.php
Normal file
130
lib/RippingCluster/Worker/Plugin/HandBrake.class.php
Normal file
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
class RippingCluster_Worker_Plugin_HandBrake implements RippingCluster_Worker_IPlugin {
|
||||
|
||||
const DEINTERLACE_ALWAYS = 1;
|
||||
const DEINTERLACE_SELECTIVELY = 2;
|
||||
|
||||
private $stdout;
|
||||
private $stderr;
|
||||
|
||||
private $job;
|
||||
|
||||
private $client_job_id;
|
||||
private $rip_options;
|
||||
|
||||
private function __construct(GearmanJob $job) {
|
||||
$this->stdout = '';
|
||||
$this->stderr = '';
|
||||
|
||||
$this->job = $job;
|
||||
|
||||
$this->client_job_id = $job->unique();
|
||||
$this->rip_options = unserialize($job->workload());
|
||||
}
|
||||
|
||||
public static function init() {
|
||||
|
||||
}
|
||||
|
||||
public static function workerFunctions() {
|
||||
return array(
|
||||
'handbrake_rip' => array(__CLASS__, 'rip'),
|
||||
);
|
||||
}
|
||||
|
||||
public static function rip(GearmanJob $job) {
|
||||
$rip = new self($job);
|
||||
$rip->execute();
|
||||
}
|
||||
|
||||
public function execute() {
|
||||
$config = RippingCluster_Main::instance()->config();
|
||||
|
||||
$handbrake_cmd_raw = array(
|
||||
'-n', $config->get('rips.nice'),
|
||||
$config->get('rips.handbrake_binary'),
|
||||
$this->evaluateOption('input_filename', '-i'),
|
||||
$this->evaluateOption('output_filename', '-o'),
|
||||
$this->evaluateOption('title'),
|
||||
$this->evaluateOption('format', '-f'),
|
||||
$this->evaluateOption('video_codec', '-e'),
|
||||
$this->evaluateOption('quantizer', '-q'),
|
||||
$this->evaluateOption('video_width', '-w'),
|
||||
$this->evaluateOption('video_height', '-l'),
|
||||
$this->evaluateOption('deinterlace'),
|
||||
$this->evaluateOption('audio_tracks', '-a'),
|
||||
$this->evaluateOption('audio_codec', '-E'),
|
||||
$this->evaluateOption('audio_names', '-A'),
|
||||
$this->evaluateOption('subtitle_tracks', '-s'),
|
||||
);
|
||||
|
||||
$handbrake_cmd = array($config->get('rips.nice_binary'));
|
||||
foreach(new RecursiveIteratorIterator(new RecursiveArrayIterator($handbrake_cmd_raw)) as $value) {
|
||||
$handbrake_cmd[] = escapeshellarg($value);
|
||||
}
|
||||
$handbrake_cmd = join(' ', $handbrake_cmd);
|
||||
|
||||
return RippingCluster_ForegroundTask::execute($handbrake_cmd, null, null, null, array($this, 'callbackStdout'), array($this, 'callbackStderr'), $this);
|
||||
|
||||
}
|
||||
|
||||
public function evaluateOption($name, $option = null) {
|
||||
switch($name) {
|
||||
case 'title': {
|
||||
if (!$this->rip_options[$name] || (int)$this->rip_options[$name] < 0) {
|
||||
return array('-L');
|
||||
} else {
|
||||
return array('-t', $this->rip_options[$name]);
|
||||
}
|
||||
} break;
|
||||
|
||||
case 'deinterlace': {
|
||||
switch ($this->rip_options[$name]) {
|
||||
case self::DEINTERLACE_ALWAYS:
|
||||
return array('-d');
|
||||
case self::DEINTERLACE_SELECTIVELY:
|
||||
return array('-5');
|
||||
default:
|
||||
return array();
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return array(isset($option) ? $option : $name, $this->rip_options[$name]);
|
||||
}
|
||||
}
|
||||
|
||||
public function callbackStdout($rip, $data) {
|
||||
$this->stdout .= $data;
|
||||
|
||||
while (count($lines = preg_split('/[\r\n]+/', $this->stdout, 2)) > 1) {
|
||||
$line = $lines[0];
|
||||
$this->stdout = $lines[1];
|
||||
|
||||
$log = RippingCluster_Main::instance()->log();
|
||||
$log->info($line);
|
||||
}
|
||||
}
|
||||
|
||||
public function callbackStderr($rip, $data) {
|
||||
$this->stderr .= $data;
|
||||
|
||||
while (count($lines = preg_split('/[\r\n]+/', $this->stderr, 2)) > 1) {
|
||||
$line = $lines[0];
|
||||
$rip->stderr = $lines[1];
|
||||
|
||||
$matches = array();
|
||||
if (preg_match('/Encoding: task \d+ of \d+, (\d+\.\d+) %/', $line, $matches)) {
|
||||
$numerator = 100 * $matches[1];
|
||||
$this->job->sendStatus($numerator, 100);
|
||||
} else {
|
||||
$log = RippingCluster_Main::instance()->log();
|
||||
$log->debug($line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
?>
|
||||
27
lib/RippingCluster/Worker/PluginFactory.class.php
Normal file
27
lib/RippingCluster/Worker/PluginFactory.class.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
class RippingCluster_Worker_PluginFactory extends RippingCluster_PluginFactory {
|
||||
|
||||
const PLUGIN_DIR = 'RippingCluster/Worker/Plugin/';
|
||||
const PREFIX = 'RippingCluster_Worker_Plugin_';
|
||||
|
||||
public static function init() {
|
||||
|
||||
}
|
||||
|
||||
public static function scan() {
|
||||
$candidatePlugins = parent::findPlugins(self::PLUGIN_DIR);
|
||||
|
||||
parent::loadPlugins($candidatePlugins, self::PREFIX);
|
||||
}
|
||||
|
||||
public static function getPluginWorkerFunctions($plugin) {
|
||||
if ( ! isset(parent::$validPlugins[$plugin])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return call_user_func(array(parent::$validPlugins[$plugin], 'workerFunctions'));
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
13
lib/RippingCluster/WorkerLogEntry.class.php
Normal file
13
lib/RippingCluster/WorkerLogEntry.class.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
class RippingCluster_WorkerLogEntry extends RippingCluster_LogEntry {
|
||||
|
||||
public static function initialise() {
|
||||
parent::$table_name = 'worker_log';
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
RippingCluster_WorkerLogEntry::initialise();
|
||||
|
||||
?>
|
||||
Reference in New Issue
Block a user