Merge branch 'saas-dev' of https://github.com/sourcefabric/Airtime into saas-dev

This commit is contained in:
Duncan Sommerville 2015-05-27 14:02:00 -04:00
commit 51a3f19f43
31 changed files with 501 additions and 323 deletions

View File

@ -101,6 +101,11 @@ class Bootstrap extends Zend_Application_Bootstrap_Bootstrap
$csrf_namespace->authtoken = sha1(uniqid(rand(), 1)); $csrf_namespace->authtoken = sha1(uniqid(rand(), 1));
$csrf_namespace->setExpirationSeconds(2 * 60 * 60); $csrf_namespace->setExpirationSeconds(2 * 60 * 60);
} }
//Here we are closing the session for writing because otherwise no requests
//in this session will be handled in parallel. This gives a major boost to the perceived performance
//of the application (page load times are more consistent, no lock contention).
session_write_close();
} }
/** /**

View File

@ -2,6 +2,10 @@
class Application_Common_TuneIn class Application_Common_TuneIn
{ {
/**
* @param $title url encoded string
* @param $artist url encoded string
*/
public static function sendMetadataToTunein($title, $artist) public static function sendMetadataToTunein($title, $artist)
{ {
$credQryStr = self::getCredentialsQueryString(); $credQryStr = self::getCredentialsQueryString();
@ -13,12 +17,20 @@ class Application_Common_TuneIn
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_TIMEOUT, 30); curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_exec($ch); $xmlResponse = curl_exec($ch);
if (curl_error($ch)) { if (curl_error($ch)) {
Logging::error("Failed to reach TuneIn: ". curl_errno($ch)." - ". curl_error($ch) . " - " . curl_getinfo($ch, CURLINFO_EFFECTIVE_URL)); Logging::error("Failed to reach TuneIn: ". curl_errno($ch)." - ". curl_error($ch) . " - " . curl_getinfo($ch, CURLINFO_EFFECTIVE_URL));
} }
curl_close($ch); curl_close($ch);
$xmlObj = new SimpleXMLElement($xmlResponse);
if (!$xmlObj || $xmlObj->head->status != "200") {
Logging::info("Error occurred pushing metadata to TuneIn:");
Logging::info($xmlResponse);
} else if ($xmlObj->head->status == "200") {
Application_Model_Preference::setLastTuneinMetadataUpdate(time());
}
} }
private static function getCredentialsQueryString() { private static function getCredentialsQueryString() {
@ -29,20 +41,4 @@ class Application_Common_TuneIn
return "?partnerId=".$tuneInPartnerID."&partnerKey=".$tuneInPartnerKey."&id=".$tuneInStationID; return "?partnerId=".$tuneInPartnerID."&partnerKey=".$tuneInPartnerKey."&id=".$tuneInStationID;
} }
public static function updateOfflineMetadata() {
$credQryStr = self::getCredentialsQueryString();
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, TUNEIN_API_URL . $credQryStr . "&commercial=true");
curl_setopt($ch, CURLOPT_FAILONERROR, 1);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_exec($ch);
if (curl_error($ch)) {
Logging::error("Failed to reach TuneIn: ". curl_errno($ch)." - ". curl_error($ch) . " - " . curl_getinfo($ch, CURLINFO_EFFECTIVE_URL));
}
curl_close($ch);
}
} }

View File

@ -1516,4 +1516,29 @@ class ApiController extends Zend_Controller_Action
} }
/**
* This function is called from PYPO (pypofetch) every 2 minutes and updates
* metadata on TuneIn if we haven't done so in the last 4 minutes. We have
* to do this because TuneIn turns off metadata if it has not received a
* request within 5 minutes. This is necessary for long tracks > 5 minutes.
*/
public function updateMetadataOnTuneinAction()
{
if (!Application_Model_Preference::getTuneinEnabled()) {
$this->_helper->json->sendJson(array(0));
}
$lastTuneInMetadataUpdate = Application_Model_Preference::geLastTuneinMetadataUpdate();
if (time() - $lastTuneInMetadataUpdate >= 240) {
$metadata = $metadata = Application_Model_Schedule::getCurrentPlayingTrack();
if (!is_null($metadata)) {
Application_Common_TuneIn::sendMetadataToTunein(
$metadata["title"],
$metadata["artist"]
);
}
}
$this->_helper->json->sendJson(array(1));
}
} }

View File

@ -8,6 +8,8 @@ class LoginController extends Zend_Controller_Action
public function init() public function init()
{ {
//Open the session for writing, because we close it for writing by default in Bootstrap.php as an optimization.
session_start();
} }
public function indexAction() public function indexAction()

View File

@ -31,6 +31,8 @@ class PlaylistController extends Zend_Controller_Action
->addActionContext('empty-content', 'json') ->addActionContext('empty-content', 'json')
->initContext(); ->initContext();
//This controller writes to the session all over the place, so we're going to reopen it for writing here.
session_start(); //Reopen the session for writing
} }
private function getPlaylist($p_type) private function getPlaylist($p_type)

View File

