Merge branch '2.1.x' of dev.sourcefabric.org:airtime into 2.1.x

This commit is contained in:
Martin Konecny 2012-06-25 18:31:07 -04:00
commit 84f867a00f
17 changed files with 298 additions and 147 deletions

View File

@ -23,7 +23,9 @@ date_default_timezone_set('UTC');
date_default_timezone_set(Application_Model_Preference::GetTimezone()); date_default_timezone_set(Application_Model_Preference::GetTimezone());
global $CC_CONFIG; global $CC_CONFIG;
$CC_CONFIG['airtime_version'] = Application_Model_Preference::GetAirtimeVersion(); $airtime_version = Application_Model_Preference::GetAirtimeVersion();
$uniqueid = Application_Model_Preference::GetUniqueId();
$CC_CONFIG['airtime_version'] = md5($airtime_version + $uniqueid);
require_once __DIR__."/configs/navigation.php"; require_once __DIR__."/configs/navigation.php";

View File

@ -100,6 +100,12 @@ class LoginController extends Zend_Controller_Action
public function passwordRestoreAction() public function passwordRestoreAction()
{ {
global $CC_CONFIG;
$request = $this->getRequest();
$baseUrl = $request->getBaseUrl();
$this->view->headScript()->appendFile($baseUrl.'/js/airtime/login/password-restore.js?'.$CC_CONFIG['airtime_version'],'text/javascript');
if (!Application_Model_Preference::GetEnableSystemEmail()) { if (!Application_Model_Preference::GetEnableSystemEmail()) {
$this->_redirect('login'); $this->_redirect('login');
} }

View File

@ -29,5 +29,13 @@ class Application_Form_PasswordRestore extends Zend_Form
'ViewHelper' 'ViewHelper'
) )
)); ));
$cancel = new Zend_Form_Element_Button("cancel");
$cancel->class = 'ui-button ui-widget ui-state-default ui-button-text-only center';
$cancel->setLabel("Cancel")
->setIgnore(True)
->setAttrib('onclick', 'redirectToLogin();')
->setDecorators(array('ViewHelper'));
$this->addElement($cancel);
} }
} }

View File

