Merge branch 'feature-mkv-plugins'; commit 'HEAD^'

This commit is contained in:
2010-10-12 19:30:27 +01:00
15 changed files with 451 additions and 32 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

@@ -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);
@@ -207,15 +206,15 @@ class RippingCluster_Source_Plugin_HandBrake extends RippingCluster_PluginBase i
// Check all of the source directories specified in the config // Check all of the source directories specified in the config
$source_directories = $config->get('source.handbrake.dir'); $source_directories = $config->get('source.handbrake.dir');
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>
@@ -9,19 +9,32 @@
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> <ul>
{foreach from=$sources item=source} {foreach from=$all_sources key=type item=sources}
{assign var='source_plugin' value=$source->plugin()} <li>{$type}
{assign var='source_filename' value=$source->filename()} {if $sources}
{assign var='source_filename_encoded' value=$source->filenameEncoded()} <ul>
<li> {foreach from=$sources item=source}
[ <a href="{$base_uri}rips/source-details/plugin/{$source_plugin}/id/{$source_filename_encoded}" title="Browse source details">Browse</a> | {assign var='source_plugin' value=$source->plugin()}
<a href="{$base_uri}rips/setup-rip/plugin/{$source_plugin}/id/{$source_filename_encoded}" title="Rip this source">Rip</a> ] {assign var='source_filename' value=$source->filename()}
{$source_filename|escape:'html'}{if $sources_cached.$source_filename} (cached){/if} {assign var='source_filename_encoded' value=$source->filenameEncoded()}
{assign var='source_cached' value="$source->isCached()}
<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/setup-rip/plugin/{$source_plugin}/id/{$source_filename_encoded}" title="Rip this source">Rip</a> ]
{$source_filename|escape:'html'}{if $source_cached} (cached){/if}
</li>
{/foreach}
</ul>
{else}
<p>
<em>There are no {$type} sources available to rip.</em>
</p>
{/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 currently no sources available to rip.</em>
</p> </p>
{/if} {/if}