Implement Station History API, convert history report to use it.
This commit is contained in:
parent
7d4330665b
commit
9a2d5108e8
|
@ -163,6 +163,11 @@ return function(\Slim\App $app) {
|
|||
|
||||
$this->get('/nowplaying', Controller\Api\NowplayingController::class.':indexAction');
|
||||
|
||||
// This would not normally be POST-able, but Bootgrid requires it
|
||||
$this->map(['GET', 'POST'], '/history', Controller\Api\Stations\HistoryController::class)
|
||||
->setName('api:stations:history')
|
||||
->add([Middleware\Permissions::class, 'view station reports', true]);
|
||||
|
||||
// This would not normally be POST-able, but Bootgrid requires it
|
||||
$this->map(['GET', 'POST'], '/requests', Controller\Api\RequestsController::class.':listAction')
|
||||
->setName('api:requests:list');
|
||||
|
|
|
@ -3,21 +3,25 @@
|
|||
<?php
|
||||
/** @var \App\Assets $assets */
|
||||
$assets
|
||||
->load('daterangepicker')
|
||||
->load('bootgrid');
|
||||
?>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header ch-alt">
|
||||
<h2><?=__('Song Playback Timeline') ?></h2>
|
||||
|
||||
<ul class="actions">
|
||||
<li>
|
||||
<a href="<?=$router->fromHere(null, ['format' => 'csv']) ?>" title="<?=__('Download CSV') ?>">
|
||||
<span class="sr-only"><?=__('Download CSV') ?></span>
|
||||
<i class="zmdi zmdi-download"></i>
|
||||
<div class="row">
|
||||
<div class="col-md-5">
|
||||
<h2><?=__('Song Playback Timeline') ?></h2>
|
||||
</div>
|
||||
<div class="col-md-7 text-right">
|
||||
<a class="btn btn-default" id="reportrange" href="#">
|
||||
<i class="zmdi zmdi-calendar"></i> <span><?=__('Last 14 Days') ?></span> <i class="caret"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<a class="btn btn-primary" id="btn-export" href="<?=$router->fromHere('api:stations:history', [], ['format' => 'csv']) ?>" target="_blank" title="<?=__('Download CSV') ?>">
|
||||
<i class="zmdi zmdi-download"></i> <?=__('Download CSV') ?>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="data-table table table-striped">
|
||||
|
@ -30,65 +34,105 @@ $assets
|
|||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-column-id="date_time"><?=__('Date/Time') ?></th>
|
||||
<th data-column-id="listeners"><?=__('Listeners') ?></th>
|
||||
<th data-column-id="delta"><?=__('Change') ?></th>
|
||||
<th data-column-id="song"><?=__('Song Title') ?></th>
|
||||
<th data-column-id="playlists"><?=__('Source') ?></th>
|
||||
<th data-column-id="date_time" data-formatter="datetime"><?=__('Date/Time') ?></th>
|
||||
<th data-column-id="listeners_start"><?=__('Listeners') ?></th>
|
||||
<th data-column-id="delta" data-formatter="delta"><?=__('Change') ?></th>
|
||||
<th data-column-id="song" data-formatter="song_title"><?=__('Song Title') ?></th>
|
||||
<th data-column-id="playlists" data-formatter="playlists"><?=__('Source') ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach($songs as $song_row): ?>
|
||||
<tr class="input" id="song_<?=$song_row['timestamp'] ?>">
|
||||
<td class="text-center"><?=$customization->formatTime($song_row['timestamp_start']) ?></td>
|
||||
<td class="text-center"><big><?=$song_row['stat_start'] ?></big></td>
|
||||
<td class="text-center">
|
||||
<big>
|
||||
<?php if ($song_row['stat_delta'] > 0): ?>
|
||||
<span class="text-success"><i class="icon-caret-up"></i> <?=$song_row['stat_delta'] ?></span>
|
||||
<?php elseif ($song_row['stat_delta'] < 0): ?>
|
||||
<span class="text-danger"><i class="icon-caret-down"></i> <?=abs($song_row['stat_delta']) ?></span>
|
||||
<?php else: ?>
|
||||
0
|
||||
<?php endif; ?>
|
||||
</big>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($song_row['song']['title']): ?>
|
||||
<b><?=$song_row['song']['title'] ?></b><br>
|
||||
<?=$song_row['song']['artist'] ?>
|
||||
<?php else: ?>
|
||||
<?=$song_row['song']['text'] ?>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if (isset($song_row['request'])): ?>
|
||||
<?=sprintf(__('Request: %s'), date('D g:ia', $song_row['request']['timestamp'])) ?>
|
||||
<?php elseif (isset($song_row['playlist'])): ?>
|
||||
<?=sprintf(__('Playlist: %s'), $song_row['playlist']['name']) ?>
|
||||
<?php else: ?>
|
||||
<?=__('Live Broadcast') ?>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript" nonce="<?=$assets->getCspNonce() ?>">
|
||||
$(function() {
|
||||
$(".data-table").bootgrid({
|
||||
caseSensitive: false,
|
||||
sorting: false,
|
||||
css: {
|
||||
icon: 'zmdi icon',
|
||||
iconColumns: 'zmdi-view-module',
|
||||
iconDown: 'zmdi-sort-amount-desc',
|
||||
iconRefresh: 'zmdi-refresh',
|
||||
iconUp: 'zmdi-sort-amount-asc'
|
||||
let range_start = null,
|
||||
range_end = null;
|
||||
|
||||
const api_url = "<?=$router->fromHere('api:stations:history') ?>";
|
||||
|
||||
$(function() {
|
||||
$(".data-table").bootgrid({
|
||||
ajax: true,
|
||||
sorting: false,
|
||||
caseSensitive: false,
|
||||
css: {
|
||||
icon: 'zmdi icon',
|
||||
iconColumns: 'zmdi-view-module',
|
||||
iconDown: 'zmdi-sort-amount-desc',
|
||||
iconRefresh: 'zmdi-refresh',
|
||||
iconUp: 'zmdi-sort-amount-asc'
|
||||
},
|
||||
url: api_url,
|
||||
post: function() {
|
||||
return {
|
||||
start: range_start,
|
||||
end: range_end
|
||||
};
|
||||
},
|
||||
formatters: {
|
||||
"datetime": function(column, row) {
|
||||
return formatTimestamp(row.played_at);
|
||||
},
|
||||
"delta": function(column, row) {
|
||||
if (row.delta_total > 0) {
|
||||
return '<big><span class="text-success"><i class="zmdi zmdi-trending-up"></i> '+row.delta_total+'</span></big>';
|
||||
} else if (row.delta_total < 0) {
|
||||
return '<big><span class="text-danger"><i class="zmdi zmdi-trending-down"></i> ' + Math.abs(row.delta_total) + '</span></big>';
|
||||
} else {
|
||||
return '<big>0</big>';
|
||||
}
|
||||
},
|
||||
"song_title": function(column, row) {
|
||||
if (row.song_title) {
|
||||
return '<b>'+row.song_title+'</b><br>'+row.song_artist;
|
||||
} else {
|
||||
return row.song_text;
|
||||
}
|
||||
},
|
||||
"playlists": function(column, row) {
|
||||
if (row.is_request) {
|
||||
return <?=$this->escapeJs(__('Listener Request')) ?>;
|
||||
} else if (row.playlist) {
|
||||
return <?=$this->escapeJs(__('Playlist:')) ?>+" "+row.playlist;
|
||||
} else {
|
||||
return <?=$this->escapeJs(__('Live Broadcast')) ?>;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$('#reportrange').daterangepicker({
|
||||
startDate: moment().subtract(13, 'days'),
|
||||
endDate: moment(),
|
||||
opens: "left",
|
||||
ranges: {
|
||||
"<?=__('Today') ?>": [moment(), moment()],
|
||||
"<?=__('Yesterday') ?>": [moment().subtract(1, 'days'), moment().subtract(1, 'days')],
|
||||
"<?=__('Last 7 Days') ?>": [moment().subtract(6, 'days'), moment()],
|
||||
"<?=__('Last 14 Days') ?>": [moment().subtract(13, 'days'), moment()],
|
||||
"<?=__('Last 30 Days') ?>": [moment().subtract(29, 'days'), moment()],
|
||||
"<?=__('This Month') ?>": [moment().startOf('month'), moment().endOf('month')],
|
||||
"<?=__('Last Month') ?>": [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')]
|
||||
}
|
||||
}, function(start, end) {
|
||||
$('#reportrange span').html(start.format('MMMM D, YYYY') + ' - ' + end.format('MMMM D, YYYY'));
|
||||
|
||||
range_start = start.format('YYYY-MM-DD');
|
||||
range_end = end.format('YYYY-MM-DD');
|
||||
|
||||
$('#btn-export').attr('href', api_url+'?format=csv&start='+range_start+'&end='+range_end);
|
||||
$('.data-table').bootgrid("reload");
|
||||
});
|
||||
|
||||
function formatTimestamp(unix_timestamp) {
|
||||
var m = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
var d = new Date(unix_timestamp*1000);
|
||||
return [m[d.getMonth()],' ',d.getDate(),', ',d.getFullYear()," ",
|
||||
(d.getHours() % 12 || 12),":",(d.getMinutes() < 10 ? '0' : '')+d.getMinutes(),
|
||||
" ",d.getHours() >= 12 ? 'PM' : 'AM'].join('');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -28,13 +28,6 @@ class ApiProvider implements ServiceProviderInterface
|
|||
);
|
||||
};
|
||||
|
||||
$di[Stations\MediaController::class] = function($di) {
|
||||
return new Stations\MediaController(
|
||||
$di[\Doctrine\ORM\EntityManager::class],
|
||||
$di[\App\Customization::class]
|
||||
);
|
||||
};
|
||||
|
||||
$di[NowplayingController::class] = function($di) {
|
||||
return new NowplayingController(
|
||||
$di[\Doctrine\ORM\EntityManager::class],
|
||||
|
@ -45,7 +38,13 @@ class ApiProvider implements ServiceProviderInterface
|
|||
$di[RequestsController::class] = function($di) {
|
||||
return new RequestsController(
|
||||
$di[\Doctrine\ORM\EntityManager::class],
|
||||
$di['router'],
|
||||
$di[\App\ApiUtilities::class]
|
||||
);
|
||||
};
|
||||
|
||||
$di[Stations\HistoryController::class] = function($di) {
|
||||
return new Stations\HistoryController(
|
||||
$di[\Doctrine\ORM\EntityManager::class],
|
||||
$di[\App\ApiUtilities::class]
|
||||
);
|
||||
};
|
||||
|
@ -57,6 +56,13 @@ class ApiProvider implements ServiceProviderInterface
|
|||
);
|
||||
};
|
||||
|
||||
$di[Stations\MediaController::class] = function($di) {
|
||||
return new Stations\MediaController(
|
||||
$di[\Doctrine\ORM\EntityManager::class],
|
||||
$di[\App\Customization::class]
|
||||
);
|
||||
};
|
||||
|
||||
$di[Stations\ServicesController::class] = function($di) {
|
||||
return new Stations\ServicesController(
|
||||
$di[\Doctrine\ORM\EntityManager::class],
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
namespace App\Controller\Api;
|
||||
|
||||
use App\Doctrine\Paginator;
|
||||
use App\Http\Router;
|
||||
use App\Utilities;
|
||||
use App\ApiUtilities;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
|
@ -15,22 +14,17 @@ class RequestsController
|
|||
/** @var EntityManager */
|
||||
protected $em;
|
||||
|
||||
/** @var Router */
|
||||
protected $router;
|
||||
|
||||
/** @var ApiUtilities */
|
||||
protected $api_utils;
|
||||
|
||||
/**
|
||||
* RequestsController constructor.
|
||||
* @param EntityManager $em
|
||||
* @param Router $router
|
||||
* @param ApiUtilities $api_utils
|
||||
*/
|
||||
public function __construct(EntityManager $em, Router $router, ApiUtilities $api_utils)
|
||||
public function __construct(EntityManager $em, ApiUtilities $api_utils)
|
||||
{
|
||||
$this->em = $em;
|
||||
$this->router = $router;
|
||||
$this->api_utils = $api_utils;
|
||||
}
|
||||
|
||||
|
@ -102,22 +96,20 @@ class RequestsController
|
|||
$paginator->setFromRequest($request);
|
||||
|
||||
$is_bootgrid = $paginator->isFromBootgrid();
|
||||
$router = $request->getRouter();
|
||||
|
||||
$paginator->setPostprocessor(function($media_row) use ($station_id, $is_bootgrid) {
|
||||
$paginator->setPostprocessor(function($media_row) use ($station_id, $is_bootgrid, $router) {
|
||||
/** @var Entity\StationMedia $media_row */
|
||||
$row = new Entity\Api\StationRequest;
|
||||
$row->song = $media_row->api($this->api_utils);
|
||||
$row->request_id = (int)$media_row->getId();
|
||||
$row->request_url = (string)$this->router->named('api:requests:submit', [
|
||||
$row->request_url = (string)$router->named('api:requests:submit', [
|
||||
'station' => $station_id,
|
||||
'media_id' => $media_row->getUniqueId(),
|
||||
]);
|
||||
|
||||
if ($is_bootgrid) {
|
||||
$row = json_decode(json_encode($row), true);
|
||||
foreach($row['song'] as $song_key => $song_val) {
|
||||
$row['song_'.$song_key] = $song_val;
|
||||
}
|
||||
return Utilities::flatten_array($row, '_');
|
||||
}
|
||||
|
||||
return $row;
|
||||
|
|
|
@ -0,0 +1,123 @@
|
|||
<?php
|
||||
namespace App\Controller\Api\Stations;
|
||||
|
||||
use App;
|
||||
use App\Doctrine\Paginator;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use App\Entity;
|
||||
use App\Http\Request;
|
||||
use App\Http\Response;
|
||||
|
||||
/**
|
||||
* Class HistoryController
|
||||
* @package App\Controller\Api\Stations
|
||||
* @see App\Controller\Api\ApiProvider
|
||||
*/
|
||||
class HistoryController
|
||||
{
|
||||
/** @var EntityManager */
|
||||
protected $em;
|
||||
|
||||
/** @var App\ApiUtilities */
|
||||
protected $api_utils;
|
||||
|
||||
/**
|
||||
* @param EntityManager $em
|
||||
* @param App\ApiUtilities $api_utils
|
||||
*/
|
||||
public function __construct(EntityManager $em, App\ApiUtilities $api_utils)
|
||||
{
|
||||
$this->em = $em;
|
||||
$this->api_utils = $api_utils;
|
||||
}
|
||||
|
||||
public function __invoke(Request $request, Response $response, $station_id)
|
||||
{
|
||||
$station = $request->getStation();
|
||||
|
||||
if ($request->hasParam('start')) {
|
||||
$start = strtotime($request->getParam('start') . ' 00:00:00');
|
||||
$end = strtotime($request->getParam('end', $request->getParam('start')) . ' 23:59:59');
|
||||
} else {
|
||||
$start = strtotime('-2 weeks');
|
||||
$end = time();
|
||||
}
|
||||
|
||||
$qb = $this->em->createQueryBuilder();
|
||||
|
||||
$qb->select('sh, sr, sp, s')
|
||||
->from(Entity\SongHistory::class, 'sh')
|
||||
->leftJoin('sh.request', 'sr')
|
||||
->leftJoin('sh.playlist', 'sp')
|
||||
->leftJoin('sh.song', 's')
|
||||
->where('sh.station_id = :station_id')
|
||||
->andWhere('sh.timestamp_end >= :start AND sh.timestamp_start <= :end')
|
||||
->andWhere('sh.listeners_start IS NOT NULL')
|
||||
->setParameter('station_id', $station_id)
|
||||
->setParameter('start', $start)
|
||||
->setParameter('end', $end);
|
||||
|
||||
if ($request->getParam('format', 'json') === 'csv') {
|
||||
$export_all = [];
|
||||
$export_all[] = [
|
||||
'Date',
|
||||
'Time',
|
||||
'Listeners',
|
||||
'Delta',
|
||||
'Likes',
|
||||
'Dislikes',
|
||||
'Track',
|
||||
'Artist',
|
||||
'Playlist'
|
||||
];
|
||||
|
||||
foreach ($qb->getQuery()->getArrayResult() as $song_row) {
|
||||
$export_row = [
|
||||
date('Y-m-d', $song_row['timestamp_start']),
|
||||
date('g:ia', $song_row['timestamp_start']),
|
||||
$song_row['listeners_start'],
|
||||
$song_row['delta_total'],
|
||||
$song_row['score_likes'],
|
||||
$song_row['score_dislikes'],
|
||||
$song_row['song']['title'] ?: $song_row['song']['text'],
|
||||
$song_row['song']['artist'],
|
||||
$song_row['playlist']['name'] ?? '',
|
||||
];
|
||||
|
||||
$export_all[] = $export_row;
|
||||
}
|
||||
|
||||
$csv_file = App\Export::csv($export_all);
|
||||
$csv_filename = $station->getShortName() . '_timeline_' . date('Ymd', $start) . '_to_'. date('Ymd', $end).'.csv';
|
||||
|
||||
return $response->renderStringAsFile($csv_file, 'text/csv', $csv_filename);
|
||||
}
|
||||
|
||||
$search_phrase = trim($request->getParam('searchPhrase'));
|
||||
if (!empty($search_phrase)) {
|
||||
$qb->andWhere('(s.title LIKE :query OR s.artist LIKE :query)')
|
||||
->setParameter('query', '%'.$search_phrase.'%');
|
||||
}
|
||||
|
||||
$qb->orderBy('sh.timestamp_start', 'DESC');
|
||||
|
||||
$paginator = new Paginator($qb);
|
||||
$paginator->setFromRequest($request);
|
||||
|
||||
$is_bootgrid = $paginator->isFromBootgrid();
|
||||
|
||||
$paginator->setPostprocessor(function($sh_row) use ($is_bootgrid) {
|
||||
|
||||
/** @var Entity\SongHistory $sh_row */
|
||||
$row = $sh_row->api(new Entity\Api\DetailedSongHistory, $this->api_utils);
|
||||
|
||||
if ($is_bootgrid) {
|
||||
return App\Utilities::flatten_array($row, '_');
|
||||
}
|
||||
|
||||
return $row;
|
||||
});
|
||||
|
||||
return $paginator->write($response);
|
||||
}
|
||||
}
|
|
@ -10,19 +10,19 @@ use InfluxDB\Database;
|
|||
|
||||
class IndexController
|
||||
{
|
||||
use Traits\SongHistoryFilters;
|
||||
/** @var EntityManager */
|
||||
protected $em;
|
||||
|
||||
/** @var Database */
|
||||
protected $influx;
|
||||
|
||||
/**
|
||||
* IndexController constructor.
|
||||
* @param EntityManager $em
|
||||
* @param Database $influx
|
||||
*/
|
||||
public function __construct(EntityManager $em, Cache $cache, Database $influx)
|
||||
public function __construct(EntityManager $em, Database $influx)
|
||||
{
|
||||
$this->em = $em;
|
||||
$this->cache = $cache;
|
||||
$this->influx = $influx;
|
||||
}
|
||||
|
||||
|
@ -111,11 +111,6 @@ class IndexController
|
|||
->setMaxResults(40)
|
||||
->getArrayResult();
|
||||
|
||||
$ignored_songs = $this->_getIgnoredSongs();
|
||||
$song_totals_raw['played'] = array_filter($song_totals_raw['played'], function ($value) use ($ignored_songs) {
|
||||
return !(isset($ignored_songs[$value['song_id']]));
|
||||
});
|
||||
|
||||
// Compile the above data.
|
||||
$song_totals = [];
|
||||
foreach ($song_totals_raw as $total_type => $total_records) {
|
||||
|
@ -143,11 +138,6 @@ class IndexController
|
|||
->setParameter('timestamp', $threshold)
|
||||
->getArrayResult();
|
||||
|
||||
$ignored_songs = $this->_getIgnoredSongs();
|
||||
$songs_played_raw = array_filter($songs_played_raw, function ($value) use ($ignored_songs) {
|
||||
return !(isset($ignored_songs[$value['song_id']]));
|
||||
});
|
||||
|
||||
$songs_played_raw = array_values($songs_played_raw);
|
||||
$songs = [];
|
||||
|
||||
|
|
|
@ -10,7 +10,8 @@ use App\Http\Response;
|
|||
|
||||
class ReportsController
|
||||
{
|
||||
use Traits\SongHistoryFilters;
|
||||
/** @var EntityManager */
|
||||
protected $em;
|
||||
|
||||
/** @var RadioAutomation */
|
||||
protected $sync_automation;
|
||||
|
@ -18,77 +19,17 @@ class ReportsController
|
|||
/**
|
||||
* ReportsController constructor.
|
||||
* @param EntityManager $em
|
||||
* @param Cache $cache
|
||||
* @param RadioAutomation $sync_automation
|
||||
*/
|
||||
public function __construct(EntityManager $em, Cache $cache, RadioAutomation $sync_automation)
|
||||
public function __construct(EntityManager $em, RadioAutomation $sync_automation)
|
||||
{
|
||||
$this->em = $em;
|
||||
$this->cache = $cache;
|
||||
$this->sync_automation = $sync_automation;
|
||||
}
|
||||
|
||||
public function timelineAction(Request $request, Response $response, $station_id, $format = 'html'): Response
|
||||
public function timelineAction(Request $request, Response $response): Response
|
||||
{
|
||||
$station = $request->getStation();
|
||||
|
||||
$songs_played_raw = $this->_getEligibleHistory($station_id);
|
||||
|
||||
$songs = [];
|
||||
foreach ($songs_played_raw as $song_row) {
|
||||
// Song has no recorded ending.
|
||||
if ($song_row['timestamp_end'] == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$song_row['stat_start'] = $song_row['listeners_start'];
|
||||
$song_row['stat_end'] = $song_row['listeners_end'];
|
||||
$song_row['stat_delta'] = $song_row['delta_total'];
|
||||
|
||||
$songs[] = $song_row;
|
||||
}
|
||||
|
||||
if ($format === 'csv') {
|
||||
$export_all = [];
|
||||
$export_all[] = [
|
||||
'Date',
|
||||
'Time',
|
||||
'Listeners',
|
||||
'Delta',
|
||||
'Likes',
|
||||
'Dislikes',
|
||||
'Track',
|
||||
'Artist',
|
||||
'Playlist'
|
||||
];
|
||||
|
||||
foreach ($songs as $song_row) {
|
||||
$export_row = [
|
||||
date('Y-m-d', $song_row['timestamp_start']),
|
||||
date('g:ia', $song_row['timestamp_start']),
|
||||
$song_row['stat_start'],
|
||||
$song_row['stat_delta'],
|
||||
$song_row['score_likes'],
|
||||
$song_row['score_dislikes'],
|
||||
$song_row['song']['title'] ?: $song_row['song']['text'],
|
||||
$song_row['song']['artist'],
|
||||
$song_row['playlist']['name'] ?? '',
|
||||
];
|
||||
|
||||
$export_all[] = $export_row;
|
||||
}
|
||||
|
||||
$csv_file = \App\Export::csv($export_all);
|
||||
$csv_filename = $station->getShortName() . '_timeline_' . date('Ymd') . '.csv';
|
||||
|
||||
return $response->renderStringAsFile($csv_file, 'text/csv', $csv_filename);
|
||||
}
|
||||
|
||||
$songs = array_reverse($songs);
|
||||
|
||||
return $request->getView()->renderToResponse($response, 'stations/reports/timeline', [
|
||||
'songs' => $songs,
|
||||
]);
|
||||
return $request->getView()->renderToResponse($response, 'stations/reports/timeline');
|
||||
}
|
||||
|
||||
public function performanceAction(Request $request, Response $response, $station_id, $format = 'html'): Response
|
||||
|
|
|
@ -50,7 +50,6 @@ class StationsProvider implements ServiceProviderInterface
|
|||
$di[IndexController::class] = function($di) {
|
||||
return new IndexController(
|
||||
$di[\Doctrine\ORM\EntityManager::class],
|
||||
$di[\App\Cache::class],
|
||||
$di[\InfluxDB\Database::class]
|
||||
);
|
||||
};
|
||||
|
@ -97,7 +96,6 @@ class StationsProvider implements ServiceProviderInterface
|
|||
$di[ReportsController::class] = function($di) {
|
||||
return new ReportsController(
|
||||
$di[\Doctrine\ORM\EntityManager::class],
|
||||
$di[\App\Cache::class],
|
||||
$di[\App\Sync\Task\RadioAutomation::class]
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,90 +0,0 @@
|
|||
<?php
|
||||
namespace App\Controller\Stations\Traits;
|
||||
|
||||
use App\Cache;
|
||||
use App\Entity\Song;
|
||||
use App\Entity\SongHistory;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
|
||||
trait SongHistoryFilters
|
||||
{
|
||||
/** @var Cache */
|
||||
protected $cache;
|
||||
|
||||
/** @var EntityManager */
|
||||
protected $em;
|
||||
|
||||
protected function _getEligibleHistory($station_id)
|
||||
{
|
||||
$cache_name = 'stations/'.$station_id.'/history';
|
||||
|
||||
$songs_played_raw = $this->cache->get($cache_name);
|
||||
|
||||
if (!$songs_played_raw) {
|
||||
try {
|
||||
$first_song = $this->em->createQuery('SELECT sh.timestamp_start FROM '.SongHistory::class.' sh
|
||||
WHERE sh.station_id = :station_id AND sh.listeners_start IS NOT NULL
|
||||
ORDER BY sh.timestamp_start ASC')
|
||||
->setParameter('station_id', $station_id)
|
||||
->setMaxResults(1)
|
||||
->getSingleScalarResult();
|
||||
} catch (\Exception $e) {
|
||||
$first_song = strtotime('Yesterday 00:00:00');
|
||||
}
|
||||
|
||||
$min_threshold = strtotime('-2 weeks');
|
||||
$threshold = max($first_song, $min_threshold);
|
||||
|
||||
// Get all songs played in timeline.
|
||||
$songs_played_raw = $this->em->createQuery('SELECT sh, sr, sp, s
|
||||
FROM '.SongHistory::class.' sh
|
||||
LEFT JOIN sh.request sr
|
||||
LEFT JOIN sh.playlist sp
|
||||
LEFT JOIN sh.song s
|
||||
WHERE sh.station_id = :station_id AND sh.timestamp_start >= :timestamp AND sh.listeners_start IS NOT NULL
|
||||
ORDER BY sh.timestamp_start ASC')
|
||||
->setParameter('station_id', $station_id)
|
||||
->setParameter('timestamp', $threshold)
|
||||
->getArrayResult();
|
||||
|
||||
$ignored_songs = $this->_getIgnoredSongs();
|
||||
$songs_played_raw = array_filter($songs_played_raw, function ($value) use ($ignored_songs) {
|
||||
return !(isset($ignored_songs[$value['song_id']]));
|
||||
});
|
||||
|
||||
$songs_played_raw = array_values($songs_played_raw);
|
||||
|
||||
$this->cache->save($songs_played_raw, $cache_name, 60 * 5);
|
||||
}
|
||||
|
||||
return $songs_played_raw;
|
||||
}
|
||||
|
||||
protected function _getIgnoredSongs()
|
||||
{
|
||||
$song_hashes = $this->cache->get('stations/all/ignored_songs');
|
||||
|
||||
if (!$song_hashes) {
|
||||
$ignored_phrases = ['Offline', 'Sweeper', 'Bumper', 'Unknown'];
|
||||
|
||||
$qb = $this->em->createQueryBuilder();
|
||||
$qb->select('s.id')->from(Song::class, 's');
|
||||
|
||||
foreach ($ignored_phrases as $i => $phrase) {
|
||||
$qb->orWhere('s.text LIKE ?' . ($i + 1));
|
||||
$qb->setParameter($i + 1, '%' . $phrase . '%');
|
||||
}
|
||||
|
||||
$song_hashes_raw = $qb->getQuery()->getArrayResult();
|
||||
$song_hashes = [];
|
||||
|
||||
foreach ($song_hashes_raw as $row) {
|
||||
$song_hashes[$row['id']] = $row['id'];
|
||||
}
|
||||
|
||||
$this->cache->save($song_hashes, 'stations/all/ignored_songs', 86400);
|
||||
}
|
||||
|
||||
return $song_hashes;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
namespace App\Entity\Api;
|
||||
|
||||
/**
|
||||
* @SWG\Definition(type="object")
|
||||
*/
|
||||
class DetailedSongHistory extends SongHistory
|
||||
{
|
||||
/**
|
||||
* Number of listeners when the song playback started.
|
||||
*
|
||||
* @SWG\Property(example=94)
|
||||
* @var int
|
||||
*/
|
||||
public $listeners_start;
|
||||
|
||||
/**
|
||||
* Number of listeners when song playback ended.
|
||||
*
|
||||
* @SWG\Property(example=105)
|
||||
* @var int
|
||||
*/
|
||||
public $listeners_end;
|
||||
|
||||
/**
|
||||
* The sum total change of listeners between the song's start and ending.
|
||||
*
|
||||
* @SWG\Property(example=11)
|
||||
* @var int
|
||||
*/
|
||||
public $delta_total;
|
||||
}
|
|
@ -68,7 +68,7 @@ class SongHistoryRepository extends BaseRepository
|
|||
$return = [];
|
||||
foreach ($history as $sh) {
|
||||
/** @var Entity\SongHistory $sh */
|
||||
$return[] = $sh->api($api_utils);
|
||||
$return[] = $sh->api(new Entity\Api\SongHistory, $api_utils);
|
||||
}
|
||||
|
||||
return $return;
|
||||
|
|
|
@ -459,11 +459,12 @@ class SongHistory
|
|||
}
|
||||
|
||||
/**
|
||||
* @return Api\SongHistory|Api\NowPlayingCurrentSong
|
||||
* @param Api\SongHistory $response
|
||||
* @param \App\ApiUtilities $api
|
||||
* @return Api\SongHistory
|
||||
*/
|
||||
public function api(\App\ApiUtilities $api, $now_playing = false)
|
||||
public function api(Api\SongHistory $response, \App\ApiUtilities $api)
|
||||
{
|
||||
$response = ($now_playing) ? new Api\NowPlayingCurrentSong : new Api\SongHistory;
|
||||
$response->sh_id = (int)$this->id;
|
||||
$response->played_at = (int)$this->timestamp_start;
|
||||
$response->duration = (int)$this->duration;
|
||||
|
@ -476,6 +477,12 @@ class SongHistory
|
|||
$response->playlist = '';
|
||||
}
|
||||
|
||||
if ($response instanceof Api\DetailedSongHistory) {
|
||||
$response->listeners_start = (int)$this->listeners_start;
|
||||
$response->listeners_end = (int)$this->listeners_end;
|
||||
$response->delta_total = (int)$this->delta_total;
|
||||
}
|
||||
|
||||
$response->song = ($this->media)
|
||||
? $this->media->api($api)
|
||||
: $this->song->api($api);
|
||||
|
|
|
@ -180,7 +180,7 @@ class NowPlaying extends TaskAbstract
|
|||
|
||||
$next_song = $this->history_repo->getNextSongForStation($station);
|
||||
if ($next_song instanceof Entity\SongHistory) {
|
||||
$np->playing_next = $next_song->api($this->api_utils);
|
||||
$np->playing_next = $next_song->api(new Entity\Api\SongHistory, $this->api_utils);
|
||||
} else {
|
||||
$np->playing_next = null;
|
||||
}
|
||||
|
@ -209,7 +209,7 @@ class NowPlaying extends TaskAbstract
|
|||
$next_song = $this->history_repo->getNextSongForStation($station);
|
||||
|
||||
if ($next_song instanceof Entity\SongHistory) {
|
||||
$np->playing_next = $next_song->api($this->api_utils);
|
||||
$np->playing_next = $next_song->api(new Entity\Api\SongHistory, $this->api_utils);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -231,7 +231,7 @@ class NowPlaying extends TaskAbstract
|
|||
}
|
||||
|
||||
// Register a new item in song history.
|
||||
$np->now_playing = $sh_obj->api($this->api_utils, true);
|
||||
$np->now_playing = $sh_obj->api(new Entity\Api\NowPlayingCurrentSong, $this->api_utils);
|
||||
}
|
||||
|
||||
$np->update();
|
||||
|
|
|
@ -589,4 +589,51 @@ class Utilities
|
|||
|
||||
return getHostByName(getHostName()) ?? 'localhost';
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten an array from format:
|
||||
* [
|
||||
* 'user' => [
|
||||
* 'id' => 1,
|
||||
* 'name' => 'test',
|
||||
* ]
|
||||
* ]
|
||||
*
|
||||
* to format:
|
||||
* [
|
||||
* 'user.id' => 1,
|
||||
* 'user.name' => 'test',
|
||||
* ]
|
||||
*
|
||||
* This function is used to create replacements for variables in strings.
|
||||
*
|
||||
* @param array|object $array
|
||||
* @param string $separator
|
||||
* @param null $prefix
|
||||
* @return array
|
||||
*/
|
||||
public static function flatten_array($array, $separator = '.', $prefix = null): array
|
||||
{
|
||||
if (!is_array($array)) {
|
||||
if (is_object($array)) {
|
||||
// Quick and dirty conversion from object to array.
|
||||
$array = json_decode(json_encode($array), true);
|
||||
} else {
|
||||
return $array;
|
||||
}
|
||||
}
|
||||
|
||||
$return = [];
|
||||
|
||||
foreach($array as $key => $value) {
|
||||
$return_key = $prefix ? $prefix.$separator.$key : $key;
|
||||
if (\is_array($value)) {
|
||||
$return = array_merge($return, self::flatten_array($value, $separator, $return_key));
|
||||
} else {
|
||||
$return[$return_key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
namespace App\Webhook\Connector;
|
||||
|
||||
use App\Entity;
|
||||
use App\Utilities;
|
||||
use Monolog\Logger;
|
||||
|
||||
abstract class AbstractConnector implements ConnectorInterface
|
||||
|
@ -29,44 +30,6 @@ abstract class AbstractConnector implements ConnectorInterface
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten an array from format:
|
||||
* [
|
||||
* 'user' => [
|
||||
* 'id' => 1,
|
||||
* 'name' => 'test',
|
||||
* ]
|
||||
* ]
|
||||
*
|
||||
* to format:
|
||||
* [
|
||||
* 'user.id' => 1,
|
||||
* 'user.name' => 'test',
|
||||
* ]
|
||||
*
|
||||
* This function is used to create replacements for variables in strings.
|
||||
*
|
||||
* @param array $array
|
||||
* @param string $separator
|
||||
* @param null $prefix
|
||||
* @return array
|
||||
*/
|
||||
protected function _flattenArray(array $array, $separator = '.', $prefix = null): array
|
||||
{
|
||||
$return = [];
|
||||
|
||||
foreach($array as $key => $value) {
|
||||
$return_key = $prefix ? $prefix.$separator.$key : $key;
|
||||
if (\is_array($value)) {
|
||||
$return = array_merge($return, $this->_flattenArray($value, $separator, $return_key));
|
||||
} else {
|
||||
$return[$return_key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace variables in the format {{ blah }} with the flattened contents of the NowPlaying API array.
|
||||
*
|
||||
|
@ -76,7 +39,7 @@ abstract class AbstractConnector implements ConnectorInterface
|
|||
*/
|
||||
public function _replaceVariables(array $raw_vars, Entity\Api\NowPlaying $np): array
|
||||
{
|
||||
$values = $this->_flattenArray(json_decode(json_encode($np), true));
|
||||
$values = Utilities::flatten_array($np, '.');
|
||||
$vars = [];
|
||||
|
||||
foreach($raw_vars as $var_key => $var_value) {
|
||||
|
|
Loading…
Reference in New Issue