7 Commits

Author SHA1 Message Date
6a57a6fca5 Merge branch 'release-0.2' 2011-08-22 19:04:36 +01:00
a061c23041 Bumped version number to 0.2 2011-08-22 19:03:36 +01:00
4300034afa Merge branch 'feature-ajax' into develop 2011-08-22 18:48:59 +01:00
eb1e330bc4 Added ajax support, and verified with an ajaxified Delete Source feature 2011-08-22 18:47:45 +01:00
41fc0a2cc3 Improve logging in HandBrake worker 2011-08-21 16:12:32 +01:00
95fe2e7641 Add additional logging for batch job runner 2011-08-21 16:11:51 +01:00
8d2ca716df Updated worker to copy rather than move output to final location
This causes ACLs to be inherited from the destination, rather than
preserving permissions from the temporary build location on the local
machine.
2011-08-20 11:07:30 +01:00
23 changed files with 417 additions and 54 deletions

View File

@@ -66,21 +66,31 @@ class Net_Gearman_Job_HandBrake extends Net_Gearman_Job_Common implements Rippin
if ($return_val) { if ($return_val) {
// Remove any temporary output files // Remove any temporary output files
if (file_exists($args['temp_output_filename'])) { if (file_exists($args['temp_output_filename'])) {
unlink($args['temp_output_filename']); $result = unlink($args['temp_output_filename']);
if (!$result) {
RippingCluster_WorkerLogEntry::warning($log, $this->job->id(), "Failed to remove temporary output file, still exists at '{$args['temp_output_filename']}'.");
} }
$this->fail($return_val); }
$this->fail("Call to HandBrake failed with return code {$return_val}.");
} else { } else {
// Move the temporary output file to the desired destination // Copy the temporary output file to the desired destination
$move = rename($args['temp_output_filename'], $args['rip_options']['output_filename']); $move = copy($args['temp_output_filename'], $args['rip_options']['output_filename']);
if ($move) { if ($move) {
// Remove the temporary output file
$result = unlink($args['temp_output_filename']);
if (!$result) {
RippingCluster_WorkerLogEntry::warning($log, $this->job->id(), "Failed to remove temporary output file, still exists at '{$args['temp_output_filename']}'.");
}
//Report success
$this->job->updateStatus(RippingCluster_JobStatus::COMPLETE); $this->job->updateStatus(RippingCluster_JobStatus::COMPLETE);
$this->complete( array( $this->complete( array(
'id' => $this->job->id() 'id' => $this->job->id()
)); ));
} else { } else {
RippingCluster_WorkerLogEntry::error($log, $this->job->id(), "Failed to move temporary output file to proper destination. File retained as '{$args['temp_output_filename']}'."); RippingCluster_WorkerLogEntry::error($log, $this->job->id(), "Failed to copy temporary output file to proper destination. File retained as '{$args['temp_output_filename']}'.");
$this->job->updateStatus(RippingCluster_JobStatus::FAILED); $this->job->updateStatus(RippingCluster_JobStatus::FAILED);
$this->fail('-1'); $this->fail('Encode complete, but output file could not be copied to the correct place.');
} }
} }
} }
@@ -123,7 +133,7 @@ class Net_Gearman_Job_HandBrake extends Net_Gearman_Job_Common implements Rippin
$status = $rip->job->currentStatus(); $status = $rip->job->currentStatus();
$status->updateRipProgress($matches[1]); $status->updateRipProgress($matches[1]);
$this->status($matches[1], 100); $this->status($matches[1], 100);
} else { } else if (!preg_match('/^\s+$/', $line)) {
$log = RippingCluster_Main::instance()->log(); $log = RippingCluster_Main::instance()->log();
RippingCluster_WorkerLogEntry::debug($log, $rip->job->id(), $line); RippingCluster_WorkerLogEntry::debug($log, $rip->job->id(), $line);
} }

View File

