diff --git a/HandBrakeCluster/Exceptions.class.php b/HandBrakeCluster/Exceptions.class.php index 09fc7fc..3841070 100644 --- a/HandBrakeCluster/Exceptions.class.php +++ b/HandBrakeCluster/Exceptions.class.php @@ -24,4 +24,7 @@ class HandBrakeCluster_Exception_CacheException extends HandBrakeCluster class HandBrakeCluster_Exception_InvalidCacheDir extends HandBrakeCluster_Exception_CacheException {}; class HandBrakeCluster_Exception_CacheObjectNotFound extends HandBrakeCluster_Exception_CacheException {}; -?> \ No newline at end of file +class HandBrakeCluster_Exception_LogicException extends HandBrakeCluster_Exception {}; +class HandBrakeCluster_Exception_JobNotRunning extends HandBrakeCluster_Exception_LogicException {}; + +?> diff --git a/HandBrakeCluster/Job.class.php b/HandBrakeCluster/Job.class.php index 5aca165..940bf0f 100644 --- a/HandBrakeCluster/Job.class.php +++ b/HandBrakeCluster/Job.class.php @@ -44,6 +44,12 @@ class HandBrakeCluster_Job { $this->subtitle_tracks = $subtitle_tracks; } + public function __clone() { + $this->id = null; + + $this->create(); + } + public static function fromDatabaseRow($row) { return new HandBrakeCluster_Job( HandBrakeCluster_Rips_Source::load($rips['source']), @@ -124,12 +130,14 @@ class HandBrakeCluster_Job { $jobs = array(); foreach ($titles as $title => $details) { if (HandBrakeCluster_Main::issetelse($details['queue'])) { + HandBrakeCluster_Main::issetelse($details['output_filename'], HandBrakeCluster_Exception_InvalidParameters); + $job = new HandBrakeCluster_Job( $source, null, HandBrakeCluster_Main::issetelse($details['name'], 'unnamed job'), $source->filename(), - HandBrakeCluster_Main::issetelse($details['output_filename'], HandBrakeCluster_Exception_InvalidParameters), + $global_options['output-directory'] . DIRECTORY_SEPARATOR . $details['output_filename'], $title, $global_options['format'], $global_options['video-codec'], @@ -179,6 +187,18 @@ class HandBrakeCluster_Job { $status = HandBrakeCluster_JobStatus::updateStatusForJob($this, HandBrakeCluster_JobStatus::CREATED); } + public function delete() { + $database = HandBrakeCluster_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 = HandBrakeCluster_Main::instance(); $config = $main->config(); @@ -188,8 +208,8 @@ class HandBrakeCluster_Job { // Construct the rip options $rip_options = array( 'nice' => $config->get('rips.nice', 15), - 'input_filename' => dirname($this->source_filename) . DIRECTORY_SEPARATOR . basename($this->source_filename), - 'output_filename' => dirname($this->destination_filename) . DIRECTORY_SEPARATOR . basename($this->destination_filename), + 'input_filename' => $this->source_filename, + 'output_filename' => $this->destination_filename, 'title' => $this->title, 'format' => $this->format, 'video_codec' => $this->video_codec, @@ -233,6 +253,22 @@ class HandBrakeCluster_Job { return HandBrakeCluster_JobStatus::updateStatusForJob($this, $new_status, $rip_progress); } + public function calculateETA() { + $current_status = $this->currentStatus(); + if ($current_status->status() != HandBrakeCluster_JobStatus::RUNNING) { + throw new HandBrakeCluster_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; } diff --git a/HandBrakeCluster/Main.class.php b/HandBrakeCluster/Main.class.php index 32f4101..b68e018 100644 --- a/HandBrakeCluster/Main.class.php +++ b/HandBrakeCluster/Main.class.php @@ -32,6 +32,8 @@ class HandBrakeCluster_Main { $this->smarty->cache_dir = './tmp/cache'; $this->smarty->config_fir = './config'; + $this->smarty->register_modifier('formatDuration', array('HandBrakeCluster_Main', 'formatDuration')); + $this->smarty->assign('version', '0.1'); $this->base_uri = dirname($_SERVER['SCRIPT_NAME']) . '/'; @@ -160,12 +162,39 @@ class HandBrakeCluster_Main { return $var; } - if (preg_match('/^HandBrakeCluster_Exception/', $default) && class_exists($default) && is_subclass_of($default, HandBrakeCluster_Exception)) { + if (is_string($default) && preg_match('/^HandBrakeCluster_Exception/', $default) && class_exists($default) && is_subclass_of($default, HandBrakeCluster_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; + } } diff --git a/HandBrakeCluster/RequestParser.class.php b/HandBrakeCluster/RequestParser.class.php index 7ccb2c7..802b585 100644 --- a/HandBrakeCluster/RequestParser.class.php +++ b/HandBrakeCluster/RequestParser.class.php @@ -72,12 +72,16 @@ class HandBrakeCluster_RequestParser { 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_subclass_of($default, HandBrakeCluster_Exception)) { + if (is_string($default) && preg_match('/^HandBrakeCluster_Exception/', $default) && class_exists($default) && is_subclass_of($default, HandBrakeCluster_Exception)) { throw new $default(); } diff --git a/HandBrakeCluster/Rips/Source.class.php b/HandBrakeCluster/Rips/Source.class.php index 024ae59..a39f6dd 100644 --- a/HandBrakeCluster/Rips/Source.class.php +++ b/HandBrakeCluster/Rips/Source.class.php @@ -131,7 +131,7 @@ class HandBrakeCluster_Rips_Source { $title->addChapter($matches['id'], $matches['duration']); } break; - case $title && $mode == self::PM_AUDIO && preg_match('/^ \+ (?P\d+), (?P.+) \((?P.+)\) \((?P.+) ch\) \((?P.+)\), (?P\d+)Hz, (?P\d+)bps$/', $line, $matches): { + 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 HandBrakeCluster_Rips_SourceAudioTrack( $matches['id'], $matches['name'], $matches['format'], $matches['channels'], @@ -196,7 +196,27 @@ class HandBrakeCluster_Rips_Source { } 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; diff --git a/images/caution.png b/images/caution.png new file mode 100644 index 0000000..cb9d5ea Binary files /dev/null and b/images/caution.png differ diff --git a/images/redo.png b/images/redo.png new file mode 100644 index 0000000..882dafb Binary files /dev/null and b/images/redo.png differ diff --git a/images/trash.png b/images/trash.png new file mode 100644 index 0000000..531d42f Binary files /dev/null and b/images/trash.png differ diff --git a/pages/jobs.php b/pages/jobs.php index c0aab25..5de528b 100644 --- a/pages/jobs.php +++ b/pages/jobs.php @@ -1,6 +1,90 @@ smarty->assign('jobs', $jobs); +$main = HandBrakeCluster_Main::instance(); +$req = $main->request(); +$config = $main->config(); + +if ($req->get('submit')) { + $action = HandBrakeCluster_Main::issetelse($_POST['action'], HandBrakeCluster_Exception_InvalidParameters); + + # If a bulk action was selected, the action will be a single term, otherwise it will also contain + # the id of the single item to act upon. Work out which was used now. + $matches = $job_ids = array(); + if (preg_match('/^(.*)\[(\d+)\]$/', $action, $matches)) { + $action = $matches[1]; + $job_ids = array($matches[2]); + } + else { + $job_ids = $_POST['include']; + } + + $jobs = array(); + foreach ($job_ids as $job_id) { + $job = HandBrakeCluster_Job::fromId($job_id); + if (!$job) { + throw new HandBrakeCluster_Exception_InvalidParameters('job_id'); + } + $jobs[] = $job; + } + + switch ($action) { + case 'mark-failed': { + foreach ($jobs as $job) { + $job->updateStatus(HandBrakeCluster_JobStatus::FAILED); + } + } break; + + case 'retry': { + # Clone each of the selected jobs + foreach ($jobs as $job) { + $new_job = clone $job; + } + + # Dispatch all the jobs in one run + HandBrakeCluster_Job::runAllJobs(); + + # Redirect to the job queued page to show the jobs were successfully dispatched + HandBrakeCluster_Page::redirect('rips/setup-rip/queued'); + } break; + + case 'delete': { + foreach ($jobs as $job) { + $job->delete(); + } + } break; + + default: { + throw new HandBrakeCluster_Exception_InvalidParameters('action'); + } + } + + HandBrakeCluster_Page::redirect('jobs'); + +} else { + + if (isset($_POST['view'])) { + $statusName = urlencode($_POST['view']); + HandBrakeCluster_Page::redirect("jobs/view/{$statusName}"); + } + + $statusName = $req->get('view', 'any'); + switch ($statusName) { + case 'any': $status = null; break; + case 'queued': $status = HandBrakeCluster_JobStatus::QUEUED; break; + case 'running': $status = HandBrakeCluster_JobStatus::RUNNING; break; + case 'complete': $status = HandBrakeCluster_JobStatus::COMPLETE; break; + case 'failed': $status = HandBrakeCluster_JobStatus::FAILED; break; + default: throw new HandBrakeCluster_Exception_InvalidParameters('view'); + } + + $jobs = array(); + if ($status) { + $jobs = HandBrakeCluster_Job::allWithStatus($status); + } else { + $jobs = HandBrakeCluster_Job::all(); + } + + $this->smarty->assign('jobs', $jobs); +} ?> diff --git a/pages/rips/setup-rip.php b/pages/rips/setup-rip.php index 99511af..80b4da2 100644 --- a/pages/rips/setup-rip.php +++ b/pages/rips/setup-rip.php @@ -29,6 +29,7 @@ if ($req->get('submit')) { $this->smarty->assign('source', $source); $this->smarty->assign('titles', $source->titles()); $this->smarty->assign('longest_title', $source->longestTitle()); + $this->smarty->assign('default_output_directory', $config->get('rips.default.output_directory')); } ?> diff --git a/styles/normal.css b/styles/normal.css index 86d8927..89dafec 100644 --- a/styles/normal.css +++ b/styles/normal.css @@ -104,8 +104,8 @@ label { width: 16px; } -table#setup-rips input,select { - +form#setup-rips input[type="text"] { + width: 30em; } #quantizer-slider { diff --git a/templates/jobs.tpl b/templates/jobs.tpl index b312e36..7dd1eb2 100644 --- a/templates/jobs.tpl +++ b/templates/jobs.tpl @@ -1,6 +1,32 @@

Jobs

{if $jobs} + +
+
+ View + + + + + +
+
+ +
+
+ Bulk Actions + + + + +
@@ -8,20 +34,46 @@ + {foreach from=$jobs item=job} {assign var=current_status value=$job->currentStatus()} - + - + + {/foreach}
Destination Title StatusActions
{$job->name()}{$job->name()} {$job->destinationFilename()} {$job->title()}{$current_status->statusName()}{if $current_status->hasProgressInfo()} ({$current_status->ripProgress()}%, last updated {$current_status->mtime()|date_format:"%D %T"}){/if} + {$current_status->statusName()} + {if $current_status->hasProgressInfo()} +
+ Progress: {$current_status->ripProgress()}%
+ At: {$current_status->mtime()|date_format:"%D %T"}
+ ETA: {$job->calculateETA()|formatDuration} + {/if} +
+
+ + + + +
+
+
+ Bulk Actions + + + + +
+
{else} There are no jobs {/if} + diff --git a/templates/rips/setup-rip.tpl b/templates/rips/setup-rip.tpl index f585cae..f97ea3f 100644 --- a/templates/rips/setup-rip.tpl +++ b/templates/rips/setup-rip.tpl @@ -8,15 +8,17 @@ to see a list of running jobs, or the logs page for more detailed progress information.

{else} +

{$source->filename()|htmlspecialchars}

+
Configure global rip options - +
- +
@@ -58,7 +60,7 @@
{foreach from=$titles item=title} -

Title {$title->id()} (Duration: {$title->duration()}, Chapters: {$title->chapterCount()})

+

Title {$title->id()} (Duration: {$title->duration()}, Chapters: {$title->chapterCount()})

Configure title rip options @@ -68,11 +70,16 @@
+
+ + +
+
@@ -142,7 +149,7 @@ {literal}