@ -32,6 +32,7 @@ class PreferenceController extends Zend_Controller_Action
$form = new Application_Form_Preferences(); $form = new Application_Form_Preferences();
$values = array(); $values = array();
session_start(); //Open session for writing.
if ($request->isPost()) { if ($request->isPost()) {
$values = $request->getPost(); $values = $request->getPost();
@ -94,6 +95,8 @@ class PreferenceController extends Zend_Controller_Action
$this->view->headScript()->appendFile($baseUrl.'js/airtime/preferences/support-setting.js?'.$CC_CONFIG['airtime_version'],'text/javascript'); $this->view->headScript()->appendFile($baseUrl.'js/airtime/preferences/support-setting.js?'.$CC_CONFIG['airtime_version'],'text/javascript');
$this->view->statusMsg = ""; $this->view->statusMsg = "";
session_start(); //Open session for writing.
$form = new Application_Form_SupportSettings(); $form = new Application_Form_SupportSettings();
if ($request->isPost()) { if ($request->isPost()) {
$values = $request->getPost(); $values = $request->getPost();
@ -128,6 +131,8 @@ class PreferenceController extends Zend_Controller_Action
public function removeLogoAction() public function removeLogoAction()
{ {
session_start(); //Open session for writing.
$this->view->layout()->disableLayout(); $this->view->layout()->disableLayout();
// Remove reliance on .phtml files to render requests // Remove reliance on .phtml files to render requests
$this->_helper->viewRenderer->setNoRender(true); $this->_helper->viewRenderer->setNoRender(true);
@ -145,6 +150,8 @@ class PreferenceController extends Zend_Controller_Action
$this->view->headScript()->appendFile($baseUrl.'js/airtime/preferences/streamsetting.js?'.$CC_CONFIG['airtime_version'],'text/javascript'); $this->view->headScript()->appendFile($baseUrl.'js/airtime/preferences/streamsetting.js?'.$CC_CONFIG['airtime_version'],'text/javascript');
session_start(); //Open session for writing.
// get current settings // get current settings
$setting = Application_Model_StreamSetting::getStreamSetting(); $setting = Application_Model_StreamSetting::getStreamSetting();
@ -435,6 +442,8 @@ class PreferenceController extends Zend_Controller_Action
public function setSourceConnectionUrlAction() public function setSourceConnectionUrlAction()
{ {
session_start(); //Open session for writing.
$request = $this->getRequest(); $request = $this->getRequest();
$type = $request->getParam("type", null); $type = $request->getParam("type", null);
$url = urldecode($request->getParam("url", null)); $url = urldecode($request->getParam("url", null));
@ -453,6 +462,8 @@ class PreferenceController extends Zend_Controller_Action
public function getAdminPasswordStatusAction() public function getAdminPasswordStatusAction()
{ {
session_start(); //Open session for writing.
$out = array(); $out = array();
$num_of_stream = intval(Application_Model_Preference::GetNumOfStreams()); $num_of_stream = intval(Application_Model_Preference::GetNumOfStreams());
for ($i=1; $i<=$num_of_stream; $i++) { for ($i=1; $i<=$num_of_stream; $i++) {

View File

@ -51,6 +51,11 @@ class ScheduleController extends Zend_Controller_Action
$baseUrl = Application_Common_OsPath::getBaseDir(); $baseUrl = Application_Common_OsPath::getBaseDir();
//Embed the schedule in our page response so we don't have to make an AJAX request to get this data after the page load.
$scheduleController = new ScheduleController($this->getRequest(), $this->getResponse());
$scheduleController->eventFeedPreloadAction();
$events = json_encode($scheduleController->view->events);
$this->view->headScript()->appendScript( $this->view->headScript()->appendScript(
"var calendarPref = {};\n". "var calendarPref = {};\n".
"calendarPref.weekStart = ".Application_Model_Preference::GetWeekStartDay().";\n". "calendarPref.weekStart = ".Application_Model_Preference::GetWeekStartDay().";\n".
@ -59,7 +64,7 @@ class ScheduleController extends Zend_Controller_Action
"calendarPref.timeScale = '".Application_Model_Preference::GetCalendarTimeScale()."';\n". "calendarPref.timeScale = '".Application_Model_Preference::GetCalendarTimeScale()."';\n".
"calendarPref.timeInterval = ".Application_Model_Preference::GetCalendarTimeInterval().";\n". "calendarPref.timeInterval = ".Application_Model_Preference::GetCalendarTimeInterval().";\n".
"calendarPref.weekStartDay = ".Application_Model_Preference::GetWeekStartDay().";\n". "calendarPref.weekStartDay = ".Application_Model_Preference::GetWeekStartDay().";\n".
"var calendarEvents = null;" "var calendarEvents = $events;"
); );
$this->view->headScript()->appendFile($baseUrl.'js/contextmenu/jquery.contextMenu.js?'.$CC_CONFIG['airtime_version'],'text/javascript'); $this->view->headScript()->appendFile($baseUrl.'js/contextmenu/jquery.contextMenu.js?'.$CC_CONFIG['airtime_version'],'text/javascript');
@ -145,7 +150,7 @@ class ScheduleController extends Zend_Controller_Action
} else if ($calendar_interval == "agendaWeek") { } else if ($calendar_interval == "agendaWeek") {
list($start, $end) = Application_Model_Show::getStartEndCurrentWeekView(); list($start, $end) = Application_Model_Show::getStartEndCurrentWeekView();
} else if ($calendar_interval == "month") { } else if ($calendar_interval == "month") {
list($start, $end) = Application_Model_Show::getStartEndCurrentMonthView(); list($start, $end) = Application_Model_Show::getStartEndCurrentMonthPlusView();
} else { } else {
Logging::error("Invalid Calendar Interval '$calendar_interval'"); Logging::error("Invalid Calendar Interval '$calendar_interval'");
} }
@ -295,17 +300,21 @@ class ScheduleController extends Zend_Controller_Action
} }
} }
/** This is a nasty hack to let us embed the the data the dashboard needs into the HTML response for each page.
* This was originally loaded AFTER page load by AJAX, which is needlessly slow. This should have been templated in.
*/
public static function printCurrentPlaylistForEmbedding()
{
$front = Zend_Controller_Front::getInstance();
$scheduleController = new ScheduleController($front->getRequest(), $front->getResponse());
$scheduleController->getCurrentPlaylistAction();
echo(json_encode($scheduleController->view));
}
public function getCurrentPlaylistAction() public function getCurrentPlaylistAction()
{ {
$range = Application_Model_Schedule::GetPlayOrderRangeOld(); $range = Application_Model_Schedule::GetPlayOrderRangeOld();
// If there is no current track playing update TuneIn so it doesn't
// display outdated metadata
//TODO: find a better solution for this so we don't spam the station on TuneIn
/*if (is_null($range["current"]) && Application_Model_Preference::getTuneinEnabled()) {
Application_Common_TuneIn::updateOfflineMetadata();
}*/
$show = Application_Model_Show::getCurrentShow(); $show = Application_Model_Show::getCurrentShow();
/* Convert all UTC times to localtime before sending back to user. */ /* Convert all UTC times to localtime before sending back to user. */

View File

@ -42,7 +42,7 @@ class ShowbuilderController extends Zend_Controller_Action
$this->view->headScript()->appendFile($baseUrl.'js/datatables/plugin/dataTables.pluginAPI.js?'.$CC_CONFIG['airtime_version'],'text/javascript'); $this->view->headScript()->appendFile($baseUrl.'js/datatables/plugin/dataTables.pluginAPI.js?'.$CC_CONFIG['airtime_version'],'text/javascript');
$this->view->headScript()->appendFile($baseUrl.'js/datatables/plugin/dataTables.fnSetFilteringDelay.js?'.$CC_CONFIG['airtime_version'],'text/javascript'); $this->view->headScript()->appendFile($baseUrl.'js/datatables/plugin/dataTables.fnSetFilteringDelay.js?'.$CC_CONFIG['airtime_version'],'text/javascript');
$this->view->headScript()->appendFile($baseUrl.'js/datatables/plugin/dataTables.ColVis.js?'.$CC_CONFIG['airtime_version'],'text/javascript'); $this->view->headScript()->appendFile($baseUrl.'js/datatables/plugin/dataTables.ColVis.js?'.$CC_CONFIG['airtime_version'],'text/javascript');
$this->view->headScript()->appendFile($baseUrl.'js/datatables/plugin/dataTables.ColReorder.js?'.$CC_CONFIG['airtime_version'],'text/javascript'); //$this->view->headScript()->appendFile($baseUrl.'js/datatables/plugin/dataTables.ColReorder.js?'.$CC_CONFIG['airtime_version'],'text/javascript');
$this->view->headScript()->appendFile($baseUrl.'js/datatables/plugin/dataTables.FixedColumns.js?'.$CC_CONFIG['airtime_version'],'text/javascript'); $this->view->headScript()->appendFile($baseUrl.'js/datatables/plugin/dataTables.FixedColumns.js?'.$CC_CONFIG['airtime_version'],'text/javascript');
$this->view->headScript()->appendFile($baseUrl.'js/datatables/plugin/dataTables.columnFilter.js?'.$CC_CONFIG['airtime_version'], 'text/javascript'); $this->view->headScript()->appendFile($baseUrl.'js/datatables/plugin/dataTables.columnFilter.js?'.$CC_CONFIG['airtime_version'], 'text/javascript');
@ -67,7 +67,8 @@ class ShowbuilderController extends Zend_Controller_Action
if (isset($values["Privacy"])) { if (isset($values["Privacy"])) {
Application_Model_Preference::SetPrivacyPolicyCheck($values["Privacy"]); Application_Model_Preference::SetPrivacyPolicyCheck($values["Privacy"]);
} }
// unset session session_start(); //open session for writing again
// unset referrer
Zend_Session::namespaceUnset('referrer'); Zend_Session::namespaceUnset('referrer');
} elseif ($values["Publicise"] == '1' && $form->isValid($values)) { } elseif ($values["Publicise"] == '1' && $form->isValid($values)) {
Application_Model_Preference::SetHeadTitle($values["stnName"], $this->view); Application_Model_Preference::SetHeadTitle($values["stnName"], $this->view);
@ -88,7 +89,8 @@ class ShowbuilderController extends Zend_Controller_Action
if (isset($values["Privacy"])) { if (isset($values["Privacy"])) {
Application_Model_Preference::SetPrivacyPolicyCheck($values["Privacy"]); Application_Model_Preference::SetPrivacyPolicyCheck($values["Privacy"]);
} }
// unset session session_start(); //open session for writing again
// unset referrer
Zend_Session::namespaceUnset('referrer'); Zend_Session::namespaceUnset('referrer');
} else { } else {
$logo = Application_Model_Preference::GetStationLogo(); $logo = Application_Model_Preference::GetStationLogo();
@ -156,7 +158,7 @@ class ShowbuilderController extends Zend_Controller_Action
//populate date range form for show builder. //populate date range form for show builder.
$now = time(); $now = time();
$from = $request->getParam("from", $now); $from = $request->getParam("from", $now);
$to = $request->getParam("to", $now + (24*60*60)); $to = $request->getParam("to", $now + (3*60*60));
$utcTimezone = new DateTimeZone("UTC"); $utcTimezone = new DateTimeZone("UTC");
$displayTimeZone = new DateTimeZone(Application_Model_Preference::GetTimezone()); $displayTimeZone = new DateTimeZone(Application_Model_Preference::GetTimezone());
@ -342,6 +344,7 @@ class ShowbuilderController extends Zend_Controller_Action
$selectedItems = $request->getParam("selectedItem"); $selectedItems = $request->getParam("selectedItem");
$afterItem = $request->getParam("afterItem"); $afterItem = $request->getParam("afterItem");
/*
$log_vars = array(); $log_vars = array();
$log_vars["url"] = $_SERVER['HTTP_HOST']; $log_vars["url"] = $_SERVER['HTTP_HOST'];
$log_vars["action"] = "showbuilder/schedule-move"; $log_vars["action"] = "showbuilder/schedule-move";
@ -349,6 +352,7 @@ class ShowbuilderController extends Zend_Controller_Action
$log_vars["params"]["selected_items"] = $selectedItems; $log_vars["params"]["selected_items"] = $selectedItems;
$log_vars["params"]["destination_after_item"] = $afterItem; $log_vars["params"]["destination_after_item"] = $afterItem;
Logging::info($log_vars); Logging::info($log_vars);
*/
try { try {
$scheduler = new Application_Model_Scheduler(); $scheduler = new Application_Model_Scheduler();

View File

@ -13,16 +13,18 @@ class SystemstatusController extends Zend_Controller_Action
public function indexAction() public function indexAction()
{ {
/*
$services = array( $services = array(
"pypo"=>Application_Model_Systemstatus::GetPypoStatus(), "pypo"=>Application_Model_Systemstatus::GetPypoStatus(),
"liquidsoap"=>Application_Model_Systemstatus::GetLiquidsoapStatus(), "liquidsoap"=>Application_Model_Systemstatus::GetLiquidsoapStatus(),
//"media-monitor"=>Application_Model_Systemstatus::GetMediaMonitorStatus(), //"media-monitor"=>Application_Model_Systemstatus::GetMediaMonitorStatus(),
); );
*/
$partitions = Application_Model_Systemstatus::GetDiskInfo(); $partitions = Application_Model_Systemstatus::GetDiskInfo();
$this->view->status = new StdClass; $this->view->status = new StdClass;
$this->view->status->services = $services; //$this->view->status->services = $services;
$this->view->status->partitions = $partitions; $this->view->status->partitions = $partitions;
} }
} }

View File

@ -16,6 +16,9 @@ class UserController extends Zend_Controller_Action
public function addUserAction() public function addUserAction()
{ {
// Start the session to re-open write permission to the session so we can
// create the namespace for our csrf token verification
session_start();
$CC_CONFIG = Config::getConfig(); $CC_CONFIG = Config::getConfig();
$request = $this->getRequest(); $request = $this->getRequest();
@ -119,6 +122,7 @@ class UserController extends Zend_Controller_Action
public function editUserAction() public function editUserAction()
{ {
session_start(); //Reopen session for writing.
$request = $this->getRequest(); $request = $this->getRequest();
$form = new Application_Form_EditUser(); $form = new Application_Form_EditUser();
if ($request->isPost()) { if ($request->isPost()) {

View File

@ -61,28 +61,23 @@ class UsersettingsController extends Zend_Controller_Action
public function getTimelineDatatableAction() public function getTimelineDatatableAction()
{ {
$start = microtime(true);
$data = Application_Model_Preference::getTimelineDatatableSetting(); $data = Application_Model_Preference::getTimelineDatatableSetting();
if (!is_null($data)) { if (!is_null($data)) {
$this->view->settings = $data; $this->view->settings = $data;
} }
$end = microtime(true);
Logging::debug("getting timeline datatables info took:");
Logging::debug(floatval($end) - floatval($start));
} }
public function remindmeAction() public function remindmeAction()
{ {
// unset session // unset session
session_start(); //open session for writing again
Zend_Session::namespaceUnset('referrer'); Zend_Session::namespaceUnset('referrer');
Application_Model_Preference::SetRemindMeDate(); Application_Model_Preference::SetRemindMeDate();
} }
public function remindmeNeverAction() public function remindmeNeverAction()
{ {
session_start(); //open session for writing again
Zend_Session::namespaceUnset('referrer'); Zend_Session::namespaceUnset('referrer');
//pass in true to indicate 'Remind me never' was clicked //pass in true to indicate 'Remind me never' was clicked
Application_Model_Preference::SetRemindMeDate(true); Application_Model_Preference::SetRemindMeDate(true);
@ -91,6 +86,7 @@ class UsersettingsController extends Zend_Controller_Action
public function donotshowregistrationpopupAction() public function donotshowregistrationpopupAction()
{ {
// unset session // unset session
session_start(); //open session for writing again
Zend_Session::namespaceUnset('referrer'); Zend_Session::namespaceUnset('referrer');
} }

View File

@ -87,43 +87,8 @@ class Application_Form_TuneInPreferences extends Zend_Form_SubForm
if (!$xmlObj || $xmlObj->head->status != "200") { if (!$xmlObj || $xmlObj->head->status != "200") {
$valid = false; $valid = false;
} else if ($xmlObj->head->status == "200") { } else if ($xmlObj->head->status == "200") {
Application_Model_Preference::setLastTuneinMetadataUpdate(time());
$valid = true; $valid = true;
// Make another request to TuneIn to update the metadata right away
// and to turn off the commercial flag.
/*$metadata = Application_Model_Schedule::getCurrentPlayingTrack();
if (!is_null($metadata)) {
Logging::info($metadata);
// Replace empty strings with "n/a" since the TuneIn API will complain
// and return an error that title and/or artist is not set.
$metadata["artist"] = empty($metadata["artist"]) ? "n/a" : $metadata["artist"];
$metadata["title"] = empty($metadata["title"]) ? "n/a" : $metadata["title"];
Logging::info($metadata);
$metadataQryStr = "&artist=" . $metadata["artist"] . "&title=" . $metadata["title"];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, TUNEIN_API_URL . $qry_str . "&commercial=false" . $metadataQryStr);
curl_setopt($ch, CURLOPT_FAILONERROR, 1);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$xmlData = curl_exec($ch);
Logging::info($xmlData);
if (curl_error($ch)) {
Logging::error("Failed to reach TuneIn: " . curl_errno($ch) . " - " . curl_error($ch) . " - " . curl_getinfo($ch, CURLINFO_EFFECTIVE_URL));
}
curl_close($ch);
$xmlObj = new SimpleXMLElement($xmlData);
if (!$xmlObj || $xmlObj->head->status != "200") {
Logging::error("Failed updating metadata on TuneIn");
}
}*/
} }
} }
} else { } else {

View File

@ -34,6 +34,23 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
"scheduled_play_switch"=>$sss['scheduled_play'])) ?> "scheduled_play_switch"=>$sss['scheduled_play'])) ?>
<?php $partial = array('menu.phtml', 'default'); <?php $partial = array('menu.phtml', 'default');
$this->navigation()->menu()->setPartial($partial); ?> $this->navigation()->menu()->setPartial($partial); ?>
<script type="text/javascript">
var schedulePreLoaded = <?php
//Awful hack to speed up loading - Embed the schedule in the response so that the dashboard
//doesn't have to make a separate AJAX request to get this data.
require_once("ScheduleController.php");
ScheduleController::printCurrentPlaylistForEmbedding();
?>;
//The DOM elements that these calls depend on exist by this point:
parseItems(schedulePreLoaded.entries);
parseSourceStatus(schedulePreLoaded.source_status);
parseSwitchStatus(schedulePreLoaded.switch_status);
</script>
<div id="nav"> <div id="nav">
<div class="logo"></div> <div class="logo"></div>
<div class="personal-block solo"> <div class="personal-block solo">

View File

@ -1493,4 +1493,14 @@ class Application_Model_Preference
{ {
return self::getValue("tunein_station_id"); return self::getValue("tunein_station_id");
} }
public static function geLastTuneinMetadataUpdate()
{
return self::getValue("last_tunein_metadata_update");
}
public static function setLastTuneinMetadataUpdate($value)
{
self::setValue("last_tunein_metadata_update", $value);
}
} }

View File

@ -98,10 +98,8 @@ SQL;
$utcNow = new DateTime("now", new DateTimeZone("UTC")); $utcNow = new DateTime("now", new DateTimeZone("UTC"));
$shows = Application_Model_Show::getPrevCurrentNext($utcNow, $utcTimeEnd, $showsToRetrieve); $shows = Application_Model_Show::getPrevCurrentNext($utcNow, $utcTimeEnd, $showsToRetrieve);
$previousShowID = count($shows['previousShow'])>0?$shows['previousShow'][0]['instance_id']:null;
$currentShowID = count($shows['currentShow'])>0?$shows['currentShow']['instance_id']:null; $currentShowID = count($shows['currentShow'])>0?$shows['currentShow']['instance_id']:null;
$nextShowID = count($shows['nextShow'])>0?$shows['nextShow'][0]['instance_id']:null; $results = Application_Model_Schedule::getPreviousCurrentNextMedia($utcNow, $currentShowID);
$results = self::GetPrevCurrentNext($previousShowID, $currentShowID, $nextShowID, $utcNow);
$range = array( $range = array(
"station" => array ( "station" => array (
@ -136,10 +134,8 @@ SQL;
$utcNow = new DateTime("now", new DateTimeZone("UTC")); $utcNow = new DateTime("now", new DateTimeZone("UTC"));
$shows = Application_Model_Show::getPrevCurrentNextOld($utcNow); $shows = Application_Model_Show::getPrevCurrentNextOld($utcNow);
$previousShowID = count($shows['previousShow'])>0?$shows['previousShow'][0]['instance_id']:null;
$currentShowID = count($shows['currentShow'])>0?$shows['currentShow'][0]['instance_id']:null; $currentShowID = count($shows['currentShow'])>0?$shows['currentShow'][0]['instance_id']:null;
$nextShowID = count($shows['nextShow'])>0?$shows['nextShow'][0]['instance_id']:null; $results = Application_Model_Schedule::getPreviousCurrentNextMedia($utcNow, $currentShowID);
$results = self::GetPrevCurrentNext($previousShowID, $currentShowID, $nextShowID, $utcNow);
$range = array( $range = array(
"env" => APPLICATION_ENV, "env" => APPLICATION_ENV,
@ -157,129 +153,158 @@ SQL;
} }
/** /**
* Queries the database for the set of schedules one hour before * Attempts to find a media item (track or webstream) that is currently playing.
* and after the given time. If a show starts and ends within that * If a current media item is currently playing, this function then attempts to
* time that is considered the current show. Then the scheduled item * find an item that played previously and is scheduled to play next.
* before it is the previous show, and the scheduled item after it *
* is the next show. This way the dashboard getCurrentPlaylist is * @param $utcNow DateTime current time in UTC
* very fast. But if any one of the three show types are not found * @param $currentShowInstanceId cc_show_instance id of the show instance currently playing
* through this mechanism a call is made to the old way of querying * @return array with data about the previous, current, and next media items playing.
* the database to find the track info. * Returns an empty arrays if there is no media item currently playing
**/ */
public static function GetPrevCurrentNext($p_previousShowID, $p_currentShowID, $p_nextShowID, $utcNow) public static function getPreviousCurrentNextMedia($utcNow, $currentShowInstanceId)
{ {
$timeZone = new DateTimeZone("UTC"); //This function works entirely in UTC. $timeZone = new DateTimeZone("UTC"); //This function works entirely in UTC.
assert(get_class($utcNow) === "DateTime"); assert(get_class($utcNow) === "DateTime");
assert($utcNow->getTimeZone() == $timeZone); assert($utcNow->getTimeZone() == $timeZone);
if ($p_previousShowID == null && $p_currentShowID == null && $p_nextShowID == null) {
return;
}
$sql = "SELECT %%columns%% st.starts as starts, st.ends as ends,
st.media_item_played as media_item_played, si.ends as show_ends
%%tables%% WHERE ";
$fileColumns = "ft.artist_name, ft.track_title, ";
$fileJoin = "FROM cc_schedule st JOIN cc_files ft ON st.file_id = ft.id
LEFT JOIN cc_show_instances si ON st.instance_id = si.id";
$streamColumns = "ws.name AS artist_name, wm.liquidsoap_data AS track_title, ";
$streamJoin = <<<SQL
FROM cc_schedule AS st
JOIN cc_webstream ws ON st.stream_id = ws.id
LEFT JOIN cc_show_instances AS si ON st.instance_id = si.id
LEFT JOIN cc_subjs AS sub ON sub.id = ws.creator_id
LEFT JOIN
(SELECT *
FROM cc_webstream_metadata
ORDER BY start_time DESC LIMIT 1) AS wm ON st.id = wm.instance_id
SQL;
$predicateArr = array();
$paramMap = array();
if (isset($p_previousShowID)) {
$predicateArr[] = 'st.instance_id = :previousShowId';
$paramMap[':previousShowId'] = $p_previousShowID;
}
if (isset($p_currentShowID)) {
$predicateArr[] = 'st.instance_id = :currentShowId';
$paramMap[':currentShowId'] = $p_currentShowID;
}
if (isset($p_nextShowID)) {
$predicateArr[] = 'st.instance_id = :nextShowId';
$paramMap[':nextShowId'] = $p_nextShowID;
}
$sql .= " (".implode(" OR ", $predicateArr).") ";
$sql .= ' AND st.playout_status > 0 ORDER BY st.starts';
$filesSql = str_replace("%%columns%%", $fileColumns, $sql);
$filesSql = str_replace("%%tables%%", $fileJoin, $filesSql);
$streamSql = str_replace("%%columns%%", $streamColumns, $sql);
$streamSql = str_replace("%%tables%%", $streamJoin, $streamSql);
$sql = "SELECT * FROM (($filesSql) UNION ($streamSql)) AS unioned ORDER BY starts";
$rows = Application_Common_Database::prepareAndExecute($sql, $paramMap);
$numberOfRows = count($rows);
$results['previous'] = null; $results['previous'] = null;
$results['current'] = null; $results['current'] = null;
$results['next'] = null; $results['next'] = null;
for ($i = 0; $i < $numberOfRows; ++$i) { $sql = "select s.id, s.starts, s.ends, s.file_id, s.stream_id, s.media_item_played,
s.instance_id, si.ends as show_ends from cc_schedule s left join cc_show_instances si
on s.instance_id = si.id where s.playout_status > 0 and s.starts <= :p1
and s.ends >= :p2 and s.instance_id = :p3 order by starts desc limit 1";
// if the show is overbooked, then update the track end time to the end of the show time. $params = array(
if ($rows[$i]['ends'] > $rows[$i]["show_ends"]) { ":p1" => $utcNow->format("Y-m-d H:i:s"),
$rows[$i]['ends'] = $rows[$i]["show_ends"]; ":p2" => $utcNow->format("Y-m-d H:i:s"),
":p3" => $currentShowInstanceId
);
$rows = Application_Common_Database::prepareAndExecute($sql, $params);
if (count($rows) < 1) {
return $results;
} }
$curShowStartTime = new DateTime($rows[$i]['starts'], $timeZone); if ($rows[0]["show_ends"] < $utcNow->format("Y-m-d H:i:s")) {
$curShowEndTime = new DateTime($rows[$i]['ends'], $timeZone); return $results;
}
if (($curShowStartTime <= $utcNow) && ($curShowEndTime >= $utcNow)) { $currentMedia = $rows[0];
if ($i - 1 >= 0) {
$results['previous'] = array("name"=>$rows[$i-1]["artist_name"]." - ".$rows[$i-1]["track_title"], if ($currentMedia["ends"] > $currentMedia["show_ends"]) {
"starts"=>$rows[$i-1]["starts"], $currentMedia["ends"] = $currentMedia["show_ends"];
"ends"=>$rows[$i-1]["ends"],
"type"=>'track');
} }
$results['current'] = array("name"=>$rows[$i]["artist_name"]." - ".$rows[$i]["track_title"],
"starts"=>$rows[$i]["starts"], $currentMediaScheduleId = $currentMedia["id"];
"ends"=> (($rows[$i]["ends"] > $rows[$i]["show_ends"]) ? $rows[$i]["show_ends"]: $rows[$i]["ends"]), $currentMediaFileId = $currentMedia["file_id"];
"media_item_played"=>$rows[$i]["media_item_played"], $currentMediaStreamId = $currentMedia["stream_id"];
"record"=>0, if (isset($currentMediaFileId)) {
"type"=>'track'); $currentMediaType = "track";
if (isset($rows[$i+1])) { $currentFile = CcFilesQuery::create()
$results['next'] = array("name"=>$rows[$i+1]["artist_name"]." - ".$rows[$i+1]["track_title"], ->filterByDbId($currentMediaFileId)
"starts"=>$rows[$i+1]["starts"], ->findOne();
"ends"=>$rows[$i+1]["ends"], $currentMediaName = $currentFile->getDbArtistName() . " - " . $currentFile->getDbTrackTitle();
"type"=>'track'); } else if (isset($currentMediaStreamId)) {
$currentMediaType = "webstream";
$currentWebstream = CcWebstreamQuery::create()
->filterByDbId($currentMediaStreamId)
->findOne();
$currentWebstreamMetadata = CcWebstreamMetadataQuery::create()
->filterByDbInstanceId($currentMedia["instance_id"])
->orderByDbStartTime(Criteria::DESC)
->findOne();
$currentMediaName = $currentWebstream->getDbName();
if (isset($currentWebstreamMetadata)) {
$currentMediaName .= " - " . $currentWebstreamMetadata->getDbLiquidsoapData();
} }
break; } else {
$currentMediaType = null;
} }
if ($curShowEndTime < $utcNow ) { $results["current"] = array(
$previousIndex = $i; "starts" => $currentMedia["starts"],
"ends" => $currentMedia["ends"],
"type" => $currentMediaType,
"name" => $currentMediaName,
"media_item_played" => $currentMedia["media_item_played"],
"record" => "0"
);
$previousMedia = CcScheduleQuery::create()
->filterByDbId($currentMediaScheduleId-1)
->filterByDbPlayoutStatus(0, Criteria::GREATER_THAN)
->orderByDbStarts()
->findOne();
if (isset($previousMedia)) {
$previousMediaFileId = $previousMedia->getDbFileId();
$previousMediaStreamId = $previousMedia->getDbStreamId();
if (isset($previousMediaFileId)) {
$previousMediaType = "track";
$previousFile = CcFilesQuery::create()
->filterByDbId($previousMediaFileId)
->findOne();
$previousMediaName = $previousFile->getDbArtistName() . " - " . $previousFile->getDbTrackTitle();
} else if (isset($previousMediaStreamId)) {
$previousMediaName = null;
$previousMediaType = "webstream";
$previousWebstream = CcWebstreamQuery::create()
->filterByDbId($previousMediaStreamId)
->findOne();
/*$previousWebstreamMetadata = CcWebstreamMetadataQuery::create()
->filterByDbInstanceId($previousMedia->getDbInstanceId())
->orderByDbStartTime(Criteria::DESC)
->findOne();*/
$previousMediaName = $previousWebstream->getDbName();
} else {
$previousMediaType = null;
} }
if ($curShowStartTime > $utcNow) { $results["previous"] = array(
$results['next'] = array("name"=>$rows[$i]["artist_name"]." - ".$rows[$i]["track_title"], "starts" => $previousMedia->getDbStarts(),
"starts"=>$rows[$i]["starts"], "ends" => $previousMedia->getDbEnds(),
"ends"=>$rows[$i]["ends"], "type" => $previousMediaType,
"type"=>'track'); "name" => $previousMediaName
break; );
} }
$nextMedia = CcScheduleQuery::create()
->filterByDbId($currentMediaScheduleId+1)
->orderByDbStarts()
->findOne();
if (isset($nextMedia)) {
$nextMediaFileId = $nextMedia->getDbFileId();
$nextMediaStreamId = $nextMedia->getDbStreamId();
if (isset($nextMediaFileId)) {
$nextMediaType = "track";
$nextFile = CcFilesQuery::create()
->filterByDbId($nextMediaFileId)
->findOne();
$nextMediaName = $nextFile->getDbArtistName() . " - " . $nextFile->getDbTrackTitle();
} else if (isset($nextMediaStreamId)) {
$nextMediaType = "webstream";
$nextWebstream = CcWebstreamQuery::create()
->filterByDbId($nextMediaStreamId)
->findOne();
/*$nextWebstreamMetadata = CcWebstreamMetadataQuery::create()
->filterByDbInstanceId($nextMedia->getDbInstanceId())
->orderByDbStartTime(Criteria::DESC)
->findOne();*/
$nextMediaName = $nextWebstream->getDbName();
} else {
$nextMediaType = null;
} }
//If we didn't find a a current show because the time didn't fit we may still have $results["next"] = array(
//found a previous show so use it. "starts" => $nextMedia->getDbStarts(),
if ($results['previous'] === null && isset($previousIndex)) { "ends" => $nextMedia->getDbEnds(),
$results['previous'] = array("name"=>$rows[$previousIndex]["artist_name"]." - ".$rows[$previousIndex]["track_title"], "type" => $nextMediaType,
"starts"=>$rows[$previousIndex]["starts"], "name" => $nextMediaName
"ends"=>$rows[$previousIndex]["ends"]);; );
} }
return $results; return $results;
} }
public static function GetLastScheduleItem($p_timeNow) public static function GetLastScheduleItem($p_timeNow)

View File

@ -1012,10 +1012,10 @@ class Application_Model_Scheduler
*/ */
public function moveItem($selectedItems, $afterItems, $adjustSched = true) public function moveItem($selectedItems, $afterItems, $adjustSched = true)
{ {
$startProfile = microtime(true); //$startProfile = microtime(true);
$this->con->beginTransaction(); $this->con->beginTransaction();
$this->con->useDebug(true); //$this->con->useDebug(true);
try { try {
@ -1024,8 +1024,8 @@ class Application_Model_Scheduler
$this->validateRequest($afterItems); $this->validateRequest($afterItems);
$endProfile = microtime(true); $endProfile = microtime(true);
Logging::debug("validating move request took:"); //Logging::debug("validating move request took:");
Logging::debug(floatval($endProfile) - floatval($startProfile)); //Logging::debug(floatval($endProfile) - floatval($startProfile));
$afterInstance = CcShowInstancesQuery::create()->findPK($afterItems[0]["instance"], $this->con); $afterInstance = CcShowInstancesQuery::create()->findPK($afterItems[0]["instance"], $this->con);
@ -1066,18 +1066,18 @@ class Application_Model_Scheduler
$this->removeGaps($instance, $schedIds); $this->removeGaps($instance, $schedIds);
$endProfile = microtime(true); //$endProfile = microtime(true);
Logging::debug("removing gaps from instance $instance:"); //Logging::debug("removing gaps from instance $instance:");
Logging::debug(floatval($endProfile) - floatval($startProfile)); //Logging::debug(floatval($endProfile) - floatval($startProfile));
} }
$startProfile = microtime(true); //$startProfile = microtime(true);
$this->insertAfter($afterItems, null, $movedData, $adjustSched, true); $this->insertAfter($afterItems, null, $movedData, $adjustSched, true);
$endProfile = microtime(true); //$endProfile = microtime(true);
Logging::debug("inserting after removing gaps."); //Logging::debug("inserting after removing gaps.");
Logging::debug(floatval($endProfile) - floatval($startProfile)); //Logging::debug(floatval($endProfile) - floatval($startProfile));
$modified = array_keys($modifiedMap); $modified = array_keys($modifiedMap);
//need to adjust shows we have moved items from. //need to adjust shows we have moved items from.
@ -1087,7 +1087,7 @@ class Application_Model_Scheduler
$instance->updateScheduleStatus($this->con); $instance->updateScheduleStatus($this->con);
} }
$this->con->useDebug(false); //$this->con->useDebug(false);
$this->con->commit(); $this->con->commit();
Application_Model_RabbitMq::PushSchedule(); Application_Model_RabbitMq::PushSchedule();

View File

@ -974,10 +974,7 @@ SQL;
foreach ($shows as &$show) { foreach ($shows as &$show) {
$options = array(); $options = array();
//only bother calculating percent for week or day view.
if (intval($days) <= 7) {
$options["percent"] = Application_Model_Show::getPercentScheduled($show["starts"], $show["ends"], $show["time_filled"]); $options["percent"] = Application_Model_Show::getPercentScheduled($show["starts"], $show["ends"], $show["time_filled"]);
}
if (isset($show["parent_starts"])) { if (isset($show["parent_starts"])) {
$parentStartsDT = new DateTime($show["parent_starts"], $utcTimezone); $parentStartsDT = new DateTime($show["parent_starts"], $utcTimezone);
@ -1432,39 +1429,70 @@ SQL;
} }
public static function getStartEndCurrentMonthView() { public static function getStartEndCurrentMonthView() {
$first_day_of_calendar_month_view = mktime(0, 0, 0, date("n"), 1);
$weekStart = Application_Model_Preference::GetWeekStartDay();
while (date('w', $first_day_of_calendar_month_view) != $weekStart) {
$first_day_of_calendar_month_view -= 60*60*24;
}
$last_day_of_calendar_view = $first_day_of_calendar_month_view + 3600*24*42;
$start = new DateTime("@".$first_day_of_calendar_month_view); $utcTimeZone = new DateTimeZone("UTC");
$end = new DateTime("@".$last_day_of_calendar_view);
//We have to get the start of the day in the user's timezone, and then convert that to UTC.
$start = new DateTime("first day of this month", new DateTimeZone(Application_Model_Preference::GetUserTimezone()));
$start->setTimezone($utcTimeZone); //Covert it to UTC.
$monthInterval = new DateInterval("P1M");
$end = clone($start);
$end->add($monthInterval);
return array($start, $end); return array($start, $end);
} }
public static function getStartEndCurrentWeekView() { /** Returns the start and end date that FullCalendar will display for today's month.
$first_day_of_calendar_week_view = mktime(0, 0, 0, date("n"), date("j")); *
$weekStart = Application_Model_Preference::GetWeekStartDay(); * FullCalendar displays 6 weeks, starting on a Sunday, for a total of 42 days. This function returns 42 days worth
while (date('w', $first_day_of_calendar_week_view) != $weekStart) { * of data (a few days before, and a few days after.)
$first_day_of_calendar_week_view -= 60*60*24; */
public static function getStartEndCurrentMonthPlusView() {
$utcTimeZone = new DateTimeZone("UTC");
//We have to get the start of the day in the user's timezone, and then convert that to UTC.
$start = new DateTime("first day of this month", new DateTimeZone(Application_Model_Preference::GetUserTimezone()));
$dayOfWeekNumeric = $start->format('w');
$start->sub(new DateInterval("P{$dayOfWeekNumeric}D")); //Subtract the index of the day of the week the month starts on. (adds this many days from the previous month)
$start->setTimezone($utcTimeZone); //Covert it to UTC.
$fullCalendarMonthInterval = new DateInterval("P42D"); //42 days
$end = clone($start);
$end->add($fullCalendarMonthInterval);
return array($start, $end);
} }
$last_day_of_calendar_view = $first_day_of_calendar_week_view + 3600*24*7;
$start = new DateTime("@".$first_day_of_calendar_week_view);
$end = new DateTime("@".$last_day_of_calendar_view);
public static function getStartEndCurrentWeekView() {
$weekStartDayNum = Application_Model_Preference::GetWeekStartDay();
$utcTimeZone = new DateTimeZone("UTC");
//We have to get the start of the week in the user's timezone, and then convert that to UTC.
$start = new DateTime("Sunday last week", new DateTimeZone(Application_Model_Preference::GetUserTimezone()));
$start->add(new DateInterval("P{$weekStartDayNum}D")); //Shift the start date to the station's "Week Starts on Day"
$start->setTimezone($utcTimeZone); //Covert it to UTC.
$weekInterval = new DateInterval("P1W");
$end = clone($start);
$end->add($weekInterval);
return array($start, $end); return array($start, $end);
} }
public static function getStartEndCurrentDayView() { public static function getStartEndCurrentDayView() {
$today = mktime(0, 0, 0, date("n"), date("j")); $utcTimeZone = new DateTimeZone("UTC");
$tomorrow = $today + 3600*24;
$start = new DateTime("@".$today); //We have to get the start of the day in the user's timezone, and then convert that to UTC.
$end = new DateTime("@".$tomorrow); $start = new DateTime("today", new DateTimeZone(Application_Model_Preference::GetUserTimezone()));
$start->setTimezone($utcTimeZone); //Covert it to UTC.
$dayInterval = new DateInterval("P1D");
$end = clone($start);
$end->add($dayInterval);
return array($start, $end); return array($start, $end);
} }

View File

@ -1,31 +1,6 @@
<table width="60%" cellpadding="0" cellspacing="0" border="0" class="statustable"> <table width="60%" cellpadding="0" cellspacing="0" border="0" class="statustable">
<thead>
<tr class="ui-state-default strong">
<td><?php echo _("Service") ?></td>
<td><?php echo _("Status") ?></td>
<td><?php echo _("Uptime") ?></td>
<td><?php echo _("CPU") ?></td>
<td><?php echo _("Memory") ?></td>
</tr>
</thead>
<tbody> <tbody>
<!--
<tr class="odd">
<td><?php echo sprintf(_("%s Version"), PRODUCT_NAME) ?></td>
<td>1.9.3</td>
<td>&nbsp;</td>
</tr>
-->
<?php $i=0; ?>
<?php foreach($this->status->services as $key=>$value): ?>
<tr class="<?php echo ($i&1) == 0 ? "even":"odd"; $i++; ?>" id="<?php echo $value["name"]; ?>">
<td><?php echo $value["name"]; ?></td>
<td><span></span></td>
<td></td>
<td></td>
<td></td>
</tr>
<?php endforeach; ?>
<tr id="partitions" class="even"> <tr id="partitions" class="even">
<th colspan="5"><?php echo _("Disk Space") ?></th> <th colspan="5"><?php echo _("Disk Space") ?></th>
</tr> </tr>

View File

@ -395,7 +395,6 @@ function getScheduleFromServer(){
parseSourceStatus(data.source_status); parseSourceStatus(data.source_status);
parseSwitchStatus(data.switch_status); parseSwitchStatus(data.switch_status);
showName = data.show_name; showName = data.show_name;
setTimeout(getScheduleFromServer, serverUpdateInterval);
}, error:function(jqXHR, textStatus, errorThrown){}}); }, error:function(jqXHR, textStatus, errorThrown){}});
} }
@ -456,7 +455,7 @@ var stream_window = null;
function init() { function init() {
//begin producer "thread" //begin producer "thread"
getScheduleFromServer(); setInterval(getScheduleFromServer, serverUpdateInterval);
//begin consumer "thread" //begin consumer "thread"
secondsTimer(); secondsTimer();

View File

@ -213,3 +213,21 @@ function resizeToMaxHeight(ele, targetHeight){
img.css("width", newWidth+"px"); img.css("width", newWidth+"px");
} }
} }
/* From http://stackoverflow.com/questions/123999/how-to-tell-if-a-dom-element-is-visible-in-the-current-viewport/#7557433 */
function isInView(el) {
//special bonus for those using jQuery
if (typeof jQuery === "function" && el instanceof jQuery) {
el = el[0];
}
var rect = el.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && /*or $(window).height() */
rect.right <= (window.innerWidth || document.documentElement.clientWidth) /*or $(window).width() */
);
}

View File

@ -8,20 +8,31 @@ var AIRTIME = (function(AIRTIME) {
mod = AIRTIME.library; mod = AIRTIME.library;
mod.checkAddButton = function() { mod.checkAddButton = function() {
var selected = mod.getChosenItemsLength(), $cursor = $('tr.cursor-selected-row'), check = false; var selected = mod.getChosenItemsLength(), $cursor = $('tr.sb-selected'), check = false,
shows = $('tr.sb-header'), current = $('tr.sb-current-show'),
cursorText = $.i18n._('Add to next show');
// make sure library items are selected and a cursor is selected. // make sure library items are selected and a cursor is selected.
if (selected !== 0 && $cursor.length !== 0) { if (selected !== 0) {
check = true; check = true;
} }
if (shows.length === 0) {
check = false;
}
if (check === true) { if (check === true) {
AIRTIME.button.enableButton("btn-group #library-plus", false); AIRTIME.button.enableButton("btn-group #library-plus", false);
} else { } else {
AIRTIME.button.disableButton("btn-group #library-plus", false); AIRTIME.button.disableButton("btn-group #library-plus", false);
} }
AIRTIME.library.changeAddButtonText($('.btn-group #library-plus #lib-plus-text'), ' '+$.i18n._('Add to selected show')); if ($cursor.length !== 0) {
cursorText = $.i18n._('Add before selected items');
} else if (current.length !== 0) {
cursorText = $.i18n._('Add to current show');
}
AIRTIME.library.changeAddButtonText($('.btn-group #library-plus #lib-plus-text'), ' '+ cursorText);
}; };
mod.fnRowCallback = function(nRow, aData, iDisplayIndex, iDisplayIndexFull) { mod.fnRowCallback = function(nRow, aData, iDisplayIndex, iDisplayIndexFull) {
@ -98,7 +109,7 @@ var AIRTIME = (function(AIRTIME) {
"type" : type "type" : type
}); });
$("#show_builder_table tr.cursor-selected-row").each(function(i, el) { $("#show_builder_table tr.sb-selected").each(function(i, el) {
aData.push($(el).prev().data("aData")); aData.push($(el).prev().data("aData"));
}); });
@ -113,12 +124,35 @@ var AIRTIME = (function(AIRTIME) {
} }
if (aSchedIds.length == 0) { if (aSchedIds.length == 0) {
alert($.i18n._("Please select a cursor position on timeline.")); addToCurrentOrNext(aSchedIds);
return false;
} }
AIRTIME.showbuilder.fnAdd(aMediaIds, aSchedIds); AIRTIME.showbuilder.fnAdd(aMediaIds, aSchedIds);
}; };
function addToCurrentOrNext(arr) {
var el;
// Get the show instance id of the first non-data row (id = 0)
// The second last row in the table with that instance id is the
// last schedule item for the first show. (This is important for
// the Now Playing screen if multiple shows are in view).
el = $("[si_id="+$("#0").attr("si_id")+"]");
var temp = el.eq(-2).data("aData");
arr.push({
"id" : temp.id,
"instance" : temp.instance,
"timestamp" : temp.timestamp
});
if (!isInView(el)) {
$('.dataTables_scrolling.sb-padded').animate({
scrollTop: el.offset().top
}, 0);
}
return arr;
}
mod.setupLibraryToolbar = function() { mod.setupLibraryToolbar = function() {
var $toolbar = $(".lib-content .fg-toolbar:first"); var $toolbar = $(".lib-content .fg-toolbar:first");
@ -146,7 +180,7 @@ var AIRTIME = (function(AIRTIME) {
}); });
} }
$("#show_builder_table tr.cursor-selected-row") $("#show_builder_table tr.sb-selected")
.each(function(i, el) { .each(function(i, el) {
aData.push($(el).prev().data("aData")); aData.push($(el).prev().data("aData"));
}); });
@ -162,6 +196,10 @@ var AIRTIME = (function(AIRTIME) {
}); });
} }
if (aSchedIds.length == 0) {
addToCurrentOrNext(aSchedIds);
}
AIRTIME.showbuilder.fnAdd(aMediaIds, aSchedIds); AIRTIME.showbuilder.fnAdd(aMediaIds, aSchedIds);
}); });