@ -20,58 +20,77 @@ class Application_Model_LiveLog
if ($rows != null) { if ($rows != null) {
$last_row = self::UpdateLastLogEndTime(array_pop($rows)); $last_row = self::UpdateLastLogEndTime(array_pop($rows));
array_push($rows, $last_row); array_push($rows, $last_row);
$skip = false;
} else {
$sql = "SELECT * FROM CC_LIVE_LOG"
." WHERE state = 'L'"
." ORDER BY id";
$rows = $con->query($sql)->fetchAll();
if ($rows != null) {
$last_row = self::UpdateLastLogEndTime(array_pop($rows));
array_push($rows, $last_row);
foreach ($rows as $row) {
$sql_delete = "DELETE FROM CC_LIVE_LOG"
." WHERE id = '{$row['id']}'";
$con->exec($sql_delete);
}
}
$skip = true;
} }
$hours = 0; $hours = 0;
$minutes = 0; $minutes = 0;
$seconds = 0; $seconds = 0;
foreach ($rows as $row) { if (!$skip) {
$end = new DateTime($row['end_time']); foreach ($rows as $row) {
$start = new DateTime($row['start_time']); $end = new DateTime($row['end_time']);
$duration = $start->diff($end); $start = new DateTime($row['start_time']);
$duration = $duration->format("%H:%i:%s"); $duration = $start->diff($end);
$intervals = explode(":", $duration); $duration = $duration->format("%H:%i:%s");
for ($i = 0; $i < sizeof($intervals); $i++) { $intervals = explode(":", $duration);
if (!isset($intervals[$i])) { for ($i = 0; $i < sizeof($intervals); $i++) {
$intervals[$i] = 0; if (!isset($intervals[$i])) {
$intervals[$i] = 0;
}
}
// Trim milliseconds (DateInterval does not support)
$sec = explode(".", $intervals[2]);
if (isset($sec[0])) {
$intervals[2] = $sec[0];
}
$seconds += $intervals[2];
if ($seconds / 60 >= 1) {
$minutes += 1;
$seconds -= 60;
}
$minutes += $intervals[1];
if ($minutes / 60 >= 1) {
$hours += 1;
$minutes -= 60;
}
$hours += $intervals[0];
if (!$p_keepData) {
// Delete data we just used to start a new log history
$sql_delete = "DELETE FROM CC_LIVE_LOG"
." WHERE id = '{$row['id']}'";
$con->exec($sql_delete);
} }
} }
//Trim milliseconds
// Trim milliseconds (DateInterval does not support) $seconds = explode(".", $seconds);
$sec = explode(".", $intervals[2]); if (isset($seconds[0])) {
if (isset($sec[0])) { $minutes = (double)(($hours*60)+$minutes . "." . $seconds[0]);
$intervals[2] = $sec[0];
} }
else {
$seconds += $intervals[2]; $minutes = (double)(($hours*60)+$minutes);
if ($seconds / 60 >= 1) {
$minutes += 1;
$seconds -= 60;
} }
$minutes += $intervals[1];
if ($minutes / 60 >= 1) {
$hours += 1;
$minutes -= 60;
}
$hours += $intervals[0];
if (!$p_keepData) {
// Delete data we just used to start a new log history
$sql_delete = "DELETE FROM CC_LIVE_LOG"
." WHERE id = '{$row['id']}'";
$con->exec($sql_delete);
}
}
//Trim milliseconds
$seconds = explode(".", $seconds);
if (isset($seconds[0])) {
$minutes = (double)(($hours*60)+$minutes . "." . $seconds[0]);
}
else {
$minutes = (double)(($hours*60)+$minutes);
} }
return $minutes; return $minutes;
} catch (Exception $e) { } catch (Exception $e) {
@ -99,121 +118,141 @@ class Application_Model_LiveLog
if ($rows != null) { if ($rows != null) {
$last_row = self::UpdateLastLogEndTime(array_pop($rows)); $last_row = self::UpdateLastLogEndTime(array_pop($rows));
array_push($rows, $last_row); array_push($rows, $last_row);
$skip = false;
}
else {
$sql = "SELECT * FROM CC_LIVE_LOG"
." WHERE state = 'S'"
." ORDER BY id";
$rows = $con->query($sql)->fetchAll();
if ($rows != null) {
$last_row = self::UpdateLastLogEndTime(array_pop($rows));
array_push($rows, $last_row);
foreach ($rows as $row) {
$sql_delete = "DELETE FROM CC_LIVE_LOG"
." WHERE id = '{$row['id']}'";
$con->exec($sql_delete);
}
}
$skip = true;
} }
$hours = 0; $hours = 0;
$minutes = 0; $minutes = 0;
$seconds = 0; $seconds = 0;
/* Get all shows and tracks from cc_schedule that played if (!$skip) {
* during a scheduled state /* Get all shows and tracks from cc_schedule that played
*/ * during a scheduled state
foreach ($rows as $row) { */
$sql_get_tracks = "SELECT * FROM cc_schedule" foreach ($rows as $row) {
." WHERE starts >= '{$row['start_time']}'" $sql_get_tracks = "SELECT * FROM cc_schedule"
." AND starts < '{$row['end_time']}'" ." WHERE starts >= '{$row['start_time']}'"
." AND file_id IS NOT NULL" ." AND starts < '{$row['end_time']}'"
." AND media_item_played IS TRUE"; ." AND file_id IS NOT NULL"
$tracks = $con->query($sql_get_tracks)->fetchAll(); ." AND media_item_played IS TRUE";
foreach ($tracks as $track) { $tracks = $con->query($sql_get_tracks)->fetchAll();
if ($track['ends'] > $row['end_time']) { foreach ($tracks as $track) {
$scheduled_ends = new DateTime($row['end_time']); if ($track['ends'] > $row['end_time']) {
$track_ends = new DateTime($track['ends']); $scheduled_ends = new DateTime($row['end_time']);
$extra_time = $scheduled_ends->diff($track_ends); $track_ends = new DateTime($track['ends']);
$extra_time = $scheduled_ends->diff($track_ends);
/* Get difference between clip_length /* Get difference between clip_length
* and the extra time. We need to subtract * and the extra time. We need to subtract
* this difference from the track's * this difference from the track's
* clip length. * clip length.
*/ */
$clip_length = $track['clip_length']; $clip_length = $track['clip_length'];
//Convert clip_length into seconds //Convert clip_length into seconds
$clip_length_intervals = explode(":", $clip_length); $clip_length_intervals = explode(":", $clip_length);
for ($i = 0; $i < sizeof($clip_length_intervals); $i++) { for ($i = 0; $i < sizeof($clip_length_intervals); $i++) {
if (!isset($clip_length_intervals[$i])) { if (!isset($clip_length_intervals[$i])) {
$clip_length_intervals[$i] = 0; $clip_length_intervals[$i] = 0;
}
} }
} $clip_length_seconds = $clip_length_intervals[0]*3600 + $clip_length_intervals[1]*60 + $clip_length_intervals[2];
$clip_length_seconds = $clip_length_intervals[0]*3600 + $clip_length_intervals[1]*60 + $clip_length_intervals[2];
$extra_time = $extra_time->format("%H:%i:%s");
$extra_time = $extra_time->format("%H:%i:%s"); //Convert extra_time into seconds;
//Convert extra_time into seconds; $extra_time_intervals = explode(":", $extra_time);
$extra_time_intervals = explode(":", $extra_time); for ($i = 0; $i < sizeof($extra_time_intervals); $i++) {
for ($i = 0; $i < sizeof($extra_time_intervals); $i++) { if (!isset($extra_time_intervals[$i])) {
if (!isset($extra_time_intervals[$i])) { $extra_time_intervals[$i] = 0;
$extra_time_intervals[$i] = 0; }
} }
} $extra_time_seconds = $extra_time_intervals[0]*3600 + $extra_time_intervals[1]*60 + $extra_time_intervals[2];
$extra_time_seconds = $extra_time_intervals[0]*3600 + $extra_time_intervals[1]*60 + $extra_time_intervals[2];
$clip_length_seconds -= $extra_time_seconds; $clip_length_seconds -= $extra_time_seconds;
//Convert new clip_length into "H-i-s" format //Convert new clip_length into "H-i-s" format
$clip_length_arr = array(); $clip_length_arr = array();
if ($clip_length_seconds / 3600 >= 1) { if ($clip_length_seconds / 3600 >= 1) {
array_push($clip_length_arr, str_pad(floor($clip_length_seconds / 3600), 2, "0", STR_PAD_LEFT)); array_push($clip_length_arr, str_pad(floor($clip_length_seconds / 3600), 2, "0", STR_PAD_LEFT));
$clip_length_seconds -= floor($clip_length_seconds / 3600); $clip_length_seconds -= floor($clip_length_seconds / 3600);
}
else {
array_push($clip_length_arr, "00");
}
if ($clip_length_seconds / 60 >= 1) {
array_push($clip_length_arr, str_pad(floor($clip_length_seconds / 60), 2, "0", STR_PAD_LEFT));
$clip_length_seconds -= floor($clip_length_seconds / 60);
}
else {
array_push($clip_length_arr, "00");
}
array_push($clip_length_arr, str_pad($clip_length_seconds, 2, "0", STR_PAD_LEFT));
$clip_length = implode(":", $clip_length_arr);
} }
else { else {
array_push($clip_length_arr, "00"); $clip_length = $track['clip_length'];
}
if ($clip_length_seconds / 60 >= 1) {
array_push($clip_length_arr, str_pad(floor($clip_length_seconds / 60), 2, "0", STR_PAD_LEFT));
$clip_length_seconds -= floor($clip_length_seconds / 60);
}
else {
array_push($clip_length_arr, "00");
} }
array_push($clip_length_arr, str_pad($clip_length_seconds, 2, "0", STR_PAD_LEFT)); $intervals = explode(":", $clip_length);
$clip_length = implode(":", $clip_length_arr); for ($i = 0; $i < sizeof($intervals); $i++) {
} if (!isset($intervals[$i])) {
else { $intervals[$i] = 0;
$clip_length = $track['clip_length']; }
}
$intervals = explode(":", $clip_length);
for ($i = 0; $i < sizeof($intervals); $i++) {
if (!isset($intervals[$i])) {
$intervals[$i] = 0;
} }
} // Trim milliseconds (DateInteral does not support)
// Trim milliseconds (DateInteral does not support) $sec = explode(".", $intervals[2]);
$sec = explode(".", $intervals[2]); if (isset($sec[0])) {
if (isset($sec[0])) { $intervals[2] = $sec[0];
$intervals[2] = $sec[0]; }
$seconds += $intervals[2];
if ($seconds / 60 >= 1) {
$minutes += 1;
$seconds -= 60;
}
$minutes += $intervals[1];
if ($minutes / 60 >= 1) {
$hours += 1;
$minutes -= 60;
}
$hours += $intervals[0];
} }
$seconds += $intervals[2]; if (!$p_keepData) {
if ($seconds / 60 >= 1) { //Delete row because we do not need data anymore
$minutes += 1; $sql_delete = "DELETE FROM CC_LIVE_LOG"
$seconds -= 60; ." WHERE id = '{$row['id']}'";
$con->exec($sql_delete);
} }
$minutes += $intervals[1];
if ($minutes / 60 >= 1) {
$hours += 1;
$minutes -= 60;
}
$hours += $intervals[0];
} }
if (!$p_keepData) {
//Delete row because we do not need data anymore $seconds = explode(".", $seconds);
$sql_delete = "DELETE FROM CC_LIVE_LOG" if (isset($seconds[0])) {
." WHERE id = '{$row['id']}'"; $minutes = (double)(($hours*60)+$minutes . "." . $seconds[0]);
$con->exec($sql_delete); }
else {
$minutes = (double)(($hours*60)+$minutes);
} }
}
$seconds = explode(".", $seconds);
if (isset($seconds[0])) {
$minutes = (double)(($hours*60)+$minutes . "." . $seconds[0]);
}
else {
$minutes = (double)(($hours*60)+$minutes);
} }
return $minutes; return $minutes;
} catch (Exception $e) { } catch (Exception $e) {

View File

@ -888,7 +888,7 @@ Logging::log("getting media! - 2");
// Check if file is playable // Check if file is playable
$command = sprintf("/usr/bin/airtime-liquidsoap -c 'output.dummy(audio_to_stereo(single(\"%s\")))' 2>&1", $audio_file); $command = sprintf("/usr/bin/airtime-liquidsoap -c 'output.dummy(audio_to_stereo(single(\"%s\")))' 2>&1", $audio_file);
exec($command, $output, $rv); exec($command, $output, $rv);
if ($rv != 0 || $output[0] == 'TagLib: MPEG::Properties::read() -- Could not find a valid last MPEG frame in the stream.') { if ($rv != 0 || (!empty($output) && $output[0] == 'TagLib: MPEG::Properties::read() -- Could not find a valid last MPEG frame in the stream.')) {
$result = array("code" => 110, "message" => "This file appears to be corrupted and will not be added to media library."); $result = array("code" => 110, "message" => "This file appears to be corrupted and will not be added to media library.");
} }
else { else {

View File

@ -21,5 +21,8 @@
<dd id="submit-element"> <dd id="submit-element">
<?php echo $this->element->getElement('submit') ?> <?php echo $this->element->getElement('submit') ?>
</dd> </dd>
<dd id="cancel-element">
<?php echo $this->element->getElement('cancel') ?>
</dd>
</dl> </dl>
</form> </form>

View File

@ -69,16 +69,19 @@
<?php echo $this->form->getSubform('live_stream_subform'); ?> <?php echo $this->form->getSubform('live_stream_subform'); ?>
</div> </div>
<div style="float: left; width: 600px;"> <div style="float: left; width: 600px;">
<fieldset class="padded">
<legend>Output Stream Settings</legend>
<?php <?php
for($i=1;$i<=$this->num_stream;$i++){ for($i=1;$i<=$this->num_stream;$i++){
echo $this->form->getSubform("s".$i."_subform"); echo $this->form->getSubform("s".$i."_subform");
} }
?> ?>
</fieldset>
<?php if($this->enable_stream_conf == "true"){?> <?php if($this->enable_stream_conf == "true"){?>
<div class="button-bar bottom" id="submit-element"> <div class="button-bar bottom" id="submit-element">
<input type="submit" class="ui-button ui-state-default right-floated" value="Save" id="Save" name="Save" /> <input type="submit" class="ui-button ui-state-default right-floated" value="Save" id="Save" name="Save" />
</div> </div>
<?php }?> <?php }?>
</div> </div>
</form> </form>
</div> </div>

View File

@ -16,7 +16,7 @@
<?php echo $this->when; ?> <?php echo $this->when; ?>
<?php echo $this->repeats; ?> <?php echo $this->repeats; ?>
</div> </div>
<h3 class="collapsible-header"><span class="arrow-icon"></span>Live Stream</h3> <h3 class="collapsible-header"><span class="arrow-icon"></span>Live Stream Input</h3>
<div id="live-stream-override" class="collapsible-content"> <div id="live-stream-override" class="collapsible-content">
<?php echo $this->live; ?> <?php echo $this->live; ?>
</div> </div>

View File

@ -808,6 +808,12 @@ dt.block-display, dd.block-display {
font-size:14px; font-size:14px;
padding: 6px 10px 6px; padding: 6px 10px 6px;
} }
.login-content dd button.ui-button, .login-content dd button.btn {
width:100%;
font-size:14px;
padding: 6px 10px 6px;
}
.login-content .hidden, .hidden { .login-content .hidden, .hidden {
display:none; display:none;
} }

View File

@ -0,0 +1,3 @@
function redirectToLogin(){
window.location = "/Login"
}

View File

@ -1,3 +1,32 @@
function getFileName(ext){
var filename = $("#his_date_start").val()+"_"+$("#his_time_start").val()+"m--"+$("#his_date_end").val()+"_"+$("#his_time_end").val()+"m"
filename = filename.replace(/:/g,"h")
if(ext == "pdf"){
filename = filename+".pdf"
}else{
filename = filename+".csv"
}
return filename;
}
function setFlashFileName( nButton, oConfig, oFlash ) {
var filename = getFileName(oConfig.sExtends)
oFlash.setFileName( filename );
if(oConfig.sExtends == "pdf"){
this.fnSetText( oFlash,
"title:"+ this.fnGetTitle(oConfig) +"\n"+
"message:"+ oConfig.sPdfMessage +"\n"+
"colWidth:"+ this.fnCalcColRatios(oConfig) +"\n"+
"orientation:"+ oConfig.sPdfOrientation +"\n"+
"size:"+ oConfig.sPdfSize +"\n"+
"--/TableToolsOpts--\n" +
this.fnGetTableData(oConfig));
}else{
this.fnSetText( oFlash,
this.fnGetTableData(oConfig));
}
}
var AIRTIME = (function(AIRTIME) { var AIRTIME = (function(AIRTIME) {
var mod; var mod;
@ -37,7 +66,7 @@ var AIRTIME = (function(AIRTIME) {
"aoColumns": [ "aoColumns": [
{"sTitle": "Title", "mDataProp": "title", "sClass": "his_title"}, /* Title */ {"sTitle": "Title", "mDataProp": "title", "sClass": "his_title"}, /* Title */
{"sTitle": "Artist", "mDataProp": "artist", "sClass": "his_artist"}, /* Creator */ {"sTitle": "Creator", "mDataProp": "artist", "sClass": "his_artist"}, /* Creator */
{"sTitle": "Played", "mDataProp": "played", "sClass": "his_artist"}, /* times played */ {"sTitle": "Played", "mDataProp": "played", "sClass": "his_artist"}, /* times played */
{"sTitle": "Length", "mDataProp": "length", "sClass": "his_length library_length"}, /* Length */ {"sTitle": "Length", "mDataProp": "length", "sClass": "his_length library_length"}, /* Length */
{"sTitle": "Composer", "mDataProp": "composer", "sClass": "his_composer"}, /* Composer */ {"sTitle": "Composer", "mDataProp": "composer", "sClass": "his_composer"}, /* Composer */
@ -65,7 +94,19 @@ var AIRTIME = (function(AIRTIME) {
"sDom": 'lf<"dt-process-rel"r><"H"T><"dataTables_scrolling"t><"F"ip>', "sDom": 'lf<"dt-process-rel"r><"H"T><"dataTables_scrolling"t><"F"ip>',
"oTableTools": { "oTableTools": {
"sSwfPath": "/js/datatables/plugin/TableTools/swf/copy_cvs_xls_pdf.swf" "sSwfPath": "/js/datatables/plugin/TableTools/swf/copy_cvs_xls_pdf.swf",
"aButtons": [
"copy",
{
"sExtends": "csv",
"fnClick": setFlashFileName
},
{
"sExtends": "pdf",
"fnClick": setFlashFileName
},
"print"
]
} }
}); });
oTable.fnSetFilteringDelay(350); oTable.fnSetFilteringDelay(350);

View File

@ -890,8 +890,8 @@ var AIRTIME = (function(AIRTIME){
$ul = $("<ul/>"); $ul = $("<ul/>");
$ul.append('<li class="ui-state-default sb-button-select" title="Select"><span class="ui-icon ui-icon-document-b"></span></li>') $ul.append('<li class="ui-state-default sb-button-select" title="Select"><span class="ui-icon ui-icon-document-b"></span></li>')
.append('<li class="ui-state-default ui-state-disabled sb-button-trim" title="Delete all overbooked tracks"><span class="ui-icon ui-icon-scissors"></span></li>') .append('<li class="ui-state-default ui-state-disabled sb-button-trim" title="Remove all overbooked tracks"><span class="ui-icon ui-icon-scissors"></span></li>')
.append('<li class="ui-state-default ui-state-disabled sb-button-delete" title="Delete selected scheduled items"><span class="ui-icon ui-icon-trash"></span></li>'); .append('<li class="ui-state-default ui-state-disabled sb-button-delete" title="Remove selected scheduled items"><span class="ui-icon ui-icon-trash"></span></li>');
$toolbar.append($ul); $toolbar.append($ul);
$ul = $("<ul/>"); $ul = $("<ul/>");

View File

@ -15,8 +15,11 @@ class Version20110711161043 extends AbstractMigration
{ {
public function up(Schema $schema) public function up(Schema $schema)
{ {
$ini = parse_ini_file(__DIR__."../include/airtime-install.ini");
$stor_dir = $ini["storage_dir"];
/* 1) update cc_files table to include to "directory" column */ /* 1) update cc_files table to include to "directory" column */
$this->_addSql("INSERT INTO cc_music_dirs (type, directory) VALUES ('stor', '/srv/airtime/stor/');"); $this->_addSql("INSERT INTO cc_music_dirs (type, directory) VALUES ('stor', $stor_dir);");
$this->_addSql("INSERT INTO cc_music_dirs (type, directory) VALUES ('link', '');"); $this->_addSql("INSERT INTO cc_music_dirs (type, directory) VALUES ('link', '');");

View File

@ -318,8 +318,9 @@ class AirtimeInstall
public static function SetDefaultTimezone() public static function SetDefaultTimezone()
{ {
$con = Propel::getConnection(); $con = Propel::getConnection();
$defaultTimezone = exec("cat /etc/timezone"); // we need to run php as commandline because we want to get the timezone in cli php.ini file
$defaultTimezone = exec("php -r 'echo date_default_timezone_get().PHP_EOL;'");
$sql = "INSERT INTO cc_pref (keystr, valstr) VALUES ('timezone', '$defaultTimezone')"; $sql = "INSERT INTO cc_pref (keystr, valstr) VALUES ('timezone', '$defaultTimezone')";
$result = $con->exec($sql); $result = $con->exec($sql);
if ($result < 1) { if ($result < 1) {

View File

@ -0,0 +1,24 @@
<?php
/* All functions other than start() should be marked as
* private.
*/
class AirtimeDatabaseUpgrade{
public static function start($p_dbValues){
echo "* Updating Database".PHP_EOL;
self::task0($p_dbValues);
echo " * Complete".PHP_EOL;
}
private static function task0($p_dbValues){
$username = $p_dbValues['database']['dbuser'];
$password = $p_dbValues['database']['dbpass'];
$host = $p_dbValues['database']['host'];
$database = $p_dbValues['database']['dbname'];
$dir = __DIR__;
passthru("export PGPASSWORD=$password && psql -h $host -U $username -q -f $dir/data/upgrade.sql $database 2>&1 | grep -v \"will create implicit index\"");
}
}

View File

@ -0,0 +1,8 @@
<?php
require_once 'DbUpgrade.php';
$filename = "/etc/airtime/airtime.conf";
$values = parse_ini_file($filename, true);
AirtimeDatabaseUpgrade::start($values);

View File

@ -0,0 +1,4 @@
DELETE FROM cc_pref WHERE keystr = 'system_version';
INSERT INTO cc_pref (keystr, valstr) VALUES ('system_version', '2.1.3');
UPDATE cc_show_instances SET time_filled='00:00:00' WHERE time_filled IS NULL;