6 Commits

Author SHA1 Message Date
7ffccb851c WIP parser for mkvinfo output 2010-10-12 19:13:21 +01:00
8f88fba0ca Added set accessors to Source objects
For Source plugins that can't parse all the required information before
creating the object, set accessors are needed to populate the
information afterwards.
2010-10-12 19:06:27 +01:00
ec4cc8dad4 Removes obsolete status callback from gearman client
Status callback function was previously removed from run-jobs, but the
callback was still being registered. This change removes the
registration to prevent a warning.
2010-09-25 17:11:47 +01:00
14c6d51564 Merge branch 'master' of git+ssh://git.sihnon.net/home/git/public/handbrake-cluster-webui into feature-mkv-plugins 2010-09-24 20:08:07 +01:00
2ef47de25c Added MKV source/worker plugins, tidied sources page
Added placeholder for source plugin to read from an existing mkv file.
Added corresponding worker placeholder to transcode an mkv file using
ffmpeg.
Updated the sources page to show which sources come from which plugins.
2010-09-24 20:05:37 +01:00
9cae5046dc Fixes bug with incorrect exception name
Updates the name of the exception thrown when a database connection
cannot be established, to match with the defined exception class name.
2010-09-18 13:13:31 +01:00
16 changed files with 452 additions and 33 deletions

View File

@@ -132,8 +132,8 @@ class RippingCluster_Config {
} }
switch ($this->settings[$key]['type']) { switch ($this->settings[$key]['type']) {
case TYPE_STRING_LIST: case self::TYPE_STRING_LIST:
return explode("\n", $this->settings[$key]['value']); return array_map('trim', explode("\n", $this->settings[$key]['value']));
default: default:
return $this->settings[$key]['value']; return $this->settings[$key]['value'];

View File

@@ -23,7 +23,7 @@ class RippingCluster_Database {
try { try {
$this->dbh = new PDO("mysql:host={$this->hostname};dbname={$this->dbname}", $this->username, $this->password); $this->dbh = new PDO("mysql:host={$this->hostname};dbname={$this->dbname}", $this->username, $this->password);
} catch (PDOException $e) { } catch (PDOException $e) {
throw new RippingCluster_Exception_DatabaseConnectionFailed($e->getMessage()); throw new RippingCluster_Exception_DatabaseConnectFailed($e->getMessage());
} }
} }

View File

@@ -28,26 +28,50 @@ class RippingCluster_Rips_SourceAudioTrack {
return $name; return $name;
} }
public function setName($name) {
$this->name = $name;
}
public function format() { public function format() {
return $this->format; return $this->format;
} }
public function setFormat($format) {
$this->format = $format;
}
public function channels() { public function channels() {
return $this->channels; return $this->channels;
} }
public function setChannels($channels) {
$this->channels = $channels;
}
public function language() { public function language() {
return $this->language; return $this->language;
} }
public function setLanguage($language) {
$this->language = $language;
}
public function samplerate() { public function samplerate() {
return $this->samplerate; return $this->samplerate;
} }
public function setSampleRate($sample_rate) {
$this->samplerate = $sample_rate;
}
public function bitrate() { public function bitrate() {
return $this->bitrate; return $this->bitrate;
} }
public function setBitRate($bit_rate) {
$this->bitrate = $bit_rate;
}
}; };
?> ?>

View File

@@ -22,14 +22,26 @@ class RippingCluster_Rips_SourceSubtitleTrack {
return $this->name; return $this->name;
} }
public function setName($name) {
$this->name = $name;
}
public function language() { public function language() {
return $this->language; return $this->language;
} }
public function setLanguage($language) {
$this->language = $language;
}
public function format() { public function format() {
return $this->format; return $this->format;
} }
public function setFormat($format) {
$this->format = $format;
}
}; };
?> ?>

View File

