#49 -- Begin tracking unique listening clients for royalty reporting purposes.
This commit is contained in:
parent
ee6858f332
commit
9260af42de
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
namespace Entity;
|
||||
|
||||
/**
|
||||
* @Table(name="listener", indexes={
|
||||
* @index(name="update_idx", columns={"listener_uid", "listener_ip"}),
|
||||
* })
|
||||
* @Entity(repositoryClass="Entity\Repository\ListenerRepository")
|
||||
*/
|
||||
class Listener extends \App\Doctrine\Entity
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->timestamp_start = time();
|
||||
}
|
||||
|
||||
/**
|
||||
* @Column(name="id", type="integer")
|
||||
* @Id
|
||||
* @GeneratedValue(strategy="AUTO")
|
||||
*/
|
||||
protected $id;
|
||||
|
||||
/** @Column(name="station_id", type="integer") */
|
||||
protected $station_id;
|
||||
|
||||
/** @Column(name="listener_uid", type="integer") */
|
||||
protected $listener_uid;
|
||||
|
||||
/** @Column(name="listener_ip", type="string", length=45) */
|
||||
protected $listener_ip;
|
||||
|
||||
/** @Column(name="listener_user_agent", type="string", length=255) */
|
||||
protected $listener_user_agent;
|
||||
|
||||
/** @Column(name="timestamp_start", type="integer") */
|
||||
protected $timestamp_start;
|
||||
|
||||
public function getTimestamp()
|
||||
{
|
||||
return $this->timestamp_start;
|
||||
}
|
||||
|
||||
/** @Column(name="timestamp_end", type="integer") */
|
||||
protected $timestamp_end;
|
||||
|
||||
public function getConnectedSeconds()
|
||||
{
|
||||
return $this->timestamp_end - $this->timestamp_start;
|
||||
}
|
||||
|
||||
/**
|
||||
* @ManyToOne(targetEntity="Station", inversedBy="history")
|
||||
* @JoinColumns({
|
||||
* @JoinColumn(name="station_id", referencedColumnName="id", onDelete="CASCADE")
|
||||
* })
|
||||
*/
|
||||
protected $station;
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
namespace Migration;
|
||||
|
||||
use Doctrine\DBAL\Migrations\AbstractMigration;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
class Version20170516073708 extends AbstractMigration
|
||||
{
|
||||
/**
|
||||
* @param Schema $schema
|
||||
*/
|
||||
public function up(Schema $schema)
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');
|
||||
|
||||
$this->addSql('CREATE TABLE listener (id INT AUTO_INCREMENT NOT NULL, station_id INT NOT NULL, listener_uid INT NOT NULL, listener_ip VARCHAR(45) NOT NULL, listener_user_agent VARCHAR(255) NOT NULL, timestamp_start INT NOT NULL, timestamp_end INT NOT NULL, INDEX IDX_959C342221BDB235 (station_id), INDEX update_idx (listener_uid, listener_ip), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB');
|
||||
$this->addSql('ALTER TABLE listener ADD CONSTRAINT FK_959C342221BDB235 FOREIGN KEY (station_id) REFERENCES station (id) ON DELETE CASCADE');
|
||||
$this->addSql('ALTER TABLE song_history ADD unique_listeners SMALLINT DEFAULT NULL');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Schema $schema
|
||||
*/
|
||||
public function down(Schema $schema)
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');
|
||||
|
||||
$this->addSql('DROP TABLE listener');
|
||||
$this->addSql('ALTER TABLE song_history DROP unique_listeners');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
<?php
|
||||
namespace Entity\Repository;
|
||||
|
||||
use Entity;
|
||||
|
||||
class ListenerRepository extends \App\Doctrine\Repository
|
||||
{
|
||||
/**
|
||||
* Get the number of unique listeners for a station during a specified time period.
|
||||
*
|
||||
* @param Entity\Station $station
|
||||
* @param $timestamp_start
|
||||
* @param $timestamp_end
|
||||
* @return mixed
|
||||
*/
|
||||
public function getUniqueListeners(Entity\Station $station, $timestamp_start, $timestamp_end)
|
||||
{
|
||||
return $this->_em->createQuery('SELECT COUNT(l.id)
|
||||
FROM '.$this->_entityName.' l
|
||||
WHERE l.station_id = :station_id
|
||||
AND l.timestamp_start <= :end
|
||||
AND l.timestamp_end >= :start')
|
||||
->setParameter('station_id', $station->id)
|
||||
->setParameter('end', $timestamp_end)
|
||||
->setParameter('start', $timestamp_start)
|
||||
->getSingleScalarResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update listener data for a station.
|
||||
*
|
||||
* @param Entity\Station $station
|
||||
* @param $clients
|
||||
* @return Entity\SongHistory|mixed
|
||||
*/
|
||||
public function update(Entity\Station $station, $clients)
|
||||
{
|
||||
$clients = (array)$clients;
|
||||
|
||||
$listener_ids = [];
|
||||
|
||||
foreach($clients as $client) {
|
||||
// Check for an existing record for this client.
|
||||
$existing_id = $this->_em->createQuery('SELECT l.id FROM '.$this->_entityName.' l
|
||||
WHERE l.station_id = :station_id
|
||||
AND l.listener_uid = :uid
|
||||
AND l.listener_ip = :ip
|
||||
AND l.timestamp_end = 0')
|
||||
->setParameter('station_id', $station->id)
|
||||
->setParameter('uid', $client['uid'])
|
||||
->setParameter('ip', $client['ip'])
|
||||
->getSingleScalarResult();
|
||||
|
||||
$listener_ids[] = $existing_id;
|
||||
}
|
||||
|
||||
$this->_em->flush();
|
||||
|
||||
// Mark the end of all other clients on this station.
|
||||
$this->_em->createQuery('UPDATE '.$this->_entityName.' l
|
||||
SET l.timestamp_end = :time
|
||||
WHERE l.station_id = :station_id
|
||||
AND l.timestamp_end = 0
|
||||
AND l.id NOT IN (:ids)')
|
||||
->setParameter('time', time())
|
||||
->setParameter('station_id', $station->id)
|
||||
->setParameter('ids', $listener_ids)
|
||||
->execute();
|
||||
}
|
||||
}
|
|
@ -91,6 +91,10 @@ class SongHistoryRepository extends \App\Doctrine\Repository
|
|||
$last_sh->delta_negative = $delta_negative;
|
||||
$last_sh->delta_total = $delta_total;
|
||||
|
||||
/** @var ListenerRepository $listener_repo */
|
||||
$listener_repo = $this->_em->getRepository(Entity\Listener::class);
|
||||
$last_sh->unique_listeners = $listener_repo->getUniqueListeners($station, $last_sh->timestamp_start, time());
|
||||
|
||||
$this->_em->persist($last_sh);
|
||||
}
|
||||
|
||||
|
|
|
@ -19,6 +19,8 @@ class SongHistory extends \App\Doctrine\Entity
|
|||
$this->timestamp_end = 0;
|
||||
$this->listeners_end = 0;
|
||||
|
||||
$this->unique_listeners = 0;
|
||||
|
||||
$this->delta_total = 0;
|
||||
$this->delta_negative = 0;
|
||||
$this->delta_positive = 0;
|
||||
|
@ -68,6 +70,9 @@ class SongHistory extends \App\Doctrine\Entity
|
|||
/** @Column(name="listeners_end", type="smallint", nullable=true) */
|
||||
protected $listeners_end;
|
||||
|
||||
/** @Column(name="unique_listeners", type="smallint", nullable=true) */
|
||||
protected $unique_listeners;
|
||||
|
||||
/** @Column(name="delta_total", type="smallint") */
|
||||
protected $delta_total;
|
||||
|
||||
|
|
|
@ -64,6 +64,10 @@ class Curl
|
|||
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
|
||||
curl_setopt($curl, CURLOPT_MAXREDIRS, 3);
|
||||
|
||||
if (!empty($c_opts['basic_auth'])) {
|
||||
curl_setopt($curl, CURLOPT_USERPWD, $c_opts['basic_auth']);
|
||||
}
|
||||
|
||||
// Custom DNS management.
|
||||
curl_setopt($curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
|
||||
curl_setopt($curl, CURLOPT_DNS_CACHE_TIMEOUT, 600);
|
||||
|
|
|
@ -191,7 +191,7 @@ class LiquidSoap extends BackendAbstract
|
|||
|
||||
$output_params = [
|
||||
$output_format, // Required output format (%mp3 etc)
|
||||
'id="radio_out_' . $mount_row->id . '"',
|
||||
'id="radio_out_' . $i . '"',
|
||||
'host = "localhost"',
|
||||
'port = ' . ($broadcast_port),
|
||||
'password = "' . $broadcast_source_pw . ':#'.$i.'"',
|
||||
|
@ -206,7 +206,10 @@ class LiquidSoap extends BackendAbstract
|
|||
|
||||
case 'icecast':
|
||||
default:
|
||||
$i = 0;
|
||||
foreach ($this->station->mounts as $mount_row) {
|
||||
$i++;
|
||||
|
||||
if (!$mount_row->enable_autodj) {
|
||||
continue;
|
||||
}
|
||||
|
@ -223,7 +226,7 @@ class LiquidSoap extends BackendAbstract
|
|||
if (!empty($output_format)) {
|
||||
$output_params = [
|
||||
$output_format, // Required output format (%mp3 or %ogg)
|
||||
'id="radio_out_' . $mount_row->id . '"',
|
||||
'id="radio_out_' . $i . '"',
|
||||
'host = "localhost"',
|
||||
'port = ' . $broadcast_port,
|
||||
'password = "' . $broadcast_source_pw . '"',
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<?php
|
||||
namespace AzuraCast\Radio\Frontend;
|
||||
|
||||
use App\Debug;
|
||||
use App\Service\Curl;
|
||||
|
||||
abstract class FrontendAbstract extends \AzuraCast\Radio\AdapterAbstract
|
||||
|
@ -110,6 +111,8 @@ abstract class FrontendAbstract extends \AzuraCast\Radio\AdapterAbstract
|
|||
$np['listeners']['total'] = $np['listeners']['current'];
|
||||
}
|
||||
|
||||
Debug::print_r($np);
|
||||
|
||||
return $np;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<?php
|
||||
namespace AzuraCast\Radio\Frontend;
|
||||
|
||||
use App\Debug;
|
||||
use App\Utilities;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Entity\StationMount;
|
||||
|
@ -11,31 +12,29 @@ class IceCast extends FrontendAbstract
|
|||
protected function _getNowPlaying(&$np)
|
||||
{
|
||||
$fe_config = (array)$this->station->frontend_config;
|
||||
$reader = new \App\Xml\Reader();
|
||||
|
||||
$radio_port = $fe_config['port'];
|
||||
$np_url = 'http://localhost:' . $radio_port . '/admin/stats';
|
||||
|
||||
$np_url = 'http://localhost:' . $radio_port . '/status-json.xsl';
|
||||
Debug::log($np_url);
|
||||
|
||||
\App\Debug::log($np_url);
|
||||
|
||||
$return_raw = $this->getUrl($np_url);
|
||||
$return_raw = $this->getUrl($np_url, [
|
||||
'basic_auth' => 'admin:'.$fe_config['admin_pw'],
|
||||
]);
|
||||
|
||||
if (!$return_raw) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$return = @json_decode($return_raw, true);
|
||||
$return = $reader->fromString($return_raw);
|
||||
Debug::print_r($return);
|
||||
|
||||
\App\Debug::print_r($return);
|
||||
|
||||
if (!$return || !isset($return['icestats']['source'])) {
|
||||
if (!$return || empty($return['source'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$sources = $return['icestats']['source'];
|
||||
|
||||
if (empty($sources)) {
|
||||
return false;
|
||||
}
|
||||
$sources = $return['source'];
|
||||
|
||||
if (key($sources) === 0) {
|
||||
$mounts = $sources;
|
||||
|
@ -43,10 +42,6 @@ class IceCast extends FrontendAbstract
|
|||
$mounts = [$sources];
|
||||
}
|
||||
|
||||
if (count($mounts) == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$mounts = array_filter($mounts, function ($mount) {
|
||||
return (!empty($mount['title']) || !empty($mount['artist']));
|
||||
});
|
||||
|
@ -81,6 +76,41 @@ class IceCast extends FrontendAbstract
|
|||
|
||||
$np['listeners']['current'] = (int)$temp_array['listeners'];
|
||||
|
||||
if (!empty($temp_array['@mount'])) {
|
||||
// Attempt to fetch detailed listener information for better unique statistics.
|
||||
$selected_mount = $temp_array['@mount'];
|
||||
|
||||
$listeners_url = 'http://localhost:' . $radio_port . '/admin/listclients?mount='.urlencode($selected_mount);
|
||||
|
||||
$return_raw = $this->getUrl($listeners_url, [
|
||||
'basic_auth' => 'admin:'.$fe_config['admin_pw'],
|
||||
]);
|
||||
|
||||
if (!empty($return_raw)) {
|
||||
$listeners_raw = $reader->fromString($return_raw);
|
||||
|
||||
$np['listeners']['clients'] = [];
|
||||
|
||||
if (!empty($listeners_raw['source']['listener']))
|
||||
{
|
||||
if (key($listeners_raw['source']['listener']) === 0) {
|
||||
$listeners = $listeners_raw['source']['listener'];
|
||||
} else {
|
||||
$listeners = [$listeners_raw['source']['listener']];
|
||||
}
|
||||
|
||||
foreach($listeners as $listener) {
|
||||
$np['listeners']['clients'][] = [
|
||||
'uid' => $listener['ID'],
|
||||
'ip' => $listener['IP'],
|
||||
'user_agent' => $listener['UserAgent'],
|
||||
'connected_seconds' => $listener['Connected'],
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -16,35 +16,55 @@ class ShoutCast2 extends FrontendAbstract
|
|||
$fe_config = (array)$this->station->frontend_config;
|
||||
$radio_port = $fe_config['port'];
|
||||
|
||||
$np_url = 'http://localhost:' . $radio_port . '/stats';
|
||||
$np_url = 'http://localhost:' . $radio_port . '/statistics?json=1';
|
||||
$return_raw = $this->getUrl($np_url);
|
||||
|
||||
if (empty($return_raw)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$current_data = \App\Export::xml_to_array($return_raw);
|
||||
$current_data = json_decode($return_raw, true);
|
||||
|
||||
Debug::print_r($return_raw);
|
||||
Debug::print_r($current_data);
|
||||
|
||||
$song_data = $current_data['SHOUTCASTSERVER'];
|
||||
|
||||
Debug::print_r($song_data);
|
||||
$song_data = $current_data['streams'][0];
|
||||
|
||||
$np['meta']['status'] = 'online';
|
||||
$np['meta']['bitrate'] = $song_data['BITRATE'];
|
||||
$np['meta']['format'] = $song_data['CONTENT'];
|
||||
$np['meta']['bitrate'] = $song_data['bitrate'];
|
||||
$np['meta']['format'] = $song_data['content'];
|
||||
|
||||
$np['current_song'] = $this->getSongFromString($song_data['SONGTITLE'], '-');
|
||||
$np['current_song'] = $this->getSongFromString($song_data['songtitle'], '-');
|
||||
|
||||
$u_list = (int)$song_data['UNIQUELISTENERS'];
|
||||
$t_list = (int)$song_data['CURRENTLISTENERS'];
|
||||
$u_list = (int)$song_data['uniquelisteners'];
|
||||
$t_list = (int)$song_data['currentlisteners'];
|
||||
$np['listeners'] = [
|
||||
'current' => $this->getListenerCount($u_list, $t_list),
|
||||
'unique' => $u_list,
|
||||
'total' => $t_list,
|
||||
];
|
||||
|
||||
// Attempt to fetch detailed listener information for better unique statistics.
|
||||
$listeners_url = 'http://localhost:' . $radio_port . '/admin.cgi?sid=1&mode=viewjson&page=3';
|
||||
|
||||
$return_raw = $this->getUrl($listeners_url, [
|
||||
'basic_auth' => 'admin:'.$fe_config['admin_pw'],
|
||||
]);
|
||||
|
||||
if (!empty($return_raw)) {
|
||||
$listeners = json_decode($return_raw, true);
|
||||
|
||||
$np['listeners']['clients'] = [];
|
||||
|
||||
foreach((array)$listeners as $listener) {
|
||||
$np['listeners']['clients'][] = [
|
||||
'uid' => $listener['uid'],
|
||||
'ip' => $listener['xff'] ?: $listener['hostname'],
|
||||
'user_agent' => $listener['useragent'],
|
||||
'connected_seconds' => $listener['connecttime'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -3,9 +3,7 @@ namespace AzuraCast\Sync;
|
|||
|
||||
use App\Debug;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Entity\Song;
|
||||
use Entity\SongHistory;
|
||||
use Entity\Station;
|
||||
use Entity;
|
||||
|
||||
class NowPlaying extends SyncAbstract
|
||||
{
|
||||
|
@ -57,15 +55,29 @@ class NowPlaying extends SyncAbstract
|
|||
$nowplaying[$station]['cache'] = 'database';
|
||||
}
|
||||
|
||||
$this->di['em']->getRepository('Entity\Settings')->setSetting('nowplaying', $nowplaying);
|
||||
$this->di['em']->getRepository(Entity\Settings::class)
|
||||
->setSetting('nowplaying', $nowplaying);
|
||||
}
|
||||
|
||||
/** @var Entity\Repository\SongHistoryRepository */
|
||||
protected $history_repo;
|
||||
|
||||
/** @var Entity\Repository\SongRepository */
|
||||
protected $song_repo;
|
||||
|
||||
/** @var Entity\Repository\ListenerRepository */
|
||||
protected $listener_repo;
|
||||
|
||||
protected function _loadNowPlaying()
|
||||
{
|
||||
/** @var EntityManager $em */
|
||||
$em = $this->di['em'];
|
||||
$stations = $em->getRepository(Station::class)->findAll();
|
||||
|
||||
$this->history_repo = $em->getRepository(Entity\SongHistory::class);
|
||||
$this->song_repo = $em->getRepository(Entity\Song::class);
|
||||
$this->listener_repo = $em->getRepository(Entity\Listener::class);
|
||||
|
||||
$stations = $em->getRepository(Entity\Station::class)->findAll();
|
||||
$nowplaying = [];
|
||||
|
||||
foreach ($stations as $station) {
|
||||
|
@ -83,10 +95,10 @@ class NowPlaying extends SyncAbstract
|
|||
/**
|
||||
* Generate Structured NowPlaying Data
|
||||
*
|
||||
* @param Station $station
|
||||
* @param Entity\Station $station
|
||||
* @return array Structured NowPlaying Data
|
||||
*/
|
||||
protected function _processStation(Station $station)
|
||||
protected function _processStation(Entity\Station $station)
|
||||
{
|
||||
/** @var EntityManager $em */
|
||||
$em = $this->di['em'];
|
||||
|
@ -94,7 +106,7 @@ class NowPlaying extends SyncAbstract
|
|||
$np_old = (array)$station->nowplaying_data;
|
||||
|
||||
$np = [];
|
||||
$np['station'] = Station::api($station, $this->di);
|
||||
$np['station'] = Entity\Station::api($station, $this->di);
|
||||
|
||||
$frontend_adapter = $station->getFrontendAdapter($this->di);
|
||||
$np_new = $frontend_adapter->getNowPlaying();
|
||||
|
@ -103,26 +115,32 @@ class NowPlaying extends SyncAbstract
|
|||
$np['listeners'] = $np_new['listeners'];
|
||||
|
||||
// Pull from current NP data if song details haven't changed.
|
||||
$current_song_hash = Song::getSongHash($np_new['current_song']);
|
||||
$current_song_hash = Entity\Song::getSongHash($np_new['current_song']);
|
||||
|
||||
if (empty($np['current_song']['text'])) {
|
||||
$np['current_song'] = [];
|
||||
$np['song_history'] = $em->getRepository(SongHistory::class)->getHistoryForStation($station);
|
||||
$np['song_history'] = $this->history_repo->getHistoryForStation($station);
|
||||
} else {
|
||||
if (strcmp($current_song_hash, $np_old['current_song']['id']) == 0) {
|
||||
$np['song_history'] = $np_old['song_history'];
|
||||
|
||||
$song_obj = $em->getRepository(Song::class)->find($current_song_hash);
|
||||
$song_obj = $this->song_repo->find($current_song_hash);
|
||||
} else {
|
||||
$np['song_history'] = $em->getRepository(SongHistory::class)->getHistoryForStation($station);
|
||||
$np['song_history'] = $this->history_repo->getHistoryForStation($station);
|
||||
|
||||
$song_obj = $em->getRepository(Song::class)->getOrCreate($np_new['current_song'], true);
|
||||
$song_obj = $this->song_repo->getOrCreate($np_new['current_song'], true);
|
||||
}
|
||||
|
||||
// Update detailed listener statistics, if they exist for the station
|
||||
if (isset($np['listeners']['clients'])) {
|
||||
$this->listener_repo->update($station, $np['listeners']['clients']);
|
||||
unset($np['listeners']['clients']);
|
||||
}
|
||||
|
||||
// Register a new item in song history.
|
||||
$sh_obj = $em->getRepository(SongHistory::class)->register($song_obj, $station, $np);
|
||||
$sh_obj = $this->history_repo->register($song_obj, $station, $np);
|
||||
|
||||
$current_song = Song::api($song_obj);
|
||||
$current_song = Entity\Song::api($song_obj);
|
||||
$current_song['sh_id'] = $sh_obj->id;
|
||||
|
||||
$np['current_song'] = $current_song;
|
||||
|
|
Loading…
Reference in New Issue