Merge pull request #816 from Robbt/add_show_analytics

Add show-based listener analytics
This commit is contained in:
Kyle Robbertze 2019-05-09 16:23:41 +02:00 committed by GitHub
commit 9ac28a1aa5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 369 additions and 5 deletions

View File

@ -26,6 +26,7 @@ $ccAcl->add(new Zend_Acl_Resource('library'))
->add(new Zend_Acl_Resource('playouthistory'))
->add(new Zend_Acl_Resource('playouthistorytemplate'))
->add(new Zend_Acl_Resource('listenerstat'))
->add(new Zend_Acl_Resource('showlistenerstat'))
->add(new Zend_Acl_Resource('usersettings'))
->add(new Zend_Acl_Resource('audiopreview'))
->add(new Zend_Acl_Resource('webstream'))
@ -84,6 +85,7 @@ $ccAcl->allow('G', 'index')
->allow('H', 'playlist')
->allow('H', 'playouthistory')
->allow('H', 'listenerstat')
->allow('H', 'showlistenerstat')
->allow('A', 'playouthistorytemplate')
->allow('A', 'user')
->allow('A', 'systemstatus')

View File

@ -138,6 +138,14 @@ $pages[] = array(
'action' => 'index',
'resource' => 'listenerstat'
),
array(
'label' => _('Show Listener Stats'),
'module' => 'default',
'controller' => 'listenerstat',
'action' => 'show',
'resource' => 'showlistenerstat'
),
)
);
if (LIBRETIME_ENABLE_BILLING === true) {
@ -221,4 +229,4 @@ $container = new Zend_Navigation($pages);
$container->id = "nav";
//store it in the registry:
Zend_Registry::set('Zend_Navigation', $container);
Zend_Registry::set('Zend_Navigation', $container);

View File

@ -26,7 +26,6 @@ class ListenerstatController extends Zend_Controller_Action
$this->view->headScript()->appendFile($baseUrl.'js/timepicker/jquery.ui.timepicker.js?'.$CC_CONFIG['airtime_version'],'text/javascript');
$this->view->headScript()->appendFile($baseUrl.'js/airtime/buttons/buttons.js?'.$CC_CONFIG['airtime_version'],'text/javascript');
$this->view->headScript()->appendFile($baseUrl.'js/airtime/utilities/utilities.js?'.$CC_CONFIG['airtime_version'],'text/javascript');
$this->view->headLink()->appendStylesheet($baseUrl.'css/jquery.ui.timepicker.css?'.$CC_CONFIG['airtime_version']);
list($startsDT, $endsDT) = Application_Common_HTTPHelper::getStartEndFromRequest($request);
@ -56,12 +55,76 @@ class ListenerstatController extends Zend_Controller_Action
$this->view->errorStatus = $out;
$this->view->date_form = $form;
}
public function showAction() {
$CC_CONFIG = Config::getConfig();
$request = $this->getRequest();
$baseUrl = Application_Common_OsPath::getBaseDir();
$headScript = $this->view->headScript();
AirtimeTableView::injectTableJavaScriptDependencies($headScript, $baseUrl, $CC_CONFIG['airtime_version']);
Zend_Layout::getMvcInstance()->assign('parent_page', 'Analytics');
$this->view->headScript()->appendFile($baseUrl.'js/timepicker/jquery.ui.timepicker.js?'.$CC_CONFIG['airtime_version'],'text/javascript');
$this->view->headScript()->appendFile($baseUrl.'js/airtime/buttons/buttons.js?'.$CC_CONFIG['airtime_version'],'text/javascript');
$this->view->headScript()->appendFile($baseUrl.'js/airtime/utilities/utilities.js?'.$CC_CONFIG['airtime_version'],'text/javascript');
$this->view->headScript()->appendFile($baseUrl.'js/airtime/listenerstat/showlistenerstat.js?'.$CC_CONFIG['airtime_version'],'text/javascript');
$this->view->headLink()->appendStylesheet($baseUrl.'css/datatables/css/ColVis.css?'.$CC_CONFIG['airtime_version']);
$this->view->headLink()->appendStylesheet($baseUrl.'css/datatables/css/dataTables.colReorder.min.css?'.$CC_CONFIG['airtime_version']);
$this->view->headLink()->appendStylesheet($baseUrl.'js/datatables/plugin/TableTools-2.1.5/css/TableTools.css?'.$CC_CONFIG['airtime_version']);
$this->view->headLink()->appendStylesheet($baseUrl.'css/jquery.ui.timepicker.css?'.$CC_CONFIG['airtime_version']);
$this->view->headLink()->appendStylesheet($baseUrl.'css/show_analytics.css'.$CC_CONFIG['airtime_version']);
$user = Application_Model_User::getCurrentUser();
if ($user->isUserType(array(UTYPE_SUPERADMIN, UTYPE_ADMIN, UTYPE_PROGRAM_MANAGER))) {
$this->view->showAllShows = true;
}
$data = [];
$this->view->showData = $data;
$form = new Application_Form_ShowListenerStat();
list($startsDT, $endsDT) = Application_Common_HTTPHelper::getStartEndFromRequest($request);
$userTimezone = new DateTimeZone(Application_Model_Preference::GetUserTimezone());
$startsDT->setTimezone($userTimezone);
$endsDT->setTimezone($userTimezone);
$form->populate(array(
'his_date_start' => $startsDT->format("Y-m-d"),
'his_time_start' => $startsDT->format("H:i"),
'his_date_end' => $endsDT->format("Y-m-d"),
'his_time_end' => $endsDT->format("H:i")
));
$this->view->date_form = $form;
}
public function getDataAction(){
list($startsDT, $endsDT) = Application_Common_HTTPHelper::getStartEndFromRequest($this->getRequest());
$data = Application_Model_ListenerStat::getDataPointsWithinRange($startsDT->format(DEFAULT_TIMESTAMP_FORMAT),
$endsDT->format(DEFAULT_TIMESTAMP_FORMAT));
$this->_helper->json->sendJson($data);
}
public function getShowDataAction(){
list($startsDT, $endsDT) = Application_Common_HTTPHelper::getStartEndFromRequest($this->getRequest());
$show_id = $this->getRequest()->getParam("show_id", null);
$data = Application_Model_ListenerStat::getShowDataPointsWithinRange($startsDT->format(DEFAULT_TIMESTAMP_FORMAT),
$endsDT->format(DEFAULT_TIMESTAMP_FORMAT),$show_id);
$this->_helper->json->sendJson($data);
}
public function getAllShowData(){
list($startsDT, $endsDT) = Application_Common_HTTPHelper::getStartEndFromRequest($this->getRequest());
$data = Application_Model_ListenerStat::getAllShowDataPointsWithinRange($startsDT->format(DEFAULT_TIMESTAMP_FORMAT),
$endsDT->format(DEFAULT_TIMESTAMP_FORMAT));
return $data;
}
public function getAllShowDataAction(){
list($startsDT, $endsDT) = Application_Common_HTTPHelper::getStartEndFromRequest($this->getRequest());
$show_id = $this->getRequest()->getParam("show_id", null);
$data = Application_Model_ListenerStat::getAllShowDataPointsWithinRange($startsDT->format(DEFAULT_TIMESTAMP_FORMAT),
$endsDT->format(DEFAULT_TIMESTAMP_FORMAT));
$this->_helper->json->sendJson($data);
}
}

