diff --git a/lib/RippingCluster/Config.class.php b/lib/RippingCluster/Config.class.php index 88d6064..66785a5 100644 --- a/lib/RippingCluster/Config.class.php +++ b/lib/RippingCluster/Config.class.php @@ -132,8 +132,8 @@ class RippingCluster_Config { } switch ($this->settings[$key]['type']) { - case TYPE_STRING_LIST: - return explode("\n", $this->settings[$key]['value']); + case self::TYPE_STRING_LIST: + return array_map('trim', explode("\n", $this->settings[$key]['value'])); default: return $this->settings[$key]['value']; diff --git a/lib/RippingCluster/Rips/SourceAudioTrack.class.php b/lib/RippingCluster/Rips/SourceAudioTrack.class.php index e753f01..6b7e81e 100644 --- a/lib/RippingCluster/Rips/SourceAudioTrack.class.php +++ b/lib/RippingCluster/Rips/SourceAudioTrack.class.php @@ -28,26 +28,50 @@ class RippingCluster_Rips_SourceAudioTrack { return $name; } + public function setName($name) { + $this->name = $name; + } + public function format() { return $this->format; } + public function setFormat($format) { + $this->format = $format; + } + public function channels() { return $this->channels; } + public function setChannels($channels) { + $this->channels = $channels; + } + public function language() { return $this->language; } + public function setLanguage($language) { + $this->language = $language; + } + public function samplerate() { return $this->samplerate; } + public function setSampleRate($sample_rate) { + $this->samplerate = $sample_rate; + } + public function bitrate() { return $this->bitrate; } + public function setBitRate($bit_rate) { + $this->bitrate = $bit_rate; + } + }; ?> \ No newline at end of file diff --git a/lib/RippingCluster/Rips/SourceSubtitleTrack.class.php b/lib/RippingCluster/Rips/SourceSubtitleTrack.class.php index 9b198b6..812099c 100644 --- a/lib/RippingCluster/Rips/SourceSubtitleTrack.class.php +++ b/lib/RippingCluster/Rips/SourceSubtitleTrack.class.php @@ -22,14 +22,26 @@ class RippingCluster_Rips_SourceSubtitleTrack { return $this->name; } + public function setName($name) { + $this->name = $name; + } + public function language() { return $this->language; } + public function setLanguage($language) { + $this->language = $language; + } + public function format() { return $this->format; } + public function setFormat($format) { + $this->format = $format; + } + }; ?> \ No newline at end of file diff --git a/lib/RippingCluster/Rips/SourceTitle.class.php b/lib/RippingCluster/Rips/SourceTitle.class.php index 1afa200..385380a 100644 --- a/lib/RippingCluster/Rips/SourceTitle.class.php +++ b/lib/RippingCluster/Rips/SourceTitle.class.php @@ -54,22 +54,42 @@ class RippingCluster_Rips_SourceTitle { return $this->width; } + public function setWidth($width) { + $this->width = $width; + } + public function height() { return $this->height; } + public function setHeight($height) { + $this->height = $height; + } + public function displayAspect() { return $this->display_aspect; } + public function setDisplayAspect($display_aspect) { + $this->display_aspect = $display_aspect; + } + public function pixelAspect() { return $this->pixel_aspect; } + public function setPixelAspect($pixel_aspect) { + $this->pixel_aspect = $pixel_aspect; + } + public function framerate() { return $this->framerate; } + public function setFramerate($framerate) { + $this->framerate = $framerate; + } + public function setDisplayInfo($width, $height, $display_aspect, $pixel_aspect, $framerate) { $this->width = $width; $this->height = $height; diff --git a/lib/RippingCluster/Source.class.php b/lib/RippingCluster/Source.class.php index e602767..c3e907c 100644 --- a/lib/RippingCluster/Source.class.php +++ b/lib/RippingCluster/Source.class.php @@ -18,7 +18,7 @@ class RippingCluster_Source { $this->plugin = $plugin; } - public static function isCached($source_filename) { + public static function isSourceCached($source_filename) { $main = RippingCluster_Main::instance(); $cache = $main->cache(); $config = $main->config(); @@ -26,6 +26,14 @@ class RippingCluster_Source { 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() { if (!$this->exists) { throw new RippingCluster_Exception_InvalidSourceDirectory(); diff --git a/lib/RippingCluster/Source/Plugin/Bluray.class.php b/lib/RippingCluster/Source/Plugin/Bluray.class.php index b3a8c80..96c6082 100644 --- a/lib/RippingCluster/Source/Plugin/Bluray.class.php +++ b/lib/RippingCluster/Source/Plugin/Bluray.class.php @@ -19,13 +19,12 @@ class RippingCluster_Source_Plugin_Bluray extends RippingCluster_PluginBase impl $config = RippingCluster_Main::instance()->config(); $directories = $config->get('source.bluray.dir'); + $sources = array(); foreach ($directories as $directory) { if (!is_dir($directory)) { throw new RippingCluster_Exception_InvalidSourceDirectory($directory); } - $sources = array(); - $iterator = new RippingCluster_Utility_BlurayDirectoryIterator(new RippingCluster_Utility_VisibleFilesIterator(new DirectoryIterator($directory))); foreach ($iterator as /** @var SplFileInfo */ $source_vts) { $sources[] = self::load($source_vts->getPathname(), false); diff --git a/lib/RippingCluster/Source/Plugin/HandBrake.class.php b/lib/RippingCluster/Source/Plugin/HandBrake.class.php index d961023..aec5f88 100644 --- a/lib/RippingCluster/Source/Plugin/HandBrake.class.php +++ b/lib/RippingCluster/Source/Plugin/HandBrake.class.php @@ -25,13 +25,12 @@ class RippingCluster_Source_Plugin_HandBrake extends RippingCluster_PluginBase i $config = RippingCluster_Main::instance()->config(); $directories = $config->get('source.handbrake.dir'); + $sources = array(); foreach ($directories as $directory) { if (!is_dir($directory)) { throw new RippingCluster_Exception_InvalidSourceDirectory($directory); } - $sources = array(); - $iterator = new RippingCluster_Utility_DvdDirectoryIterator(new RippingCluster_Utility_VisibleFilesIterator(new DirectoryIterator($directory))); foreach ($iterator as /** @var SplFileInfo */ $source_vts) { $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 $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); - if (substr($real_source_filename, 0, strlen($real_source_basedir)) != $real_source_basedir) { - return false; + if (substr($real_source_filename, 0, strlen($real_source_basedir)) == $real_source_basedir) { + return true; } } - return true; + return false; } } diff --git a/lib/RippingCluster/Source/Plugin/MkvInfo.class.php b/lib/RippingCluster/Source/Plugin/MkvInfo.class.php new file mode 100644 index 0000000..f039532 --- /dev/null +++ b/lib/RippingCluster/Source/Plugin/MkvInfo.class.php @@ -0,0 +1,249 @@ +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\d+):$/', $line, $matches): { + $track_details['id'] = $matches['id']; + } break; + + case $mode == self::PM_TRACK && preg_match('/^| \+ Track type: (?P.+)$/', $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.+)$/', $line, $matches): { + $track->setFormat($matches['codec']); + } break; + + case $mode == self::PM_AUDIO && $track && preg_match('/^| \+ Language: (?P.+)$/', $line, $matches): { + $track->setLanguage($matches['language']); + } break; + + case $mode == self::PM_AUDIO && $track && preg_match('/^| \+ Sampling frequency: (?P.+)$/', $line, $matches): { + $track->setSampleRate($matches['samplerate']); + } break; + + case $mode == self::PM_AUDIO && $track && preg_match('/^| \+ Channels: (?P.+)$/', $line, $matches): { + $track->setFormat($matches['channels']); + } break; + + case $mode == self::PM_SUBTITLE && $track && preg_match('/^| \+ Language: (?P.*)$/', $line): { + $track->setLanguage($matches['language']); + } break; + + case $mode == self::PM_TITLE && $track && preg_match('/^ \+ Default duration: [\d\.]+ \((?P[\d\.]+ fps for a video track)\)$/', $line, $matches): { + $title->setFramerate($matches['framerate']); + } break; + + case $mode == self::PM_TITLE && $track && preg_match('/^ \+ Pixel width: (?P\d+)$/', $line, $matches): { + $title->setWidth($matches['width']); + } break; + + case $mode == self::PM_TITLE && $track && preg_match('/^ \+ Pixel height: (?P\d+)$/', $line, $matches): { + $title->setHeight($matches['height']); + } break; + + case $title && $mode == self::PM_CHAPTER && preg_match('/^ \+ (?P\d+): cells \d+->\d+, \d+ blocks, duration (?P\d+:\d+:\d+)$/', $line, $matches): { + $title->addChapter($matches['id'], $matches['duration']); + } break; + + case $title && $mode == self::PM_AUDIO && preg_match('/^ \+ (?P\d+), (?P.+) \((?P.+)\) \((?P(.+ ch|Dolby Surround))\) \((?P.+)\), (?P\d+)Hz, (?P\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\d+), (?P.+) \((?P.+)\) \((?P.+)\)$/', $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; + } + +} + +?> \ No newline at end of file diff --git a/lib/RippingCluster/Source/PluginFactory.class.php b/lib/RippingCluster/Source/PluginFactory.class.php index 89d6618..ec25bdc 100644 --- a/lib/RippingCluster/Source/PluginFactory.class.php +++ b/lib/RippingCluster/Source/PluginFactory.class.php @@ -31,7 +31,7 @@ class RippingCluster_Source_PluginFactory extends RippingCluster_PluginFactory { $sources = array(); foreach (self::getValidPlugins() as $plugin) { - $sources = array_merge($sources, self::enumerate($plugin)); + $sources[$plugin] = self::enumerate($plugin); } return $sources; diff --git a/lib/RippingCluster/Utility/MkvFileIterator.class.php b/lib/RippingCluster/Utility/MkvFileIterator.class.php new file mode 100644 index 0000000..37bd51d --- /dev/null +++ b/lib/RippingCluster/Utility/MkvFileIterator.class.php @@ -0,0 +1,9 @@ +current()->getFilename()); + } +} + +?> \ No newline at end of file diff --git a/lib/RippingCluster/Utility/VisibleFilesRecursiveIterator.class.php b/lib/RippingCluster/Utility/VisibleFilesRecursiveIterator.class.php new file mode 100644 index 0000000..151d938 --- /dev/null +++ b/lib/RippingCluster/Utility/VisibleFilesRecursiveIterator.class.php @@ -0,0 +1,9 @@ +current()->getFilename(), 0, 1) == '.'); + } +} + +?> \ No newline at end of file diff --git a/lib/RippingCluster/Worker/Plugin/FfmpegTranscode.class.php b/lib/RippingCluster/Worker/Plugin/FfmpegTranscode.class.php new file mode 100644 index 0000000..8d63063 --- /dev/null +++ b/lib/RippingCluster/Worker/Plugin/FfmpegTranscode.class.php @@ -0,0 +1,85 @@ +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 + } + +} + +?> \ No newline at end of file diff --git a/webui/pages/rips/sources.php b/webui/pages/rips/sources.php index 3aadca0..b9fc539 100644 --- a/webui/pages/rips/sources.php +++ b/webui/pages/rips/sources.php @@ -3,14 +3,7 @@ $main = RippingCluster_Main::instance(); $config = $main->config(); -$sources = RippingCluster_Source_PluginFactory::enumerateAll(); - -$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); +$all_sources = RippingCluster_Source_PluginFactory::enumerateAll(); +$this->smarty->assign('all_sources', $all_sources); ?> \ No newline at end of file diff --git a/webui/run-jobs.php b/webui/run-jobs.php index d553603..afd64ee 100644 --- a/webui/run-jobs.php +++ b/webui/run-jobs.php @@ -14,7 +14,6 @@ try { $gearman->addServers($config->get('rips.job_servers')); $gearman->setCreatedCallback("gearman_created_callback"); $gearman->setDataCallback("gearman_data_callback"); - $gearman->setStatusCallback("gearman_status_callback"); $gearman->setCompleteCallback("gearman_complete_callback"); $gearman->setFailCallback("gearman_fail_callback"); diff --git a/webui/templates/rips/sources.tpl b/webui/templates/rips/sources.tpl index 46951af..c0cbb1a 100644 --- a/webui/templates/rips/sources.tpl +++ b/webui/templates/rips/sources.tpl @@ -1,6 +1,6 @@

Sources

-{if $sources} +{if $all_sources}

The list below contains all the DVD sources that are available and ready for ripping.

@@ -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.

    - {foreach from=$sources item=source} - {assign var='source_plugin' value=$source->plugin()} - {assign var='source_filename' value=$source->filename()} - {assign var='source_filename_encoded' value=$source->filenameEncoded()} -
  • - [ Browse | - Rip ] - {$source_filename|escape:'html'}{if $sources_cached.$source_filename} (cached){/if} + {foreach from=$all_sources key=type item=sources} +
  • {$type} + {if $sources} +
      + {foreach from=$sources item=source} + {assign var='source_plugin' value=$source->plugin()} + {assign var='source_filename' value=$source->filename()} + {assign var='source_filename_encoded' value=$source->filenameEncoded()} + {assign var='source_cached' value="$source->isCached()} +
    • + [ Browse | + Rip ] + {$source_filename|escape:'html'}{if $source_cached} (cached){/if} +
    • + {/foreach} +
    + {else} +

    + There are no {$type} sources available to rip. +

    + {/if}
  • {/foreach}
{else}

- There are currently no DVD sources available to rip. + There are currently no sources available to rip.

{/if}