View File

@ -109,7 +109,7 @@ var AIRTIME = (function(AIRTIME) {
mod.changeAddButtonText = function($button, btnText) { mod.changeAddButtonText = function($button, btnText) {
$button.text(btnText); $button.text(btnText);
} };
mod.createToolbarButtons = function() { mod.createToolbarButtons = function() {
$menu = $("<div class='btn-toolbar' />"); $menu = $("<div class='btn-toolbar' />");
@ -135,7 +135,7 @@ var AIRTIME = (function(AIRTIME) {
"<i class='icon-white icon-trash'></i>" + "<i class='icon-white icon-trash'></i>" +
"</button>" + "</button>" +
"</div>"); "</div>");
} };
mod.createToolbarDropDown = function() { mod.createToolbarDropDown = function() {
$('#sb-select-page').click(function(){mod.selectCurrentPage();}); $('#sb-select-page').click(function(){mod.selectCurrentPage();});
@ -530,12 +530,14 @@ var AIRTIME = (function(AIRTIME) {
}, },
"fnStateSave": function (oSettings, oData) { "fnStateSave": function (oSettings, oData) {
localStorage.setItem('datatables-library', JSON.stringify(oData)); localStorage.setItem('datatables-library', JSON.stringify(oData));
/*
$.ajax({ $.ajax({
url: baseUrl+"usersettings/set-library-datatable", url: baseUrl+"usersettings/set-library-datatable",
type: "POST", type: "POST",
data: {settings : oData, format: "json"}, data: {settings : oData, format: "json"},
dataType: "json" dataType: "json"
}); });
*/
colReorderMap = oData.ColReorder; colReorderMap = oData.ColReorder;
}, },
@ -890,7 +892,6 @@ var AIRTIME = (function(AIRTIME) {
} }
}); });
checkImportStatus();
checkLibrarySCUploadStatus(); checkLibrarySCUploadStatus();
addQtipToSCIcons(); addQtipToSCIcons();