@@ -17,6 +17,7 @@ class RippingCluster_Main extends SihnonFramework_Main {
$this->request = new RippingCluster_RequestParser($request_string); $this->request = new RippingCluster_RequestParser($request_string);
switch (HBC_File) { switch (HBC_File) {
case 'ajax':
case 'index': { case 'index': {
$smarty_tmp = '/tmp/ripping-cluster'; $smarty_tmp = '/tmp/ripping-cluster';
$this->smarty = new Smarty(); $this->smarty = new Smarty();
@@ -28,10 +29,12 @@ class RippingCluster_Main extends SihnonFramework_Main {
$this->smarty->registerPlugin('modifier', 'formatDuration', array('RippingCluster_Main', 'formatDuration')); $this->smarty->registerPlugin('modifier', 'formatDuration', array('RippingCluster_Main', 'formatDuration'));
$this->smarty->registerPlugin('modifier', 'formatFilesize', array('RippingCluster_Main', 'formatFilesize')); $this->smarty->registerPlugin('modifier', 'formatFilesize', array('RippingCluster_Main', 'formatFilesize'));
$this->smarty->assign('version', '0.1'); $this->smarty->assign('version', '0.2');
$this->smarty->assign('messages', array()); $this->smarty->assign('messages', array());
$this->smarty->assign('base_uri', $this->base_uri); $this->smarty->assign('base_uri', $this->base_uri);
$this->smarty->assign('base_url', static::absoluteUrl(''));
} break; } break;
} }

View File

@@ -1,6 +1,13 @@
<?php <?php
if (isset($_SERVER['RIPPING_CLUSTER_CONFIG']) &&
file_exists($_SERVER['RIPPING_CLUSTER_CONFIG']) &&
is_readable($_SERVER['RIPPING_CLUSTER_CONFIG'])) {
require_once($_SERVER['RIPPING_CLUSTER_CONFIG']);
} else {
require_once '/etc/ripping-cluster/config.php'; require_once '/etc/ripping-cluster/config.php';
}
require_once SihnonFramework_Lib . 'SihnonFramework/Main.class.php'; require_once SihnonFramework_Lib . 'SihnonFramework/Main.class.php';
SihnonFramework_Main::registerAutoloadClasses('SihnonFramework', SihnonFramework_Lib, SihnonFramework_Main::registerAutoloadClasses('SihnonFramework', SihnonFramework_Lib,

22
webui/a.php Normal file
View File

@@ -0,0 +1,22 @@
<?php
define('HBC_File', 'ajax');
require '_inc.php';
try {
$main = RippingCluster_Main::instance();
RippingCluster_LogEntry::setLocalProgname('webui');
$smarty = $main->smarty();
$page = new RippingCluster_Page($smarty, $main->request());
if ($page->evaluate()) {
//header('Content-Type: text/json');
$smarty->display('ajax.tpl');
}
} catch (RippingCluster_Exception $e) {
die("Uncaught Exception: " . $e->getMessage());
}
?>

View File

@@ -29,8 +29,12 @@ try {
$set->addTask($task); $set->addTask($task);
$job->updateStatus(RippingCluster_JobStatus::QUEUED); $job->updateStatus(RippingCluster_JobStatus::QUEUED);
RippingCluster_ClientLogEntry::info($log, $rip_options['id'], 'Job queued', 'client');
} }
$job_count = count($jobs);
RippingCluster_ClientLogEntry::info($log, null, "Job queue started with {$job_count} jobs.", 'batch');
// Start the job queue // Start the job queue
$result = $client->runSet($set); $result = $client->runSet($set);
@@ -58,7 +62,7 @@ function gearman_fail($task) {
$job = RippingCluster_Job::fromId($task->arg['rip_options']['id']); $job = RippingCluster_Job::fromId($task->arg['rip_options']['id']);
$job->updateStatus(RippingCluster_JobStatus::FAILED); $job->updateStatus(RippingCluster_JobStatus::FAILED);
RippingCluster_ClientLogEntry::info($log, $job->id(), 'Job failed'); RippingCluster_ClientLogEntry::info($log, $job->id(), "Job failed with message: {$task->result}");
} }

141
webui/scripts/main.js Normal file
View File

@@ -0,0 +1,141 @@
var rc = {
init: function() {
rc.ajax.init();
rc.dialog.init();
rc.page.init();
},
ajax: {
init: function() {
},
get: function(url) {
$.ajax({
url: url,
type: "GET",
dataType: "json",
success: rc.ajax.success,
error: rc.ajax.failure
});
},
post: function(url, data) {
$.ajax(url, {
type: "POST",
dataType: "json",
data: data,
success: rc.ajax.success,
error: rc.ajax.failure
});
},
success: function(d, s, x) {
rc.page.update(d);
rc.dialog.prepare(d);
},
failure: function(x, s, e) {
console.log("Ajax Failure: " + s, e);
console.log(x.responseText);
}
},
dialog: {
init: function() {
$("#dialogheaderclose").click(rc.dialog.close);
},
prepare: function(d) {
if (d.dialog && d.dialog.show) {
if (d.dialog.buttons) {
switch (d.dialog.buttons.type) {
case 'yesno':
$("#dialogfooteryes").click(
function() {
rc.trigger(d.dialog.buttons.actions.yes, d.dialog.buttons.params);
}
);
$("#dialogfooterno").click(
function() {
rc.trigger(d.dialog.buttons.actions.no, d.dialog.buttons.params);
}
);
$("#dialogfooteryesno").show();
break;
}
}
$("#dialog").show();
}
},
close: function() {
$("#dialog").hide();
$(".dialogfooterbuttonset").hide();
$("#dialogcontent").html();
}
},
page: {
init: function() {
},
update: function(d) {
for ( var f in d.page_replacements) {
$("#" + f).html(d.page_replacements[f].content);
}
}
},
sources: {
remove: function(plugin, source) {
rc.ajax.get(base_url + "ajax/delete-source/plugin/" + plugin + "/id/" + source);
},
remove_confirmed: function(plugin, source) {
rc.ajax.get(base_url + "ajax/delete-source/plugin/" + plugin + "/id/" + source + "/confirm/");
}
},
actions: {
'close-dialog': function(params) {
rc.dialog.close();
},
'delete-source-confirm': function(params) {
rc.sources.remove_confirmed(params['plugin'], params['id']);
}
},
trigger: function(action, params) {
// Handle a list of actions by repeated calling self for each argument
if (action instanceof Array) {
for(i in action) {
rc.trigger(action[i], params);
}
return;
}
// Check if action is supported, and execute it
if (rc.actions[action]) {
rc.actions[action](params);
} else {
console.log("Action not supported: " +action);
}
}
};
$(document).ready(rc.init);

View File

@@ -0,0 +1,35 @@
<?php
$main = RippingCluster_Main::instance();
$req = $main->request();
$config = $main->config();
// Grab the name of this source
$encoded_filename = null;
if ($req->exists('confirm')) {
$this->smarty->assign('confirmed', true);
$plugin = $req->get('plugin', 'RippingCluster_Exception_InvalidParameters');
$encoded_filename = $req->get('id', 'RippingCluster_Exception_InvalidParameters');
$source = RippingCluster_Source_PluginFactory::loadEncoded($plugin, $encoded_filename, false);
$source->delete();
// Generate a new list of sources to update the page with
$all_sources = RippingCluster_Source_PluginFactory::enumerateAll();
$this->smarty->assign('all_sources', $all_sources);
} else {
$this->smarty->assign('confirmed', false);
$plugin = $req->get('plugin', 'RippingCluster_Exception_InvalidParameters');
$encoded_filename = $req->get('id', 'RippingCluster_Exception_InvalidParameters');
$source = RippingCluster_Source_PluginFactory::loadEncoded($plugin, $encoded_filename, false);
$this->smarty->assign('source', $source);
$this->smarty->assign('source_plugin', $plugin);
$this->smarty->assign('source_id', $encoded_filename);
}
?>

View File

@@ -0,0 +1,9 @@
<?php
$main = RippingCluster_Main::instance();
$config = $main->config();
$all_sources = RippingCluster_Source_PluginFactory::enumerateAll();
$this->smarty->assign('all_sources', $all_sources);
?>

View File

@@ -15,7 +15,7 @@ if ($req->exists('submit')) {
// Spawn the background client process to run all the jobs // Spawn the background client process to run all the jobs
RippingCluster_Job::runAllJobs(); RippingCluster_Job::runAllJobs();
RippingCluster_Page::redirect('rips/setup-rip/queued'); RippingCluster_Page::redirect('rips/setup/queued');
} elseif ($req->exists('queued')) { } elseif ($req->exists('queued')) {
$this->smarty->assign('rips_submitted', true); $this->smarty->assign('rips_submitted', true);

View File

@@ -0,0 +1,11 @@
{
{if $messages}
messages: [
{foreach from=$messages item=message}
'{$message}',
{/foreach}
],
{/if}
{$page_content}
}

View File

@@ -0,0 +1,37 @@
"page_replacements": {
{if $confirmed}
"source-list": {
{include file="fragments/source-list.tpl" assign="sources_html"}
"content": {$sources_html|json_encode}
}
{else}
"dialogcontent": {
{include file="fragments/delete-source.tpl" assign="delete_source_html"}
"content": {$delete_source_html|json_encode}
}
{/if}
{if ! $confirmed}
},
"dialog": {
"show": true,
"buttons": {
"type": "yesno",
"actions": {
"yes": [
"delete-source-confirm",
"close-dialog"
],
"no": "close-dialog"
},
"params": {
"plugin": {$source_plugin|json_encode},
"id": {$source_id|json_encode}
}
}
}
{else}
}
{/if}

View File

@@ -0,0 +1,6 @@
"page_replacements": {
"source-list": {
{include file="fragments/source-list.tpl" assign="sources_html"}
"content": {$sources_html|json_encode}
}
}

View File

@@ -0,0 +1,3 @@
<p>
Are you sure you want to delete {$source->plugin()|escape:"html"}:{$source->filename()|escape:"html"}?
</p>

View File

@@ -0,0 +1,25 @@
{foreach from=$all_sources key=type item=sources}
<li>{$type}
{if $sources}
<ul>
{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()}
<li>
[ <a href="{$base_uri}sources/details/plugin/{$source_plugin}/id/{$source_filename_encoded}" title="Browse source details">Browse</a> |
<a href="{$base_uri}rips/setup/plugin/{$source_plugin}/id/{$source_filename_encoded}" title="Rip this source">Rip</a> |
<a href="javascript:rc.sources.remove('{$source_plugin|escape:'quote'}', '{$source_filename_encoded|escape:'quote'}');" title="Delete this source">Delete</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>
{/foreach}

View File

@@ -6,9 +6,15 @@
</script> </script>
<link rel="stylesheet" type="text/css" href="{$base_uri}styles/normal.css" /> <link rel="stylesheet" type="text/css" href="{$base_uri}styles/normal.css" />
<script type="text/javascript">
var base_uri = "{$base_uri|escape:'quote'}";
var base_url = "{$base_url|escape:'quote'}";
</script>
<link type="text/css" href="{$base_uri}styles/3rdparty/jquery-ui/smoothness/jquery-ui-1.8.custom.css" rel="Stylesheet" /> <link type="text/css" href="{$base_uri}styles/3rdparty/jquery-ui/smoothness/jquery-ui-1.8.custom.css" rel="Stylesheet" />
<script type="text/javascript" src="{$base_uri}scripts/3rdparty/jquery-1.4.2.js"></script> <script type="text/javascript" src="{$base_uri}scripts/3rdparty/jquery-1.4.2.js"></script>
<script type="text/javascript" src="{$base_uri}scripts/3rdparty/jquery-ui-1.8.custom.min.js"></script> <script type="text/javascript" src="{$base_uri}scripts/3rdparty/jquery-ui-1.8.custom.min.js"></script>
<script type="text/javascript" src="{$base_uri}scripts/main.js"></script>
</head> </head>
<body> <body>
@@ -50,5 +56,23 @@
</div> </div>
<div id="centrepoint">
<div id="dialog">
<div id="dialogheader">
<div id="dialogheadertitle">Dialog</div>
<div id="dialogheaderclose">X</div>
</div>
<div id="dialogcontent"></div>
<div id="dialogfooter">
<div id="dialogfooteryesno" class="dialogfooterbuttonset">
<fieldset>
<input type="button" class="dialogbutton" id="dialogfooteryes" value="Yes" />
<input type="button" class="dialogbutton" id="dialogfooterno" value="No" />
</fieldset>
</div>
</div>
</div>
</div>
</body> </body>
</html> </html>

View File

@@ -10,7 +10,7 @@
{else} {else}
<h3>{$source->filename()|escape:"html"}</h3> <h3>{$source->filename()|escape:"html"}</h3>
<form name="setup-rips" id="setup-rips" action="{$base_uri}rips/setup-rip/submit/" method="post"> <form name="setup" id="setup-rips" action="{$base_uri}rips/setup/submit/" method="post">
<input type="hidden" name="plugin" value="{$source->plugin()|escape:"html"}" /> <input type="hidden" name="plugin" value="{$source->plugin()|escape:"html"}" />
<fieldset> <fieldset>
<legend>Configure global rip options</legend> <legend>Configure global rip options</legend>

View File

@@ -1,41 +0,0 @@
<h2>Sources</h2>
{if $all_sources}
<p>
The list below contains all the DVD sources that are available and ready for ripping.
</p>
<p>
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.
</p>
<ul>
{foreach from=$all_sources key=type item=sources}
<li>{$type}
{if $sources}
<ul>
{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()}
<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> |
<a href="{$base_uri}sources/delete/plugin/{$source_plugin}/id/{$source_filename_encoded}" title="Delete this source">Delete</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>
{/foreach}
</ul>
{else}
<p>
<em>There are currently no sources available to rip.</em>
</p>
{/if}

View File

@@ -5,7 +5,7 @@
<li>Browse <li>Browse
<ul> <ul>
<li><a href="{$base_uri}rips/sources" title="Browse Sources">Sources</a></li> <li><a href="{$base_uri}sources/list" title="Browse Sources">Sources</a></li>
</ul> </ul>
</li> </li>
</ul> </ul>

View File

@@ -0,0 +1,18 @@
<h2>Sources</h2>
{if $all_sources}
<p>
The list below contains all the DVD sources that are available and ready for ripping.
</p>
<p>
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.
</p>
<ul id="source-list">
{include file="fragments/source-list.tpl"}
</ul>
{else}
<p>
<em>There are currently no sources available to rip.</em>
</p>
{/if}

View File

@@ -93,6 +93,55 @@ label {
margin: 1em; margin: 1em;
} }
/* Centred dialog taken from http://stackoverflow.com/questions/1205457/how-to-design-a-css-for-a-centered-floating-confirm-dialog */
#centrepoint {
top: 50%;
left: 50%;
position: absolute;
}
#dialog {
position: relative;
width: 600px;
margin-left: -300px;
/*height: 20em;*/
margin-top: -20em;
display: none;
background: #eeeeee;
border: 2px solid #a7a09a;
}
#dialogheader {
height: 2em;
width: 100%;
margin: 0.3em;
}
#dialogheadertitle {
color: black;
font-weight: bold;
float: left;
}
#dialogheaderclose {
width: 1.2em;
height: 1.2em;
background-color: crimson;
color: white;
border: 1px solid fireBrick;
float: right;
margin-right: 1em;
text-align: center;
vertical-align: middle;
display: table-cell;
font-weight: bold;
cursor: pointer;
}
#dialogcontent {
padding: 0.5em;
}
.dialogfooterbuttonset {
display: none;
text-align: right;
}
.default { .default {
background: beige; background: beige;
color: darkgray; color: darkgray;