View File

@ -0,0 +1,68 @@
<?php
class Application_Form_ShowListenerStat extends Zend_Form_SubForm
{
public function init()
{
$this->setDecorators(array(
array('ViewScript', array('viewScript' => 'form/daterange.phtml'))
));
// Add start date element
$startDate = new Zend_Form_Element_Text('his_date_start');
$startDate->class = 'input_text';
$startDate->setRequired(true)
->setLabel(_('Date Start:'))
->setValue(date("Y-m-d"))
->setFilters(array('StringTrim'))
->setValidators(array(
'NotEmpty',
array('date', false, array('YYYY-MM-DD'))))
->setDecorators(array('ViewHelper'));
$startDate->setAttrib('alt', 'date');
$this->addElement($startDate);
// Add start time element
$startTime = new Zend_Form_Element_Text('his_time_start');
$startTime->class = 'input_text';
$startTime->setRequired(true)
->setValue('00:00')
->setFilters(array('StringTrim'))
->setValidators(array(
'NotEmpty',
array('date', false, array('HH:mm')),
array('regex', false, array('/^[0-2]?[0-9]:[0-5][0-9]$/', 'messages' => _('Invalid character entered')))))
->setDecorators(array('ViewHelper'));
$startTime->setAttrib('alt', 'time');
$this->addElement($startTime);
// Add end date element
$endDate = new Zend_Form_Element_Text('his_date_end');
$endDate->class = 'input_text';
$endDate->setRequired(true)
->setLabel(_('Date End:'))
->setValue(date("Y-m-d"))
->setFilters(array('StringTrim'))
->setValidators(array(
'NotEmpty',
array('date', false, array('YYYY-MM-DD'))))
->setDecorators(array('ViewHelper'));
$endDate->setAttrib('alt', 'date');
$this->addElement($endDate);
// Add end time element
$endTime = new Zend_Form_Element_Text('his_time_end');
$endTime->class = 'input_text';
$endTime->setRequired(true)
->setValue('01:00')
->setFilters(array('StringTrim'))
->setValidators(array(
'NotEmpty',
array('date', false, array('HH:mm')),
array('regex', false, array('/^[0-2]?[0-9]:[0-5][0-9]$/', 'messages' => _('Invalid character entered')))))
->setDecorators(array('ViewHelper'));
$endTime->setAttrib('alt', 'time');
$this->addElement($endTime);
}
}