View File

@ -360,13 +360,7 @@ function windowResize() {
} }
function preloadEventFeed () { function preloadEventFeed () {
var url = baseUrl+'Schedule/event-feed-preload';
var d = new Date();
$.post(url, {format: "json", cachep: d.getTime()}, function(json){
calendarEvents = json.events;
createFullCalendar({calendarInit: calendarPref}); createFullCalendar({calendarInit: calendarPref});
});
} }
var initialLoad = true; var initialLoad = true;

View File

@ -312,7 +312,7 @@ function createFullCalendar(data){
], ],
contentHeight: mainHeight, contentHeight: mainHeight,
theme: true, theme: true,
lazyFetching: false, lazyFetching: true,
serverTimestamp: parseInt(data.calendarInit.timestamp, 10), serverTimestamp: parseInt(data.calendarInit.timestamp, 10),
serverTimezoneOffset: parseInt(data.calendarInit.timezoneOffset, 10), serverTimezoneOffset: parseInt(data.calendarInit.timezoneOffset, 10),

View File

@ -278,7 +278,8 @@ var AIRTIME = (function(AIRTIME){
oSchedTable.fnDraw(); oSchedTable.fnDraw();
mod.enableUI(); mod.enableUI();
$("#library_content").find("#library_display").dataTable().fnStandingRedraw(); //Unneccessary reload of the library pane after moving tracks in the showbuilder pane.
//$("#library_content").find("#library_display").dataTable().fnStandingRedraw();
}; };
mod.getSelectedCursors = function() { mod.getSelectedCursors = function() {
@ -405,6 +406,13 @@ var AIRTIME = (function(AIRTIME){
$sbTable = $sbContent.find('table'); $sbTable = $sbContent.find('table');
var isInitialized = false; var isInitialized = false;
var emptyNode = document.createElement('div');
var lockedPreviewIcon = document.createElement('span');
lockedPreviewIcon.setAttribute('class', 'ui-icon ui-icon-locked');
var previewIcon = document.createElement('img');
previewIcon.setAttribute('src', baseUrl+'css/images/icon_audioclip.png');
previewIcon.setAttribute('title', $.i18n._("Track preview"));
oSchedTable = $sbTable.dataTable( { oSchedTable = $sbTable.dataTable( {
"aoColumns": [ "aoColumns": [
/* checkbox */ {"mDataProp": "allowed", "sTitle": "", "sWidth": "15px", "sClass": "sb-checkbox"}, /* checkbox */ {"mDataProp": "allowed", "sTitle": "", "sWidth": "15px", "sClass": "sb-checkbox"},
@ -429,6 +437,7 @@ var AIRTIME = (function(AIRTIME){
"bServerSide": true, "bServerSide": true,
"bInfo": false, "bInfo": false,
"bAutoWidth": false, "bAutoWidth": false,
"bDeferRender": true,
"bStateSave": true, "bStateSave": true,
"fnStateSaveParams": function (oSettings, oData) { "fnStateSaveParams": function (oSettings, oData) {
@ -440,12 +449,13 @@ var AIRTIME = (function(AIRTIME){
localStorage.setItem('datatables-timeline', JSON.stringify(oData)); localStorage.setItem('datatables-timeline', JSON.stringify(oData));
/*
$.ajax({ $.ajax({
url: baseUrl+"usersettings/set-timeline-datatable", url: baseUrl+"usersettings/set-timeline-datatable",
type: "POST", type: "POST",
data: {settings : oData, format: "json"}, data: {settings : oData, format: "json"},
dataType: "json" dataType: "json"
}); });*/
}, },
"fnStateLoad": function fnBuilderStateLoad(oSettings) { "fnStateLoad": function fnBuilderStateLoad(oSettings) {
var settings = localStorage.getItem('datatables-timeline'); var settings = localStorage.getItem('datatables-timeline');
@ -467,6 +477,7 @@ var AIRTIME = (function(AIRTIME){
} }
} }
/*
a = oData.ColReorder; a = oData.ColReorder;
if (a) { if (a) {
for (i = 0, length = a.length; i < length; i++) { for (i = 0, length = a.length; i < length; i++) {
@ -474,7 +485,7 @@ var AIRTIME = (function(AIRTIME){
a[i] = parseInt(a[i], 10); a[i] = parseInt(a[i], 10);
} }
} }
} }*/
oData.iCreate = parseInt(oData.iCreate, 10); oData.iCreate = parseInt(oData.iCreate, 10);
}, },
@ -494,8 +505,13 @@ var AIRTIME = (function(AIRTIME){
headerIcon; headerIcon;
fnPrepareSeparatorRow = function fnPrepareSeparatorRow(sRowContent, sClass, iNodeIndex) { fnPrepareSeparatorRow = function fnPrepareSeparatorRow(sRowContent, sClass, iNodeIndex) {
//Albert:
//$(nRow.children[iNodeIndex]).replaceWith(emptyNode);
$node = $(nRow.children[iNodeIndex]); $node = $(nRow.children[iNodeIndex]);
$node.html(sRowContent); $node.html(sRowContent);
$node.attr('colspan',100); $node.attr('colspan',100);
for (i = iNodeIndex + 1, length = nRow.children.length; i < length; i = i+1) { for (i = iNodeIndex + 1, length = nRow.children.length; i < length; i = i+1) {
$node = $(nRow.children[i]); $node = $(nRow.children[i]);
@ -504,6 +520,7 @@ var AIRTIME = (function(AIRTIME){
} }
$nRow.addClass(sClass); $nRow.addClass(sClass);
}; };
if (aData.header === true) { if (aData.header === true) {
@ -576,7 +593,9 @@ var AIRTIME = (function(AIRTIME){
$nRow.find('td').removeClass(); $nRow.find('td').removeClass();
$node = $(nRow.children[0]); $node = $(nRow.children[0]);
$node.html(''); if ($node) {
$node.empty();
}
sSeparatorHTML = '<span>'+$.i18n._("Show Empty")+'</span>'; sSeparatorHTML = '<span>'+$.i18n._("Show Empty")+'</span>';
cl = cl + " sb-empty odd"; cl = cl + " sb-empty odd";
@ -592,24 +611,32 @@ var AIRTIME = (function(AIRTIME){
sSeparatorHTML = '<span>'+$.i18n._("Recording From Line In")+'</span>'; sSeparatorHTML = '<span>'+$.i18n._("Recording From Line In")+'</span>';
cl = cl + " sb-record odd"; cl = cl + " sb-record odd";
fnPrepareSeparatorRow(sSeparatorHTML, cl, 1); fnPrepareSeparatorRow(sSeparatorHTML, cl, 1);
} }
else { else {
//add the play function if the file exists on disk. //add the play function if the file exists on disk.
$image = $nRow.find('td.sb-image'); $image = $nRow.find('td.sb-image');
$image.empty();
//check if the file exists. //check if the file exists.
if (aData.image === true) { if (aData.image === true) {
$nRow.addClass("lib-audio"); $nRow.addClass("lib-audio");
if (!isAudioSupported(aData.mime)) { if (!isAudioSupported(aData.mime)) {
$image.html('<span class="ui-icon ui-icon-locked"></span>'); //$image.html('<span class="ui-icon ui-icon-locked"></span>');
$image.append(lockedPreviewIcon);
} else { } else {
/*
$image.html('<img title="'+$.i18n._("Track preview")+'" src="'+baseUrl+'css/images/icon_audioclip.png"></img>') $image.html('<img title="'+$.i18n._("Track preview")+'" src="'+baseUrl+'css/images/icon_audioclip.png"></img>')
.click(function() { .click(function() {
open_show_preview(aData.instance, aData.pos); open_show_preview(aData.instance, aData.pos);
return false; return false;
});*/
$image.append(previewIcon.cloneNode(false));
$image.click(function() {
open_show_preview(aData.instance, aData.pos);
return false;
}); });
} }
} }
else { else {
@ -631,7 +658,7 @@ var AIRTIME = (function(AIRTIME){
$node.html('<input type="checkbox" name="'+aData.id+'"></input>'); $node.html('<input type="checkbox" name="'+aData.id+'"></input>');
} }
else { else {
$node.html(''); $node.empty();
} }
} }
@ -705,7 +732,6 @@ var AIRTIME = (function(AIRTIME){
$("#draggingContainer").remove(); $("#draggingContainer").remove();
}, },
"fnDrawCallback": function fnBuilderDrawCallback(oSettings, json) { "fnDrawCallback": function fnBuilderDrawCallback(oSettings, json) {
var isInitialized = false;
if (!isInitialized) { if (!isInitialized) {
//when coming to 'Now Playing' page we want the page //when coming to 'Now Playing' page we want the page
@ -731,6 +757,7 @@ var AIRTIME = (function(AIRTIME){
clearTimeout(mod.timeout); clearTimeout(mod.timeout);
/*
//only create the cursor arrows if the library is on the page. //only create the cursor arrows if the library is on the page.
if ($lib.length > 0 && $lib.filter(":visible").length > 0) { if ($lib.length > 0 && $lib.filter(":visible").length > 0) {
@ -775,18 +802,18 @@ var AIRTIME = (function(AIRTIME){
$tr = $table.find("tr[id="+cursorIds[i]+"][si_id="+showInstanceIds[i]+"]"); $tr = $table.find("tr[id="+cursorIds[i]+"][si_id="+showInstanceIds[i]+"]");
} }
/* If the currently playing track's cursor is selected, //If the currently playing track's cursor is selected,
* and that track is deleted, the cursor position becomes //and that track is deleted, the cursor position becomes
* unavailble. We have to check the position is available //unavailble. We have to check the position is available
* before re-highlighting it. // before re-highlighting it.
*/ //
if ($tr.find(".sb-checkbox").children().hasClass("innerWrapper")) { if ($tr.find(".sb-checkbox").children().hasClass("innerWrapper")) {
mod.selectCursor($tr); mod.selectCursor($tr);
/* If the selected cursor is the footer row we need to // If the selected cursor is the footer row we need to
* explicitly select it because that row does not have //explicitly select it because that row does not have
* innerWrapper class // innerWrapper class
*/ //
} else if ($tr.hasClass("sb-footer")) { } else if ($tr.hasClass("sb-footer")) {
mod.selectCursor($tr); mod.selectCursor($tr);
} }
@ -801,8 +828,9 @@ var AIRTIME = (function(AIRTIME){
} }
$parent.append($table); $parent.append($table);
}
}
*/
//order of importance of elements for setting the next timeout. //order of importance of elements for setting the next timeout.
elements = [ elements = [
$sbTable.find("tr."+NOW_PLAYING_CLASS), $sbTable.find("tr."+NOW_PLAYING_CLASS),
@ -827,6 +855,7 @@ var AIRTIME = (function(AIRTIME){
} }
} }
mod.checkToolBarIcons(); mod.checkToolBarIcons();
}, },
@ -957,9 +986,9 @@ var AIRTIME = (function(AIRTIME){
//item was reordered. //item was reordered.
else { else {
ui.item //ui.item
.empty() // .empty()
.after(draggingContainer.html()); // .after(draggingContainer.html());
aItemData.push(ui.item.data("aData")); aItemData.push(ui.item.data("aData"));
fnMove(); fnMove();
@ -968,9 +997,10 @@ var AIRTIME = (function(AIRTIME){
return { return {
placeholder: "sb-placeholder ui-state-highlight", placeholder: "sb-placeholder ui-state-highlight",
forcePlaceholderSize: true, //forcePlaceholderSize: true,
distance: 10, distance: 10,
helper: function(event, item) { helper:
function(event, item) {
var selected = mod.getSelectedData(NOW_PLAYING_CLASS), var selected = mod.getSelectedData(NOW_PLAYING_CLASS),
thead = $("#show_builder_table thead"), thead = $("#show_builder_table thead"),
colspan = thead.find("th").length, colspan = thead.find("th").length,
@ -985,12 +1015,8 @@ var AIRTIME = (function(AIRTIME){
} }
if (selected.length === 1) { if (selected.length === 1) {
message = $.i18n._("Moving 1 Item"); message = sprintf($.i18n._("Moving %s"), selected[0].title);
} //draggingContainer = item; //Default DataTables drag and drop
else {
message = sprintf($.i18n._("Moving %s Items"), selected.length);
}
draggingContainer = $('<tr/>') draggingContainer = $('<tr/>')
.addClass('sb-helper') .addClass('sb-helper')
.append('<td/>') .append('<td/>')
@ -1001,6 +1027,21 @@ var AIRTIME = (function(AIRTIME){
.addClass("ui-state-highlight") .addClass("ui-state-highlight")
.append(message) .append(message)
.end(); .end();
}
else {
message = sprintf($.i18n._("Moving %s Items"), selected.length);
draggingContainer = $('<tr/>')
.addClass('sb-helper')
.append('<td/>')
.find("td")
.attr("colspan", colspan)
.width(width)
.height(height)
.addClass("ui-state-highlight")
.append(message)
.end();
}
helperData = selected; helperData = selected;

View File

@ -328,10 +328,10 @@ AIRTIME = (function(AIRTIME) {
setTimeout(checkScheduleUpdates, 5000); setTimeout(checkScheduleUpdates, 5000);
} }
}); });
}
//check if the timeline view needs updating. //check if the timeline view needs updating.
checkScheduleUpdates(); setTimeout(checkScheduleUpdates, 5000);
}
}; };
mod.onResize = function() { mod.onResize = function() {

View File

@ -9,6 +9,8 @@ import pickle
import threading import threading
from urlparse import urlparse from urlparse import urlparse
requests.packages.urllib3.disable_warnings()
class PicklableHttpRequest: class PicklableHttpRequest:
def __init__(self, method, url, data, api_key): def __init__(self, method, url, data, api_key):
self.method = method self.method = method

View File

@ -23,12 +23,13 @@ setup(name='airtime_analyzer',
install_requires=[ install_requires=[
'mutagen', 'mutagen',
'pika', 'pika',
'daemon',
'python-magic', 'python-magic',
'nose', 'nose',
'coverage', 'coverage',
'mock', 'mock',
'python-daemon==1.6', 'python-daemon==1.6',
'requests', 'requests>=2.7.0',
'apache-libcloud', 'apache-libcloud',
'rgain', 'rgain',
'boto', 'boto',

View File

@ -83,6 +83,7 @@ api_config['push_stream_stats'] = 'push-stream-stats/api_key/%%api_key%%/format/
api_config['update_stream_setting_table'] = 'update-stream-setting-table/api_key/%%api_key%%/format/json' api_config['update_stream_setting_table'] = 'update-stream-setting-table/api_key/%%api_key%%/format/json'
api_config['get_files_without_silan_value'] = 'get-files-without-silan-value/api_key/%%api_key%%' api_config['get_files_without_silan_value'] = 'get-files-without-silan-value/api_key/%%api_key%%'
api_config['update_cue_values_by_silan'] = 'update-cue-values-by-silan/api_key/%%api_key%%' api_config['update_cue_values_by_silan'] = 'update-cue-values-by-silan/api_key/%%api_key%%'
api_config['update_metadata_on_tunein'] = 'update-metadata-on-tunein/api_key/%%api_key%%'
@ -530,6 +531,9 @@ class AirtimeApiClient(object):
#TODO #TODO
self.logger.error(str(e)) self.logger.error(str(e))
def update_metadata_on_tunein(self):
self.services.update_metadata_on_tunein()
class InvalidContentType(Exception): class InvalidContentType(Exception):
pass pass

View File

@ -249,7 +249,6 @@ s = if dj_live_stream_port != 0 and dj_live_stream_mp != "" then
on_connect=live_dj_connect, on_connect=live_dj_connect,
on_disconnect=live_dj_disconnect)) on_disconnect=live_dj_disconnect))
dj_live = on_metadata(notify_queue, dj_live)
ignore(output.dummy(dj_live, fallible=true)) ignore(output.dummy(dj_live, fallible=true))
switch(id="show_schedule_noise_switch", switch(id="show_schedule_noise_switch",
@ -272,7 +271,6 @@ s = if master_live_stream_port != 0 and master_live_stream_mp != "" then
on_connect=master_dj_connect, on_connect=master_dj_connect,
on_disconnect=master_dj_disconnect)) on_disconnect=master_dj_disconnect))
master_dj = on_metadata(notify_queue, master_dj)
ignore(output.dummy(master_dj, fallible=true)) ignore(output.dummy(master_dj, fallible=true))
switch(id="master_show_schedule_noise_switch", switch(id="master_show_schedule_noise_switch",
@ -284,8 +282,6 @@ else
s s
end end
# Send metadata notifications when using master source
s = on_metadata(notify_queue, s)
# Attach a skip command to the source s: # Attach a skip command to the source s:
#add_skip_command(s) #add_skip_command(s)

View File

@ -14,7 +14,7 @@ import traceback
import pure import pure
from Queue import Empty from Queue import Empty
from threading import Thread from threading import Thread, Timer
from subprocess import Popen, PIPE from subprocess import Popen, PIPE
from api_clients import api_client from api_clients import api_client
@ -447,6 +447,12 @@ class PypoFetch(Thread):
return success return success
# This function makes a request to Airtime to see if we need to
# push metadata to TuneIn. We have to do this because TuneIn turns
# off metadata if it does not receive a request every 5 minutes.
def update_metadata_on_tunein(self):
self.api_client.update_metadata_on_tunein()
Timer(120, self.update_metadata_on_tunein).start()
def main(self): def main(self):
#Make sure all Liquidsoap queues are empty. This is important in the #Make sure all Liquidsoap queues are empty. This is important in the
@ -458,8 +464,10 @@ class PypoFetch(Thread):
self.set_bootstrap_variables() self.set_bootstrap_variables()
self.update_metadata_on_tunein()
# Bootstrap: since we are just starting up, we need to grab the # Bootstrap: since we are just starting up, we need to grab the
# most recent schedule. After that we fetch the schedule every 30 # most recent schedule. After that we fetch the schedule every 8
# minutes or wait for schedule updates to get pushed. # minutes or wait for schedule updates to get pushed.
success = self.persistent_manual_schedule_fetch(max_attempts=5) success = self.persistent_manual_schedule_fetch(max_attempts=5)