@@ -54,22 +54,42 @@ class RippingCluster_Rips_SourceTitle {
return $this->width; return $this->width;
} }
public function setWidth($width) {
$this->width = $width;
}
public function height() { public function height() {
return $this->height; return $this->height;
} }
public function setHeight($height) {
$this->height = $height;
}
public function displayAspect() { public function displayAspect() {
return $this->display_aspect; return $this->display_aspect;
} }
public function setDisplayAspect($display_aspect) {
$this->display_aspect = $display_aspect;
}
public function pixelAspect() { public function pixelAspect() {
return $this->pixel_aspect; return $this->pixel_aspect;
} }
public function setPixelAspect($pixel_aspect) {
$this->pixel_aspect = $pixel_aspect;
}
public function framerate() { public function framerate() {
return $this->framerate; return $this->framerate;
} }
public function setFramerate($framerate) {
$this->framerate = $framerate;
}
public function setDisplayInfo($width, $height, $display_aspect, $pixel_aspect, $framerate) { public function setDisplayInfo($width, $height, $display_aspect, $pixel_aspect, $framerate) {
$this->width = $width; $this->width = $width;
$this->height = $height; $this->height = $height;

View File

@@ -18,7 +18,7 @@ class RippingCluster_Source {
$this->plugin = $plugin; $this->plugin = $plugin;
} }
public static function isCached($source_filename) { public static function isSourceCached($source_filename) {
$main = RippingCluster_Main::instance(); $main = RippingCluster_Main::instance();
$cache = $main->cache(); $cache = $main->cache();
$config = $main->config(); $config = $main->config();
@@ -26,6 +26,14 @@ class RippingCluster_Source {
return $cache->exists($source_filename, $config->get('rips.cache_ttl')); return $cache->exists($source_filename, $config->get('rips.cache_ttl'));
} }
public function isCached() {
$main = RippingCluster_Main::instance();
$cache = $main->cache();
$config = $main->config();
return $cache->exists($this->filename, $config->get('rips.cache_ttl'));
}
public function cache() { public function cache() {
if (!$this->exists) { if (!$this->exists) {
throw new RippingCluster_Exception_InvalidSourceDirectory(); throw new RippingCluster_Exception_InvalidSourceDirectory();

View File

@@ -19,13 +19,12 @@ class RippingCluster_Source_Plugin_Bluray extends RippingCluster_PluginBase impl
$config = RippingCluster_Main::instance()->config(); $config = RippingCluster_Main::instance()->config();
$directories = $config->get('source.bluray.dir'); $directories = $config->get('source.bluray.dir');
$sources = array();
foreach ($directories as $directory) { foreach ($directories as $directory) {
if (!is_dir($directory)) { if (!is_dir($directory)) {
throw new RippingCluster_Exception_InvalidSourceDirectory($directory); throw new RippingCluster_Exception_InvalidSourceDirectory($directory);
} }
$sources = array();
$iterator = new RippingCluster_Utility_BlurayDirectoryIterator(new RippingCluster_Utility_VisibleFilesIterator(new DirectoryIterator($directory))); $iterator = new RippingCluster_Utility_BlurayDirectoryIterator(new RippingCluster_Utility_VisibleFilesIterator(new DirectoryIterator($directory)));
foreach ($iterator as /** @var SplFileInfo */ $source_vts) { foreach ($iterator as /** @var SplFileInfo */ $source_vts) {
$sources[] = self::load($source_vts->getPathname(), false); $sources[] = self::load($source_vts->getPathname(), false);

View File

@@ -25,13 +25,12 @@ class RippingCluster_Source_Plugin_HandBrake extends RippingCluster_PluginBase i
$config = RippingCluster_Main::instance()->config(); $config = RippingCluster_Main::instance()->config();
$directories = $config->get('source.handbrake.dir'); $directories = $config->get('source.handbrake.dir');
$sources = array();
foreach ($directories as $directory) { foreach ($directories as $directory) {
if (!is_dir($directory)) { if (!is_dir($directory)) {
throw new RippingCluster_Exception_InvalidSourceDirectory($directory); throw new RippingCluster_Exception_InvalidSourceDirectory($directory);
} }
$sources = array();
$iterator = new RippingCluster_Utility_DvdDirectoryIterator(new RippingCluster_Utility_VisibleFilesIterator(new DirectoryIterator($directory))); $iterator = new RippingCluster_Utility_DvdDirectoryIterator(new RippingCluster_Utility_VisibleFilesIterator(new DirectoryIterator($directory)));
foreach ($iterator as /** @var SplFileInfo */ $source_vts) { foreach ($iterator as /** @var SplFileInfo */ $source_vts) {
$sources[] = self::load($source_vts->getPathname(), false); $sources[] = self::load($source_vts->getPathname(), false);
@@ -210,12 +209,12 @@ class RippingCluster_Source_Plugin_HandBrake extends RippingCluster_PluginBase i
foreach ($source_directories as $source_basedir) { foreach ($source_directories as $source_basedir) {
$real_source_basedir = realpath($source_basedir); $real_source_basedir = realpath($source_basedir);
if (substr($real_source_filename, 0, strlen($real_source_basedir)) != $real_source_basedir) { if (substr($real_source_filename, 0, strlen($real_source_basedir)) == $real_source_basedir) {
return false; return true;
} }
} }
return true; return false;
} }
} }

View File

@@ -0,0 +1,249 @@
<?php
class RippingCluster_Source_Plugin_MkvInfo extends RippingCluster_PluginBase implements RippingCluster_Source_IPlugin {
/**
* Name of this plugin
* @var string
*/
const PLUGIN_NAME = 'MkvInfo';
/**
* Name of the config setting that stores the list of source directories for this pluing
* @var string
*/
const CONFIG_SOURCE_DIR = 'source.mkvinfo.dir';
const PM_HEADERS = 0;
const PM_TRACK = 1;
const PM_TITLE = 2;
const PM_CHAPTER = 3;
const PM_AUDIO = 4;
const PM_SUBTITLE = 5;
/**
* Returns a list of all Sources discovered by this plugin.
*
* The sources are not scanned until specifically requested.
*
* @return array(RippingCluster_Source)
*/
public static function enumerate() {
$config = RippingCluster_Main::instance()->config();
$directories = $config->get(self::CONFIG_SOURCE_DIR);
$sources = array();
foreach ($directories as $directory) {
if (!is_dir($directory)) {
throw new RippingCluster_Exception_InvalidSourceDirectory($directory);
}
$iterator = new RippingCluster_Utility_MkvFileIterator(new RecursiveIteratorIterator(new RippingCluster_Utility_VisibleFilesRecursiveIterator(new RecursiveDirectoryIterator($directory))));
foreach ($iterator as /** @var SplFileInfo */ $source_mkv) {
$sources[] = self::load($source_mkv->getPathname(), false);
}
}
return $sources;
}
/**
* Creates an object to represent the given source.
*
* The source is not actually scanned unless specifically requested.
* An unscanned object cannot be used until it has been manually scanned.
*
* If requested, the source can be cached to prevent high load, and long scan times.
*
* @param string $source_filename Filename of the source
* @param bool $scan Request that the source be scanned for content. Defaults to true.
* @param bool $use_cache Request that the cache be used. Defaults to true.
* @return RippingCluster_Source
*/
public static function load($source_filename, $scan = true, $use_cache = true) {
$cache = RippingCluster_Main::instance()->cache();
$config = RippingCluster_Main::instance()->config();
// Ensure the source is a valid directory, and lies below the configured source_dir
if ( ! self::isValidSource($source_filename)) {
return new RippingCluster_Source($source_filename, self::name(), false);
}
$source = null;
if ($use_cache && $cache->exists($source_filename)) {
$source = unserialize($cache->fetch($source_filename));
} else {
$source = new RippingCluster_Source($source_filename, self::name(), true);
if ($scan) {
$cmd = escapeshellcmd($config->get('source.mkvinfo.bin')) . ' ' . escapeshellarg($source_filename);
list($retval, $output, $error) = RippingCluster_ForegroundTask::execute($cmd);
// Process the output
$lines = explode("\n", $output);
$track = null;
$track_details = null;
$duration = null;
$mode = self::PM_HEADERS;
foreach ($lines as $line) {
// Skip any line that doesn't begin with a |+ (with optional whitespace)
if ( ! preg_match('/^|\s*\+/', $line)) {
continue;
}
$matches = array();
switch (true) {
case $mode == self::PM_HEADERS && preg_match('/^| \+ Duration: [\d\.]+s ([\d:]+])$/', $line, $matches): {
$duration = $matches['duration'];
} break;
case preg_match('/^| \+ A track$/', $line, $matches): {
$mode = self::PM_TRACK;
$track_details = array();
} break;
case $mode == self::PM_TRACK && preg_match('/^| \+ Track number: (?P<id>\d+):$/', $line, $matches): {
$track_details['id'] = $matches['id'];
} break;
case $mode == self::PM_TRACK && preg_match('/^| \+ Track type: (?P<type>.+)$/', $line, $matches): {
switch ($type) {
case 'video': {
$mode = self::PM_TITLE;
$track = new RippingCluster_Rips_SourceTitle($track_details['id']);
$track->setDuration($duration);
} break;
case 'audio': {
$mode = self::PM_AUDIO;
$track = new RippingCluster_Rips_SourceAudioTrack($track_details['id']);
} break;
case 'subtitles': {
$mode = self::PM_SUBTITLE;
$track = new RippingCluster_Rips_SourceSubtitleTrack($track_details['id']);
} break;
}
} break;
case $mode == self::PM_AUDIO && $track && preg_match('/^| \+ Codec ID: (?P<codec>.+)$/', $line, $matches): {
$track->setFormat($matches['codec']);
} break;
case $mode == self::PM_AUDIO && $track && preg_match('/^| \+ Language: (?P<language>.+)$/', $line, $matches): {
$track->setLanguage($matches['language']);
} break;
case $mode == self::PM_AUDIO && $track && preg_match('/^| \+ Sampling frequency: (?P<samplerate>.+)$/', $line, $matches): {
$track->setSampleRate($matches['samplerate']);
} break;
case $mode == self::PM_AUDIO && $track && preg_match('/^| \+ Channels: (?P<channels>.+)$/', $line, $matches): {
$track->setFormat($matches['channels']);
} break;
case $mode == self::PM_SUBTITLE && $track && preg_match('/^| \+ Language: (?P<language>.*)$/', $line): {
$track->setLanguage($matches['language']);
} break;
case $mode == self::PM_TITLE && $track && preg_match('/^ \+ Default duration: [\d\.]+ \((?P<framerate>[\d\.]+ fps for a video track)\)$/', $line, $matches): {
$title->setFramerate($matches['framerate']);
} break;
case $mode == self::PM_TITLE && $track && preg_match('/^ \+ Pixel width: (?P<width>\d+)$/', $line, $matches): {
$title->setWidth($matches['width']);
} break;
case $mode == self::PM_TITLE && $track && preg_match('/^ \+ Pixel height: (?P<height>\d+)$/', $line, $matches): {
$title->setHeight($matches['height']);
} 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;
}
}
}
// If requested, store the new source object in the cache
if ($use_cache) {
$source->cache();
}
}
}
/**
* Creates an object to represent the given source using an encoded filename.
*
* Wraps the call to load the source after the filename has been decoded.
*
* @param string $encoded_filename Encoded filename of the source
* @param bool $scan Request that the source be scanned for content. Defaults to true.
* @param bool $use_cache Request that the cache be used. Defaults to true.
* @return RippingCluster_Source
*
* @see RippingCluster_Source_IPlugin::load()
*/
public static function loadEncoded($encoded_filename, $scan = true, $use_cache = true) {
// Decode the filename
$source_filename = base64_decode(str_replace('-', '/', $encoded_filename));
return self::load($source_filename, $scan, $use_cache);
}
/**
* Determins if a filename is a valid source loadable using this plugin
*
* @param string $source_filename Filename of the source
* @return bool
*/
public static function isValidSource($source_filename) {
$config = RippingCluster_Main::instance()->config();
// Ensure the source is a valid directory, and lies below the configured source_dir
if ( ! is_dir($source_filename)) {
return false;
}
$real_source_filename = realpath($source_filename);
// Check all of the source directories specified in the config
$source_directories = $config->get(self::CONFIG_SOURCE_DIR);
foreach ($source_directories as $source_basedir) {
$real_source_basedir = realpath($source_basedir);
if (substr($real_source_filename, 0, strlen($real_source_basedir)) != $real_source_basedir) {
return false;
}
}
return true;
}
}
?>

View File

@@ -31,7 +31,7 @@ class RippingCluster_Source_PluginFactory extends RippingCluster_PluginFactory {
$sources = array(); $sources = array();
foreach (self::getValidPlugins() as $plugin) { foreach (self::getValidPlugins() as $plugin) {
$sources = array_merge($sources, self::enumerate($plugin)); $sources[$plugin] = self::enumerate($plugin);
} }
return $sources; return $sources;

View File

@@ -0,0 +1,9 @@
<?php
class RippingCluster_Utility_MkvFileIterator extends FilterIterator {
public function accept() {
return preg_match('/\.mkv$/i', $this->current()->getFilename());
}
}
?>

View File

@@ -0,0 +1,9 @@
<?php
class RippingCluster_Utility_VisibleFilesRecursiveIterator extends RecursiveFilterIterator {
public function accept() {
return !(substr($this->current()->getFilename(), 0, 1) == '.');
}
}
?>

View File

@@ -0,0 +1,85 @@
<?php
class RippingCluster_Worker_FfmpegTranscode extends RippingCluster_PluginBase implements RippingCluster_Worker_IPlugin {
/**
* Name of this plugin
* @var string
*/
const PLUGIN_NAME = 'FfmpegTranscode';
/**
* Output produced by the worker process
* @var string
*/
private $output;
/**
* Gearman Job object describing the task distributed to this worker
* @var GearmanJob
*/
private $gearman_job;
/**
* Ripping Job that is being processed by this Worker
* @var RippingCluster_Job
*/
private $job;
/**
* Associative array of options describing the rip to be carried out
* @var array(string=>string)
*/
private $rip_options;
/**
* Constructs a new instance of this Worker class
*
* @param GearmanJob $gearman_job GearmanJob object describing the task distributed to this worker
* @throws RippingCluster_Exception_LogicException
*/
private function __construct(GearmanJob $gearman_job) {
$this->output = '';
$this->gearman_job = $gearman_job;
$this->rip_options = unserialize($this->gearman_job->workload());
if ( ! $this->rip_options['id']) {
throw new RippingCluster_Exception_LogicException("Job ID must not be zero/null");
}
$this->job = RippingCluster_Job::fromId($this->rip_options['id']);
}
/**
* Returns the list of functions (and names) implemented by this plugin for registration with Gearman
*
* @return array(string => callback)
*/
public static function workerFunctions() {
return array(
'bluray_rip' => array(__CLASS__, 'rip'),
);
}
/**
* Creates an instance of the Worker plugin, and uses it to execute a single job
*
* @param GearmanJob $job Gearman Job object, describing the work to be done
*/
public static function rip(GearmanJob $job) {
$rip = new self($job);
$rip->execute();
}
/**
* Executes the process for ripping the source to the final output
*
*/
private function execute() {
// TODO
}
}
?>

View File

@@ -3,14 +3,7 @@
$main = RippingCluster_Main::instance(); $main = RippingCluster_Main::instance();
$config = $main->config(); $config = $main->config();
$sources = RippingCluster_Source_PluginFactory::enumerateAll(); $all_sources = RippingCluster_Source_PluginFactory::enumerateAll();
$this->smarty->assign('all_sources', $all_sources);
$sources_cached = array();
foreach ($sources as $source) {
$sources_cached[$source->filename()] = RippingCluster_Source::isCached($source->filename());
}
$this->smarty->assign('sources', $sources);
$this->smarty->assign('sources_cached', $sources_cached);
?> ?>

View File

@@ -14,7 +14,6 @@ try {
$gearman->addServers($config->get('rips.job_servers')); $gearman->addServers($config->get('rips.job_servers'));
$gearman->setCreatedCallback("gearman_created_callback"); $gearman->setCreatedCallback("gearman_created_callback");
$gearman->setDataCallback("gearman_data_callback"); $gearman->setDataCallback("gearman_data_callback");
$gearman->setStatusCallback("gearman_status_callback");
$gearman->setCompleteCallback("gearman_complete_callback"); $gearman->setCompleteCallback("gearman_complete_callback");
$gearman->setFailCallback("gearman_fail_callback"); $gearman->setFailCallback("gearman_fail_callback");

View File

@@ -1,6 +1,6 @@
<h2>Sources</h2> <h2>Sources</h2>
{if $sources} {if $all_sources}
<p> <p>
The list below contains all the DVD sources that are available and ready for ripping. The list below contains all the DVD sources that are available and ready for ripping.
</p> </p>
@@ -8,20 +8,33 @@
Sources that have recently been scanned are marked <em>(cached)</em> and will load fairly quickly. Sources that have recently been scanned are marked <em>(cached)</em> and will load fairly quickly.
Sources that have not been cached will be scanned when the link is clicked, and this may take several minutes so please be patient. Sources that have not been cached will be scanned when the link is clicked, and this may take several minutes so please be patient.
</p> </p>
<ul>
{foreach from=$all_sources key=type item=sources}
<li>{$type}
{if $sources}
<ul> <ul>
{foreach from=$sources item=source} {foreach from=$sources item=source}
{assign var='source_plugin' value=$source->plugin()} {assign var='source_plugin' value=$source->plugin()}
{assign var='source_filename' value=$source->filename()} {assign var='source_filename' value=$source->filename()}
{assign var='source_filename_encoded' value=$source->filenameEncoded()} {assign var='source_filename_encoded' value=$source->filenameEncoded()}
{assign var='source_cached' value="$source->isCached()}
<li> <li>
[ <a href="{$base_uri}rips/source-details/plugin/{$source_plugin}/id/{$source_filename_encoded}" title="Browse source details">Browse</a> | [ <a href="{$base_uri}rips/source-details/plugin/{$source_plugin}/id/{$source_filename_encoded}" title="Browse source details">Browse</a> |
<a href="{$base_uri}rips/setup-rip/plugin/{$source_plugin}/id/{$source_filename_encoded}" title="Rip this source">Rip</a> ] <a href="{$base_uri}rips/setup-rip/plugin/{$source_plugin}/id/{$source_filename_encoded}" title="Rip this source">Rip</a> ]
{$source_filename|escape:'html'}{if $sources_cached.$source_filename} (cached){/if} {$source_filename|escape:'html'}{if $source_cached} (cached){/if}
</li> </li>
{/foreach} {/foreach}
</ul> </ul>
{else} {else}
<p> <p>
<em>There are currently no DVD sources available to rip.</em> <em>There are no {$type} sources available to rip.</em>
</p>
{/if}
</li>
{/foreach}
</ul>
{else}
<p>
<em>There are currently no sources available to rip.</em>
</p> </p>
{/if} {/if}