View File

@ -16,8 +16,8 @@ group by mount_name
SQL;
$data = Application_Common_Database::prepareAndExecute($sql,
array('p1'=>$p_start, 'p2'=>$p_end));
$out = array();
foreach ($data as $d) {
$jump = intval($d['count']/1000);
$jump = max(1, $jump);
@ -36,7 +36,7 @@ WHERE (temp.rownum%:p4) = :p5;
SQL;
$result = Application_Common_Database::prepareAndExecute($sql,
array('p1'=>$p_start, 'p2'=>$p_end, 'p3'=>$d['mount_name'], 'p4'=>$jump, 'p5'=>$remainder));
$utcTimezone = new DateTimeZone("UTC");
$displayTimezone = new DateTimeZone(Application_Model_Preference::GetUserTimezone());
@ -51,6 +51,7 @@ SQL;
}
}
return $out;
$enabledStreamIds = Application_Model_StreamSetting::getEnabledStreamIds();
$enabledOut = array();
@ -77,6 +78,68 @@ SQL;
return $enabledOut;
}
// this will currently log the average number of listeners to a specific show during a certain range
public static function getShowDataPointsWithinRange($p_start, $p_end, $show_id) {
$showData = [];
$ccShow = CcShowQuery::create()->findPk($show_id);
$showName = $ccShow->getDbName();
// this query selects all show instances that aired in this date range that match the show_id
$sql = <<<SQL
SELECT id, starts, ends FROM cc_show_instances WHERE show_id =:p1
AND starts >=:p2 AND ends <=:p3
SQL;
$data = Application_Common_Database::prepareAndExecute($sql,
array('p1'=>$show_id,'p2'=>$p_start, 'p3'=>$p_end));
foreach ($data as $d) {
$sql = <<<SQL
SELECT timestamp, SUM(listener_count) AS listeners
FROM cc_listener_count AS lc
INNER JOIN cc_timestamp AS ts ON (lc.timestamp_id = ts.ID)
INNER JOIN cc_mount_name AS mn ON (lc.mount_name_id = mn.ID)
WHERE (ts.timestamp >=:p1 AND ts.timestamp <=:p2)
GROUP BY timestamp
SQL;
$data = Application_Common_Database::prepareAndExecute($sql,
array('p1'=>$d['starts'], 'p2'=>$d['ends']));
$utcTimezone = new DateTimeZone("UTC");
$displayTimezone = new DateTimeZone(Application_Model_Preference::GetUserTimezone());
if (sizeof($data) > 0) {
$t = new DateTime($data[0]['timestamp'], $utcTimezone);
$t->setTimezone($displayTimezone);
// tricking javascript so it thinks the server timezone is in UTC
$average_listeners = array_sum(array_column($data, 'listeners')) / sizeof($data);
$max_num_listeners = max(array_column($data, 'listeners'));
$entry = array("show" => $showName, "time" => $t->format( 'Y-m-d H:i:s')
, "average_number_of_listeners" => $average_listeners,
"maximum_number_of_listeners" => $max_num_listeners);
array_push($showData, $entry);
}
}
return($showData);
}
public static function getAllShowDataPointsWithinRange($p_start, $p_end) {
// this query selects the id of all show instances that aired in this date range
$all_show_data = [];
$sql = <<<SQL
SELECT show_id FROM cc_show_instances
WHERE starts >=:p1 AND ends <=:p2
GROUP BY show_id
SQL;
$data = Application_Common_Database::prepareAndExecute($sql,
array('p1'=>$p_start, 'p2'=>$p_end));
foreach($data as $show_id) {
$all_show_data = array_merge(self::getShowDataPointsWithinRange($p_start,$p_end,$show_id['show_id']), $all_show_data);
}
/* option to sort by number of listeners currently commented out
usort($all_show_data, function($a, $b) {
return $a['average_number_of_listeners'] - $b['average_number_of_listeners'];
});
*/
return $all_show_data;
}
public static function insertDataPoints($p_dataPoints) {

View File

@ -0,0 +1,23 @@
<div id="showlistenerstat_content" class="alpha-block padded">
<H2><?php echo _("Listeners")?></H2>
<div class="error_window"></div>
<div id="date_form" style="float:left; margin:0px 0px">
<h3 style="padding-left: 0px">Date Range</h3>
<?php echo $this->date_form; ?>
</div>
<table id="show_builder_table" cellpadding="0" cellspacing="0" class="datatable"></table>
<table cellspacing="0" cellpadding="0" style="" id="show_stats_datatable" class="datatable">
<thead>
<tr>
<th><?php echo _("Show Name") ?></th>
<th><?php echo _("Air Date") ?></th>
<th><?php echo _("Average Listeners")?></th>
<th><?php echo _("Maximum Number of Listeners")?></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<div style="clear: both;"></div>
</div>

View File

@ -0,0 +1,10 @@
div.date_form input.error {
background-color: rgba(255,0,0,0.2);
}
div.error_window.error {
background-color: rgba(255,0,0,0.2);
color:white;
}
div.error_window {
background-color: white;
}

View File

@ -0,0 +1,127 @@
$(document).ready(function() {
showlistenerstat_content = $("#showlistenerstat_content")
dateStartId = "#his_date_start",
timeStartId = "#his_time_start",
dateEndId = "#his_date_end",
timeEndId = "#his_time_end",
show_id = "#his_show_filter";
// set width dynamically
var width = $("#showlistenerstat_content").width();
width = width * .91;
addDatePicker();
showlistenerstat_content.find("#his_submit").click(function(){
// var show_id = $("#sb_show_filter").val();
var oRange = AIRTIME.utilities.fnGetScheduleRange(dateStartId, timeStartId, dateEndId, timeEndId);
var start = oRange.start;
var end = oRange.end;
showListenerDataTable();
});
});
function getShowData(startTimestamp, endTimestamp, show_id) {
// get data
$.get(baseUrl+'Listenerstat/get-all-show-data', {start: startTimestamp, end: endTimestamp }, function(data) {
return data;
});
}
function addDatePicker() {
oBaseDatePickerSettings = {
dateFormat: 'yy-mm-dd',
//i18n_months, i18n_days_short are in common.js
monthNames: i18n_months,
dayNamesMin: i18n_days_short,
onSelect: function(sDate, oDatePicker) {
$(this).datepicker( "setDate", sDate );
},
onClose: validateTimeRange
};
oBaseTimePickerSettings = {
showPeriodLabels: false,
showCloseButton: true,
closeButtonText: $.i18n._("Done"),
showLeadingZero: false,
defaultTime: '0:00',
hourText: $.i18n._("Hour"),
minuteText: $.i18n._("Minute"),
onClose: validateTimeRange
};
showlistenerstat_content.find(dateStartId).datepicker(oBaseDatePickerSettings).blur(validateTimeRange());
showlistenerstat_content.find(timeStartId).timepicker(oBaseTimePickerSettings).blur(validateTimeRange());
showlistenerstat_content.find(dateEndId).datepicker(oBaseDatePickerSettings).blur(validateTimeRange());
showlistenerstat_content.find(timeEndId).timepicker(oBaseTimePickerSettings).blur(validateTimeRange());
}
function getStartEnd() {
return AIRTIME.utilities.fnGetScheduleRange(dateStartId, timeStartId, dateEndId, timeEndId);
}
function validateTimeRange() {
var oRange,
inputs = $('.date_form > input'),
error_window = $('.error_window'),
start, end;
oRange = AIRTIME.utilities.fnGetScheduleRange(dateStartId, timeStartId, dateEndId, timeEndId);
start = oRange.start;
end = oRange.end;
if (end >= start) {
error_window.removeClass('error');
$('.error_window').html('');
}
else {
error_window.addClass('error');
console.log('bad')
$('.error_window').html('Your start date time is after your end date time');
}
return {
start: start,
end: end,
isValid: end >= start
};
}
function showListenerDataTable() {
var oRange = AIRTIME.utilities.fnGetScheduleRange(dateStartId, timeStartId, dateEndId, timeEndId);
var start = oRange.start;
var lengthMenu = [[10, 25, 50, 100, 500, -1], [10, 25, 50, 100, 500, $.i18n._("All")]];
var end = oRange.end;
var sDom = 'l<"dt-process-rel"r><"H"T><"dataTables_scrolling"t><"F"ip>';
var show_id = $("#sb_show_filter").val();
var dt = $('#show_stats_datatable');
info = getStartEnd();
dt.dataTable({
"aoColumns": [
/* first name */ {"sName": "show", "mDataProp": "show"},
/* air date */ {"sName": "time", "mDataProp": "time"},
/* last name */ {"sName": "average_number_of_listeners", "mDataProp": "average_number_of_listeners"},
/* last name */ {"sName": "maximum_number_of_listeners", "mDataProp": "maximum_number_of_listeners"}],
"sAjaxSource": baseUrl+'Listenerstat/get-all-show-data',
"sAjaxDataProp": "",
"bDestroy": true,
"aLengthMenu": lengthMenu,
"iDisplayLength": 25,
"sPaginationType": "full_numbers",
"bJQueryUI": true,
"bAutoWidth": true,
"sDom": sDom,
"fnServerData": function ( sSource, aoData, fnCallback ) {
aoData.push({"start": start, "end": end});
$.ajax( {
"dataType": 'json',
"type": "POST",
"url": sSource,
"data": {"start": start, "end": end},
"success": fnCallback
} );
},
});
}