mirror of
https://github.com/AzuraCast/AzuraCast.git
synced 2024-06-17 22:47:04 +00:00
Major push: live station updates
Implements both a frontend websocket-based live notifications system powered by the nginx nchan module and a backend station watcher (currently for Docker only) that triggers push notifications immediately upon song or listener count changes. Both result in more timely, more accurate updates to now-playing data on the site.
This commit is contained in:
parent
cebb1fb361
commit
568af47c5b
|
@ -47,11 +47,6 @@ return function (\Slim\Container $di, $settings) {
|
|||
'modelPath' => APP_INCLUDE_BASE . '/models',
|
||||
'conn' => [
|
||||
'driver' => 'pdo_mysql',
|
||||
'host' => $_ENV['db_host'] ?? ((APP_INSIDE_DOCKER) ? 'mariadb' : 'localhost'),
|
||||
'port' => $_ENV['db_port'] ?? '3306',
|
||||
'dbname' => $_ENV['db_name'] ?? 'azuracast',
|
||||
'user' => $_ENV['db_username'] ?? 'azuracast',
|
||||
'password' => (APP_INSIDE_DOCKER) ? 'azur4c457' : $_ENV['db_password'],
|
||||
'charset' => 'utf8mb4',
|
||||
'driverOptions' => [
|
||||
\PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci',
|
||||
|
@ -59,6 +54,20 @@ return function (\Slim\Container $di, $settings) {
|
|||
]
|
||||
];
|
||||
|
||||
if (APP_INSIDE_DOCKER) {
|
||||
$options['conn']['host'] = 'mariadb';
|
||||
$options['conn']['port'] = 3306;
|
||||
$options['conn']['dbname'] = 'azuracast';
|
||||
$options['conn']['user'] = 'azuracast';
|
||||
$options['conn']['password'] = 'azur4c457';
|
||||
} else {
|
||||
$options['conn']['host'] = $_ENV['db_host'] ?? 'localhost';
|
||||
$options['conn']['port'] = $_ENV['db_port'] ?? '3306';
|
||||
$options['conn']['dbname'] = $_ENV['db_name'] ?? 'azuracast';
|
||||
$options['conn']['user'] = $_ENV['db_username'] ?? 'azuracast';
|
||||
$options['conn']['password'] = $_ENV['db_password'];
|
||||
}
|
||||
|
||||
\Doctrine\Common\Proxy\Autoloader::register($options['proxyPath'], $options['proxyNamespace']);
|
||||
|
||||
// Fetch and store entity manager.
|
||||
|
|
|
@ -54,4 +54,12 @@ class NowPlaying
|
|||
* @var string
|
||||
*/
|
||||
public $cache;
|
||||
|
||||
/**
|
||||
* Update any variable items in the feed.
|
||||
*/
|
||||
public function update()
|
||||
{
|
||||
$this->now_playing->recalculate();
|
||||
}
|
||||
}
|
34
app/models/Migration/Version20170906080352.php
Normal file
34
app/models/Migration/Version20170906080352.php
Normal file
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
namespace Migration;
|
||||
|
||||
use Doctrine\DBAL\Migrations\AbstractMigration;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
class Version20170906080352 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('ALTER TABLE station ADD adapter_api_key VARCHAR(150) DEFAULT NULL, ADD nowplaying_timestamp INT 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('ALTER TABLE station DROP adapter_api_key, DROP nowplaying_timestamp');
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ namespace Entity;
|
|||
use AzuraCast\Radio\Frontend\FrontendAbstract;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Interop\Container\ContainerInterface;
|
||||
|
||||
/**
|
||||
|
@ -51,6 +52,12 @@ class Station
|
|||
*/
|
||||
protected $backend_config;
|
||||
|
||||
/**
|
||||
* @Column(name="adapter_api_key", type="string", length=150, nullable=true)
|
||||
* @var string|null
|
||||
*/
|
||||
protected $adapter_api_key;
|
||||
|
||||
/**
|
||||
* @Column(name="description", type="text", nullable=true)
|
||||
* @var string|null
|
||||
|
@ -81,6 +88,12 @@ class Station
|
|||
*/
|
||||
protected $nowplaying;
|
||||
|
||||
/**
|
||||
* @Column(name="nowplaying_timestamp", type="integer", nullable=true)
|
||||
* @var int
|
||||
*/
|
||||
protected $nowplaying_timestamp;
|
||||
|
||||
/**
|
||||
* @Column(name="automation_settings", type="json_array", nullable=true)
|
||||
* @var array|null
|
||||
|
@ -292,6 +305,33 @@ class Station
|
|||
$this->backend_config = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return null|string
|
||||
*/
|
||||
public function getAdapterApiKey(): ?string
|
||||
{
|
||||
return $this->adapter_api_key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random new adapter API key.
|
||||
*/
|
||||
public function generateAdapterApiKey()
|
||||
{
|
||||
$this->adapter_api_key = bin2hex(random_bytes(50));
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate the supplied adapter API key.
|
||||
*
|
||||
* @param $api_key
|
||||
* @return bool
|
||||
*/
|
||||
public function validateAdapterApiKey($api_key)
|
||||
{
|
||||
return hash_equals($api_key, $this->adapter_api_key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return null|string
|
||||
*/
|
||||
|
@ -410,6 +450,15 @@ class Station
|
|||
public function setNowplaying($nowplaying = null)
|
||||
{
|
||||
$this->nowplaying = $nowplaying;
|
||||
$this->nowplaying_timestamp = time();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getNowplayingTimestamp(): int
|
||||
{
|
||||
return (int)$this->nowplaying_timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -633,6 +682,15 @@ class Station
|
|||
return;
|
||||
}
|
||||
|
||||
/** @var EntityManager $em */
|
||||
$em = $di['em'];
|
||||
|
||||
// Always regenerate a new API auth key when rewriting service configuration.
|
||||
$this->generateAdapterApiKey();
|
||||
$em->persist($this);
|
||||
$em->flush();
|
||||
|
||||
// Initialize adapters.
|
||||
$config_path = $this->getRadioConfigDir();
|
||||
$supervisor_config = [];
|
||||
$supervisor_config_path = $config_path . '/supervisord.conf';
|
||||
|
@ -656,7 +714,10 @@ class Station
|
|||
list($backend_group, $backend_program) = explode(':', $backend_name);
|
||||
|
||||
$frontend_name = $frontend->getProgramName();
|
||||
list($frontend_group, $frontend_program) = explode(':', $frontend_name);
|
||||
list(,$frontend_program) = explode(':', $frontend_name);
|
||||
|
||||
$frontend_watch_name = $frontend->getWatchProgramName();
|
||||
list(,$frontend_watch_program) = explode(':', $frontend_watch_name);
|
||||
|
||||
// Write group section of config
|
||||
$programs = [];
|
||||
|
@ -666,6 +727,9 @@ class Station
|
|||
if ($frontend->hasCommand()) {
|
||||
$programs[] = $frontend_program;
|
||||
}
|
||||
if ($frontend->hasWatchCommand()) {
|
||||
$programs[] = $frontend_watch_program;
|
||||
}
|
||||
|
||||
$supervisor_config[] = '[group:' . $backend_group . ']';
|
||||
$supervisor_config[] = 'programs=' . implode(',', $programs);
|
||||
|
@ -689,6 +753,24 @@ class Station
|
|||
$supervisor_config[] = '';
|
||||
}
|
||||
|
||||
// Write frontend watcher program
|
||||
if ($frontend->hasWatchCommand()) {
|
||||
$supervisor_config[] = '[program:' . $frontend_watch_program . ']';
|
||||
$supervisor_config[] = 'directory=' . $config_path;
|
||||
$supervisor_config[] = 'command=' . $frontend->getWatchCommand();
|
||||
$supervisor_config[] = 'user=azuracast';
|
||||
$supervisor_config[] = 'priority=95';
|
||||
|
||||
if (APP_INSIDE_DOCKER) {
|
||||
$supervisor_config[] = 'stdout_logfile=/dev/stdout';
|
||||
$supervisor_config[] = 'stdout_logfile_maxbytes=0';
|
||||
$supervisor_config[] = 'stderr_logfile=/dev/stderr';
|
||||
$supervisor_config[] = 'stderr_logfile_maxbytes=0';
|
||||
}
|
||||
|
||||
$supervisor_config[] = '';
|
||||
}
|
||||
|
||||
// Write backend
|
||||
if ($backend->hasCommand()) {
|
||||
$supervisor_config[] = '[program:' . $backend_program . ']';
|
||||
|
|
|
@ -19,17 +19,11 @@ class InternalController extends BaseController
|
|||
throw new \App\Exception('Station not found.');
|
||||
}
|
||||
|
||||
$backend_adapter = $station->getBackendAdapter($this->di);
|
||||
|
||||
if (!($backend_adapter instanceof \AzuraCast\Radio\Backend\LiquidSoap)) {
|
||||
throw new \App\Exception('Not a LiquidSoap station.');
|
||||
}
|
||||
|
||||
try {
|
||||
$this->checkStationPermission($station, 'view administration');
|
||||
} catch (\App\Exception\PermissionDenied $e) {
|
||||
$auth_key = $this->getParam('api_auth', '');
|
||||
if (!$backend_adapter->validateApiPassword($auth_key)) {
|
||||
if (!$station->validateAdapterApiKey($auth_key)) {
|
||||
throw new \App\Exception\PermissionDenied();
|
||||
}
|
||||
}
|
||||
|
@ -64,6 +58,12 @@ class InternalController extends BaseController
|
|||
|
||||
public function nextsongAction()
|
||||
{
|
||||
$backend_adapter = $this->station->getBackendAdapter($this->di);
|
||||
|
||||
if (!($backend_adapter instanceof \AzuraCast\Radio\Backend\LiquidSoap)) {
|
||||
throw new \App\Exception('Not a LiquidSoap station.');
|
||||
}
|
||||
|
||||
/** @var Entity\Repository\SongHistoryRepository $history_repo */
|
||||
$history_repo = $this->em->getRepository(Entity\SongHistory::class);
|
||||
|
||||
|
@ -81,6 +81,21 @@ class InternalController extends BaseController
|
|||
}
|
||||
}
|
||||
|
||||
public function notifyAction()
|
||||
{
|
||||
$payload = $this->request->getBody()->getContents();
|
||||
|
||||
if (!APP_IN_PRODUCTION) {
|
||||
$log = date('Y-m-d g:i:s')."\n".$this->station->getName()."\n".$payload."\n\n";
|
||||
file_put_contents(APP_INCLUDE_TEMP.'/notify.log', $log, \FILE_APPEND);
|
||||
}
|
||||
|
||||
$np_sync = new \AzuraCast\Sync\NowPlaying($this->di);
|
||||
$np_sync->processStation($this->station, $payload);
|
||||
|
||||
return $this->_return('received');
|
||||
}
|
||||
|
||||
protected function _return($output)
|
||||
{
|
||||
$this->response->getBody()->write($output);
|
||||
|
|
|
@ -19,6 +19,7 @@ return function(\Slim\App $app) {
|
|||
|
||||
$this->map(['GET', 'POST'], '/auth', 'api:internal:auth')->setName('api:internal:auth');
|
||||
$this->map(['GET', 'POST'], '/nextsong', 'api:internal:nextsong')->setName('api:internal:nextsong');
|
||||
$this->map(['GET', 'POST'], '/notify', 'api:internal:notify')->setName('api:internal:notify');
|
||||
|
||||
});
|
||||
|
||||
|
|
|
@ -10,7 +10,8 @@
|
|||
<?php endif; ?>
|
||||
|
||||
<script type="text/javascript" src="<?=$url->content('vendors/bower_components/store-js/store.min.js') ?>"></script>
|
||||
<script type="text/javascript" src="<?=$url->content('js/radio.js') ?>"></script>
|
||||
<script type="text/javascript" src="<?=$assets->getPath('js/radio.js') ?>"></script>
|
||||
<script type="text/javascript" src="<?=$assets->getPath('js/nchan.js') ?>"></script>
|
||||
<?php $this->stop('custom_js') ?>
|
||||
|
||||
<div class="block-header">
|
||||
|
@ -47,7 +48,6 @@
|
|||
<h2><?=_('Station Overview') ?></h2>
|
||||
|
||||
<ul class="actions">
|
||||
<li><a class="btn-refresh" href="#" title="<?=_('Refresh') ?>"><i class="zmdi zmdi-refresh"></i></a></li>
|
||||
<?php if ($acl->isAllowed('administer stations')): ?>
|
||||
<li><a class="" title="<?=_('Add') ?>" href="<?=$url->route(['module' => 'admin', 'controller' => 'stations', 'action' => 'edit']) ?>"><i class="zmdi zmdi-plus"></i></a></li>
|
||||
<?php endif; ?>
|
||||
|
@ -189,15 +189,14 @@ $(function () {
|
|||
});
|
||||
});
|
||||
|
||||
var np_timeout;
|
||||
$(function() {
|
||||
var sub = new NchanSubscriber('/api/live/nowplaying');
|
||||
|
||||
function nowPlaying() {
|
||||
clearTimeout(np_timeout);
|
||||
$('.btn-refresh i').addClass('zmdi-hc-spin');
|
||||
sub.on("message", function(message, message_metadata) {
|
||||
var data = JSON.parse(message);
|
||||
|
||||
$.getJSON('<?=$url->route(['module' => 'api', 'controller' => 'nowplaying']) ?>', {}, function(data) {
|
||||
$.each(data, function (i, row) {
|
||||
var station_row = $('#station_'+row.station.id);
|
||||
var station_row = $('#station_' + row.station.id);
|
||||
|
||||
if (station_row.length) {
|
||||
|
||||
|
@ -213,19 +212,8 @@ function nowPlaying() {
|
|||
station_row.find('.nowplaying-listeners').text(row.listeners.total)
|
||||
}
|
||||
});
|
||||
|
||||
$('.btn-refresh i').removeClass('zmdi-hc-spin');
|
||||
np_timeout = setTimeout('nowPlaying()', 15*1000);
|
||||
});
|
||||
}
|
||||
|
||||
$(function() {
|
||||
nowPlaying();
|
||||
|
||||
$('.btn-refresh').on('click', function (e) {
|
||||
e.preventDefault();
|
||||
nowPlaying();
|
||||
return false;
|
||||
});
|
||||
sub.start();
|
||||
});
|
||||
</script>
|
|
@ -5,6 +5,7 @@
|
|||
<?php $this->start('custom_js') ?>
|
||||
<script type="text/javascript" src="<?=$url->content('vendors/bower_components/store-js/store.min.js') ?>"></script>
|
||||
<script type="text/javascript" src="<?=$assets->getPath('js/radio.js') ?>"></script>
|
||||
<script type="text/javascript" src="<?=$assets->getPath('js/nchan.js') ?>"></script>
|
||||
<?php $this->stop('custom_js') ?>
|
||||
|
||||
<ul class="actions pull-right">
|
||||
|
@ -25,12 +26,12 @@
|
|||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
function nowPlaying() {
|
||||
$.getJSON('<?=$url->route([
|
||||
'module' => 'api',
|
||||
'controller' => 'nowplaying',
|
||||
'station' => $station->getId()
|
||||
]) ?>', {}, function (row) {
|
||||
$(function() {
|
||||
var sub = new NchanSubscriber('/api/live/nowplaying/<?=$station->getId() ?>');
|
||||
|
||||
sub.on("message", function(message, message_metadata) {
|
||||
var row = JSON.parse(message);
|
||||
|
||||
var station_row = $('#station_'+row.station.id);
|
||||
|
||||
if ('mediaSession' in navigator) {
|
||||
|
@ -43,12 +44,6 @@ function nowPlaying() {
|
|||
station_row.find('.nowplaying-title').text(row.now_playing.song.title);
|
||||
station_row.find('.nowplaying-artist').text(row.now_playing.song.artist);
|
||||
station_row.find('.nowplaying-listeners').text(row.listeners.total);
|
||||
|
||||
setTimeout('nowPlaying()', 30*1000);
|
||||
});
|
||||
}
|
||||
|
||||
$(function() {
|
||||
nowPlaying();
|
||||
});
|
||||
</script>
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
<script type="text/javascript" src="<?=$url->content('vendors/bower_components/store-js/store.min.js') ?>"></script>
|
||||
<script type="text/javascript" src="<?=$assets->getPath('js/radio.js') ?>"></script>
|
||||
<script type="text/javascript" src="<?=$assets->getPath('js/nchan.js') ?>"></script>
|
||||
<?php $this->stop('custom_js') ?>
|
||||
|
||||
<?php $this->start('custom_css') ?>
|
||||
|
@ -108,12 +109,47 @@
|
|||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
function nowPlaying() {
|
||||
$.getJSON('<?=$url->route([
|
||||
'module' => 'api',
|
||||
'controller' => 'nowplaying',
|
||||
'station' => $station->getId()
|
||||
]) ?>', {}, function (row) {
|
||||
function iterateTimer() {
|
||||
$('.nowplaying-progress:visible').each(function () {
|
||||
var time_played = $(this).data('time-played') + 1;
|
||||
var time_total = $(this).data('time-total');
|
||||
|
||||
if (time_played > time_total) {
|
||||
time_played = time_total;
|
||||
}
|
||||
|
||||
var time_display = formatTime(time_played) + ' / ' + formatTime(time_total);
|
||||
|
||||
$(this).data('time-played', time_played)
|
||||
.text(time_display)
|
||||
});
|
||||
}
|
||||
|
||||
function formatTime(time) {
|
||||
var sec_num = parseInt(time, 10);
|
||||
|
||||
var hours = Math.floor(sec_num / 3600);
|
||||
var minutes = Math.floor((sec_num - (hours * 3600)) / 60);
|
||||
var seconds = sec_num - (hours * 3600) - (minutes * 60);
|
||||
|
||||
if (hours < 10) {
|
||||
hours = "0" + hours;
|
||||
}
|
||||
if (minutes < 10) {
|
||||
minutes = "0" + minutes;
|
||||
}
|
||||
if (seconds < 10) {
|
||||
seconds = "0" + seconds;
|
||||
}
|
||||
return (hours !== "00" ? hours + ':' : "") + minutes + ':' + seconds;
|
||||
}
|
||||
|
||||
$(function() {
|
||||
var sub = new NchanSubscriber('/api/live/nowplaying/<?=$station->getId() ?>');
|
||||
|
||||
sub.on("message", function(message, message_metadata) {
|
||||
var row = JSON.parse(message);
|
||||
|
||||
var station_row = $('#station_'+row.station.id);
|
||||
|
||||
if ('mediaSession' in navigator) {
|
||||
|
@ -155,48 +191,9 @@ function nowPlaying() {
|
|||
var li = $('<li>').append('<b>'+history_row.song.title+'</b><br>'+history_row.song.artist);
|
||||
$('#song-history').append(li);
|
||||
});
|
||||
|
||||
setTimeout('nowPlaying()', 10 * 1000);
|
||||
});
|
||||
}
|
||||
|
||||
function iterateTimer() {
|
||||
$('.nowplaying-progress:visible').each(function () {
|
||||
var time_played = $(this).data('time-played') + 1;
|
||||
var time_total = $(this).data('time-total');
|
||||
|
||||
if (time_played > time_total) {
|
||||
time_played = time_total;
|
||||
}
|
||||
|
||||
var time_display = formatTime(time_played) + ' / ' + formatTime(time_total);
|
||||
|
||||
$(this).data('time-played', time_played)
|
||||
.text(time_display)
|
||||
});
|
||||
}
|
||||
|
||||
function formatTime(time) {
|
||||
var sec_num = parseInt(time, 10);
|
||||
|
||||
var hours = Math.floor(sec_num / 3600);
|
||||
var minutes = Math.floor((sec_num - (hours * 3600)) / 60);
|
||||
var seconds = sec_num - (hours * 3600) - (minutes * 60);
|
||||
|
||||
if (hours < 10) {
|
||||
hours = "0" + hours;
|
||||
}
|
||||
if (minutes < 10) {
|
||||
minutes = "0" + minutes;
|
||||
}
|
||||
if (seconds < 10) {
|
||||
seconds = "0" + seconds;
|
||||
}
|
||||
return (hours !== "00" ? hours + ':' : "") + minutes + ':' + seconds;
|
||||
}
|
||||
|
||||
$(function() {
|
||||
nowPlaying();
|
||||
sub.start();
|
||||
|
||||
setInterval(iterateTimer, 1000);
|
||||
});
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
<?php $this->start('custom_js') ?>
|
||||
<script type="text/javascript" src="<?=$url->content('vendors/bower_components/store-js/store.min.js') ?>"></script>
|
||||
<script type="text/javascript" src="<?=$assets->getPath('js/radio.js') ?>"></script>
|
||||
<script type="text/javascript" src="<?=$assets->getPath('js/nchan.js') ?>"></script>
|
||||
<?php $this->stop('custom_js') ?>
|
||||
|
||||
<div class="row">
|
||||
|
@ -288,12 +289,47 @@
|
|||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
function nowPlaying() {
|
||||
$.getJSON('<?=$url->route([
|
||||
'module' => 'api',
|
||||
'controller' => 'nowplaying',
|
||||
'station' => $station->getId()
|
||||
]) ?>', {}, function (row) {
|
||||
function iterateTimer() {
|
||||
$('.nowplaying-progress:visible').each(function () {
|
||||
var time_played = $(this).data('time-played') + 1;
|
||||
var time_total = $(this).data('time-total');
|
||||
|
||||
if (time_played > time_total) {
|
||||
time_played = time_total;
|
||||
}
|
||||
|
||||
var time_display = formatTime(time_played) + ' / ' + formatTime(time_total);
|
||||
|
||||
$(this).data('time-played', time_played)
|
||||
.text(time_display)
|
||||
});
|
||||
}
|
||||
|
||||
function formatTime(time) {
|
||||
var sec_num = parseInt(time, 10);
|
||||
|
||||
var hours = Math.floor(sec_num / 3600);
|
||||
var minutes = Math.floor((sec_num - (hours * 3600)) / 60);
|
||||
var seconds = sec_num - (hours * 3600) - (minutes * 60);
|
||||
|
||||
if (hours < 10) {
|
||||
hours = "0" + hours;
|
||||
}
|
||||
if (minutes < 10) {
|
||||
minutes = "0" + minutes;
|
||||
}
|
||||
if (seconds < 10) {
|
||||
seconds = "0" + seconds;
|
||||
}
|
||||
return (hours !== "00" ? hours + ':' : "") + minutes + ':' + seconds;
|
||||
}
|
||||
|
||||
$(function() {
|
||||
var sub = new NchanSubscriber('/api/live/nowplaying/<?=$station->getId() ?>');
|
||||
|
||||
sub.on("message", function(message, message_metadata) {
|
||||
var row = JSON.parse(message);
|
||||
|
||||
var station_row = $('#station_'+row.station.id);
|
||||
|
||||
if ('mediaSession' in navigator) {
|
||||
|
@ -344,48 +380,9 @@ function nowPlaying() {
|
|||
var li = $('<li>').append('<b>'+history_row.song.title+'</b><br>'+history_row.song.artist);
|
||||
$('#song-history').append(li);
|
||||
});
|
||||
|
||||
setTimeout('nowPlaying()', 10 * 1000);
|
||||
});
|
||||
}
|
||||
|
||||
function iterateTimer() {
|
||||
$('.nowplaying-progress:visible').each(function () {
|
||||
var time_played = $(this).data('time-played') + 1;
|
||||
var time_total = $(this).data('time-total');
|
||||
|
||||
if (time_played > time_total) {
|
||||
time_played = time_total;
|
||||
}
|
||||
|
||||
var time_display = formatTime(time_played) + ' / ' + formatTime(time_total);
|
||||
|
||||
$(this).data('time-played', time_played)
|
||||
.text(time_display)
|
||||
});
|
||||
}
|
||||
|
||||
function formatTime(time) {
|
||||
var sec_num = parseInt(time, 10);
|
||||
|
||||
var hours = Math.floor(sec_num / 3600);
|
||||
var minutes = Math.floor((sec_num - (hours * 3600)) / 60);
|
||||
var seconds = sec_num - (hours * 3600) - (minutes * 60);
|
||||
|
||||
if (hours < 10) {
|
||||
hours = "0" + hours;
|
||||
}
|
||||
if (minutes < 10) {
|
||||
minutes = "0" + minutes;
|
||||
}
|
||||
if (seconds < 10) {
|
||||
seconds = "0" + seconds;
|
||||
}
|
||||
return (hours !== "00" ? hours + ':' : "") + minutes + ':' + seconds;
|
||||
}
|
||||
|
||||
$(function() {
|
||||
nowPlaying();
|
||||
sub.start();
|
||||
|
||||
setInterval(iterateTimer, 1000);
|
||||
});
|
||||
|
|
|
@ -282,7 +282,7 @@ class LiquidSoap extends BackendAbstract
|
|||
protected function _getApiUrlCommand($endpoint, $params = [])
|
||||
{
|
||||
$params = (array)$params;
|
||||
$params['api_auth'] = $this->_getApiPassword();
|
||||
$params['api_auth'] = $this->station->getAdapterApiKey();
|
||||
|
||||
$base_url = (APP_INSIDE_DOCKER) ? 'http://nginx' : 'http://localhost';
|
||||
|
||||
|
@ -325,39 +325,6 @@ class LiquidSoap extends BackendAbstract
|
|||
return $hours . 'h' . $mins . 'm';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a stream-unique API password.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function _getApiPassword()
|
||||
{
|
||||
$be_settings = (array)$this->station->getBackendConfig();
|
||||
|
||||
if (empty($be_settings['api_password'])) {
|
||||
$be_settings['api_password'] = bin2hex(random_bytes(50));
|
||||
|
||||
$em = $this->di['em'];
|
||||
|
||||
$this->station->setBackendConfig($be_settings);
|
||||
$em->persist($this->station);
|
||||
$em->flush();
|
||||
}
|
||||
|
||||
return $be_settings['api_password'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the API password used for internal API authentication.
|
||||
*
|
||||
* @param $password
|
||||
* @return bool
|
||||
*/
|
||||
public function validateApiPassword($password)
|
||||
{
|
||||
return hash_equals($password, $this->_getApiPassword());
|
||||
}
|
||||
|
||||
public function getCommand()
|
||||
{
|
||||
if ($binary = self::getBinary()) {
|
||||
|
|
|
@ -22,6 +22,56 @@ abstract class FrontendAbstract extends \AzuraCast\Radio\AdapterAbstract
|
|||
return $this->supports_listener_detail;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return null|string The command to pass the station-watcher app.
|
||||
*/
|
||||
public function getWatchCommand()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool Whether a station-watcher command exists for this adapter.
|
||||
*/
|
||||
public function hasWatchCommand()
|
||||
{
|
||||
if (APP_TESTING_MODE || !APP_INSIDE_DOCKER) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ($this->getCommand() !== null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the supervisord programmatic name for the station-watcher command.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getWatchProgramName()
|
||||
{
|
||||
return 'station_' . $this->station->getId() . ':station_' . $this->station->getId() . '_watcher';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the AzuraCast station-watcher binary command for the specified adapter and watch URI.
|
||||
*
|
||||
* @param $adapter
|
||||
* @param $watch_uri
|
||||
* @return string
|
||||
*/
|
||||
protected function _getStationWatcherCommand($adapter, $watch_uri)
|
||||
{
|
||||
$base_url = (APP_INSIDE_DOCKER) ? 'http://nginx' : 'http://localhost';
|
||||
$notify_uri = $base_url.'/api/internal/'.$this->station->getId().'/notify?api_auth='.$this->station->getAdapterApiKey();
|
||||
|
||||
return '/var/azuracast/servers/station-watcher/cast-watcher '.$adapter.' 2000 '.$watch_uri.' '.$notify_uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default mounts when resetting or initializing a station.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getDefaultMounts()
|
||||
{
|
||||
return [
|
||||
|
@ -80,7 +130,11 @@ abstract class FrontendAbstract extends \AzuraCast\Radio\AdapterAbstract
|
|||
return Curl::request($c_opts);
|
||||
}
|
||||
|
||||
public function getNowPlaying()
|
||||
/**
|
||||
* @param string|null $payload The payload from the push notification service (if applicable)
|
||||
* @return array
|
||||
*/
|
||||
public function getNowPlaying($payload = null)
|
||||
{
|
||||
// Now Playing defaults.
|
||||
$np = [
|
||||
|
@ -102,7 +156,7 @@ abstract class FrontendAbstract extends \AzuraCast\Radio\AdapterAbstract
|
|||
];
|
||||
|
||||
// Merge station-specific info into defaults.
|
||||
$this->_getNowPlaying($np);
|
||||
$this->_getNowPlaying($np, $payload);
|
||||
|
||||
// Update status code for offline stations, clean up song info for online ones.
|
||||
if ($np['current_song']['text'] == 'Stream Offline') {
|
||||
|
@ -125,8 +179,14 @@ abstract class FrontendAbstract extends \AzuraCast\Radio\AdapterAbstract
|
|||
return $np;
|
||||
}
|
||||
|
||||
/* Stub function for the process internal handler. */
|
||||
abstract protected function _getNowPlaying(&$np);
|
||||
/**
|
||||
* Stub function for the process internal handler.
|
||||
*
|
||||
* @param $np
|
||||
* @param string|null $payload
|
||||
* @return mixed
|
||||
*/
|
||||
abstract protected function _getNowPlaying(&$np, $payload = null);
|
||||
|
||||
protected function _cleanUpString(&$value)
|
||||
{
|
||||
|
|
|
@ -9,26 +9,36 @@ use Entity;
|
|||
|
||||
class IceCast extends FrontendAbstract
|
||||
{
|
||||
public function getWatchCommand()
|
||||
{
|
||||
$fe_config = (array)$this->station->getFrontendConfig();
|
||||
|
||||
return $this->_getStationWatcherCommand(
|
||||
'icecast',
|
||||
'http://admin:'.$fe_config['admin_pw'].'@localhost:' . $fe_config['port'] . '/admin/stats'
|
||||
);
|
||||
}
|
||||
|
||||
/* Process a nowplaying record. */
|
||||
protected function _getNowPlaying(&$np)
|
||||
protected function _getNowPlaying(&$np, $payload = null)
|
||||
{
|
||||
$fe_config = (array)$this->station->getFrontendConfig();
|
||||
$reader = new \App\Xml\Reader();
|
||||
|
||||
$radio_port = $fe_config['port'];
|
||||
$np_url = 'http://'.(APP_INSIDE_DOCKER ? 'stations' : 'localhost').':' . $radio_port . '/admin/stats';
|
||||
if (empty($payload)) {
|
||||
$radio_port = $fe_config['port'];
|
||||
$np_url = 'http://' . (APP_INSIDE_DOCKER ? 'stations' : 'localhost') . ':' . $radio_port . '/admin/stats';
|
||||
|
||||
Debug::log($np_url);
|
||||
$payload = $this->getUrl($np_url, [
|
||||
'basic_auth' => 'admin:' . $fe_config['admin_pw'],
|
||||
]);
|
||||
|
||||
$return_raw = $this->getUrl($np_url, [
|
||||
'basic_auth' => 'admin:'.$fe_config['admin_pw'],
|
||||
]);
|
||||
|
||||
if (!$return_raw) {
|
||||
return false;
|
||||
if (!$payload) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$return = $reader->fromString($return_raw);
|
||||
$return = $reader->fromString($payload);
|
||||
Debug::print_r($return);
|
||||
|
||||
if (!$return || empty($return['source'])) {
|
||||
|
|
|
@ -9,7 +9,7 @@ class Remote extends FrontendAbstract
|
|||
protected $supports_listener_detail = false;
|
||||
|
||||
/* Process a nowplaying record. */
|
||||
protected function _getNowPlaying(&$np)
|
||||
protected function _getNowPlaying(&$np, $payload = null)
|
||||
{
|
||||
$mounts = $this->_getMounts();
|
||||
|
||||
|
|
|
@ -10,20 +10,33 @@ class ShoutCast2 extends FrontendAbstract
|
|||
{
|
||||
protected $supports_mounts = true;
|
||||
|
||||
public function getWatchCommand()
|
||||
{
|
||||
$fe_config = (array)$this->station->getFrontendConfig();
|
||||
|
||||
return $this->_getStationWatcherCommand(
|
||||
'shoutcast',
|
||||
'http://localhost:' . $fe_config['port'] . '/statistics?json=1'
|
||||
);
|
||||
}
|
||||
|
||||
/* Process a nowplaying record. */
|
||||
protected function _getNowPlaying(&$np)
|
||||
protected function _getNowPlaying(&$np, $payload = null)
|
||||
{
|
||||
$fe_config = (array)$this->station->getFrontendConfig();
|
||||
$radio_port = $fe_config['port'];
|
||||
|
||||
$np_url = 'http://'.(APP_INSIDE_DOCKER ? 'stations' : 'localhost').':' . $radio_port . '/statistics?json=1';
|
||||
$return_raw = $this->getUrl($np_url);
|
||||
if (empty($payload)) {
|
||||
$np_url = 'http://'.(APP_INSIDE_DOCKER ? 'stations' : 'localhost').':' . $radio_port . '/statistics?json=1';
|
||||
$payload = $this->getUrl($np_url);
|
||||
|
||||
if (empty($return_raw)) {
|
||||
return false;
|
||||
if (empty($payload)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$current_data = json_decode($return_raw, true);
|
||||
$current_data = json_decode($payload, true);
|
||||
|
||||
Debug::print_r($current_data);
|
||||
|
||||
$streams = count($current_data['streams']);
|
||||
|
|
|
@ -4,13 +4,41 @@ namespace AzuraCast\Sync;
|
|||
use App\Debug;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Entity;
|
||||
use Interop\Container\ContainerInterface;
|
||||
|
||||
class NowPlaying extends SyncAbstract
|
||||
{
|
||||
public function __construct(ContainerInterface $di)
|
||||
{
|
||||
parent::__construct($di);
|
||||
|
||||
$this->em = $di['em'];
|
||||
|
||||
$this->history_repo = $this->em->getRepository(Entity\SongHistory::class);
|
||||
$this->song_repo = $this->em->getRepository(Entity\Song::class);
|
||||
$this->listener_repo = $this->em->getRepository(Entity\Listener::class);
|
||||
}
|
||||
|
||||
/** @var EntityManager */
|
||||
protected $em;
|
||||
|
||||
/** @var Entity\Repository\SongHistoryRepository */
|
||||
protected $history_repo;
|
||||
|
||||
/** @var Entity\Repository\SongRepository */
|
||||
protected $song_repo;
|
||||
|
||||
/** @var Entity\Repository\ListenerRepository */
|
||||
protected $listener_repo;
|
||||
|
||||
|
||||
public function run()
|
||||
{
|
||||
$nowplaying = $this->_loadNowPlaying();
|
||||
|
||||
// Trigger notification to the websocket listeners.
|
||||
$this->_notify('nowplaying_all', $nowplaying);
|
||||
|
||||
// Post statistics to InfluxDB.
|
||||
$influx = $this->di->get('influx');
|
||||
$influx_points = [];
|
||||
|
@ -55,59 +83,54 @@ class NowPlaying extends SyncAbstract
|
|||
$nowplaying[$station]->cache = 'database';
|
||||
}
|
||||
|
||||
$this->di['em']->getRepository(Entity\Settings::class)
|
||||
->setSetting('nowplaying', $nowplaying);
|
||||
/** @var Entity\Repository\SettingsRepository $settings_repo */
|
||||
$settings_repo = $this->em->getRepository(Entity\Settings::class);
|
||||
|
||||
$settings_repo->setSetting('nowplaying', $nowplaying);
|
||||
}
|
||||
|
||||
/** @var Entity\Repository\SongHistoryRepository */
|
||||
protected $history_repo;
|
||||
|
||||
/** @var Entity\Repository\SongRepository */
|
||||
protected $song_repo;
|
||||
|
||||
/** @var Entity\Repository\ListenerRepository */
|
||||
protected $listener_repo;
|
||||
|
||||
/**
|
||||
* @return Entity\Api\NowPlaying[]
|
||||
*/
|
||||
protected function _loadNowPlaying()
|
||||
{
|
||||
/** @var EntityManager $em */
|
||||
$em = $this->di['em'];
|
||||
|
||||
$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();
|
||||
$stations = $this->em->getRepository(Entity\Station::class)->findAll();
|
||||
$nowplaying = [];
|
||||
|
||||
foreach ($stations as $station) {
|
||||
/** @var Entity\Station $station */
|
||||
|
||||
Debug::startTimer($station->getName());
|
||||
$last_run = $station->getNowplayingTimestamp();
|
||||
|
||||
// $name = $station->short_name;
|
||||
$nowplaying[] = $this->_processStation($station);
|
||||
if ($last_run >= (time()-10)) {
|
||||
$np = $station->getNowplaying();
|
||||
$np->update();
|
||||
|
||||
Debug::endTimer($station->getName());
|
||||
$nowplaying[] = $np;
|
||||
} else {
|
||||
Debug::startTimer($station->getName());
|
||||
|
||||
// $name = $station->short_name;
|
||||
$nowplaying[] = $this->processStation($station);
|
||||
|
||||
Debug::endTimer($station->getName());
|
||||
}
|
||||
}
|
||||
|
||||
return $nowplaying;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Structured NowPlaying Data
|
||||
* Generate Structured NowPlaying Data for a given station.
|
||||
*
|
||||
* @param Entity\Station $station
|
||||
* @param string|null $payload The request body from the watcher notification service (if applicable).
|
||||
* @return Entity\Api\NowPlaying
|
||||
*/
|
||||
protected function _processStation(Entity\Station $station)
|
||||
public function processStation(Entity\Station $station, $payload = null)
|
||||
{
|
||||
/** @var EntityManager $em */
|
||||
$em = $this->di['em'];
|
||||
|
||||
/** @var Entity\Api\NowPlaying $np_old */
|
||||
$np_old = $station->getNowplaying();
|
||||
|
||||
|
@ -115,7 +138,7 @@ class NowPlaying extends SyncAbstract
|
|||
$np->station = $station->api($station->getFrontendAdapter($this->di));
|
||||
|
||||
$frontend_adapter = $station->getFrontendAdapter($this->di);
|
||||
$np_raw = $frontend_adapter->getNowPlaying();
|
||||
$np_raw = $frontend_adapter->getNowPlaying($payload);
|
||||
|
||||
$np->listeners = new Entity\Api\NowPlayingListeners($np_raw['listeners']);
|
||||
|
||||
|
@ -163,13 +186,29 @@ class NowPlaying extends SyncAbstract
|
|||
$np->now_playing = $sh_obj->api(true);
|
||||
}
|
||||
|
||||
$np->update();
|
||||
$np->cache = 'station';
|
||||
|
||||
$station->setNowplaying($np);
|
||||
|
||||
$em->persist($station);
|
||||
$em->flush();
|
||||
$this->em->persist($station);
|
||||
$this->em->flush();
|
||||
|
||||
$this->_notify('nowplaying_'.$station->getId(), $np);
|
||||
|
||||
return $np;
|
||||
}
|
||||
|
||||
protected function _notify($channel, $body)
|
||||
{
|
||||
$base_url = (APP_INSIDE_DOCKER) ? 'nginx' : 'localhost';
|
||||
|
||||
$client = new \GuzzleHttp\Client();
|
||||
$res = $client->request('POST', 'http://'.$base_url.':9010/pub/'.urlencode($channel), [
|
||||
'headers' => [
|
||||
'Accept' => 'text/json',
|
||||
],
|
||||
'body' => json_encode($body),
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -30,7 +30,7 @@ if [ "" == "$PKG_OK" ]; then
|
|||
fi
|
||||
|
||||
APP_ENV="${APP_ENV:-production}"
|
||||
UPDATE_REVISION="{$UPDATE_REVISION:-15}"
|
||||
UPDATE_REVISION="{$UPDATE_REVISION:-16}"
|
||||
|
||||
echo "Updating AzuraCast (Environment: $APP_ENV)"
|
||||
|
||||
|
|
|
@ -1,7 +1,17 @@
|
|||
---
|
||||
- name: Install nginx
|
||||
- name: Remove packaged nginx
|
||||
become: true
|
||||
apt: pkg=nginx state=latest
|
||||
apt: package=nginx state=absent
|
||||
|
||||
- name: Install nginx/nchan
|
||||
become: true
|
||||
apt:
|
||||
deb: "{{ item }}"
|
||||
state: present
|
||||
force: yes
|
||||
with_items:
|
||||
- "https://nchan.io/download/nginx-common.ubuntu.deb"
|
||||
- "https://nchan.io/download/nginx-extras.ubuntu.deb"
|
||||
|
||||
- name: Create nginx ssl directory
|
||||
file: path="{{ item }}" state=directory owner=root group=root mode=0744
|
||||
|
|
|
@ -1,4 +1,19 @@
|
|||
client_max_body_size 50M;
|
||||
nchan_redis_url "redis://localhost:6379";
|
||||
|
||||
server {
|
||||
listen 9010;
|
||||
|
||||
location ~ /pub/(\w+)$ {
|
||||
nchan_publisher;
|
||||
nchan_use_redis on;
|
||||
|
||||
nchan_channel_id $1;
|
||||
|
||||
nchan_message_buffer_length 1;
|
||||
nchan_message_timeout "30s";
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
|
@ -55,6 +70,21 @@ server {
|
|||
proxy_pass http://127.0.0.1:$1/$3;
|
||||
}
|
||||
|
||||
# pub/sub endpoints
|
||||
location /api/live/nowplaying {
|
||||
nchan_subscriber;
|
||||
nchan_use_redis on;
|
||||
|
||||
nchan_channel_id nowplaying_all;
|
||||
}
|
||||
|
||||
location ~ /api/live/nowplaying/(\d+)$ {
|
||||
nchan_subscriber;
|
||||
nchan_use_redis on;
|
||||
|
||||
nchan_channel_id "nowplaying_$1";
|
||||
}
|
||||
|
||||
# deny access to .htaccess files, if Apache's document root
|
||||
# concurs with nginx's one
|
||||
#
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
- { role: azuracast-radio, when: update_revision|int < 8 }
|
||||
- { role: supervisord, when: update_revision|int < 13 }
|
||||
- { role: mariadb, when: update_revision|int < 15 }
|
||||
- { role: nginx, when: update_revision|int < 10 }
|
||||
- { role: nginx, when: update_revision|int < 16 }
|
||||
- { role: redis, when: update_revision|int < 14 }
|
||||
- { role: php, when: update_revision|int < 14 }
|
||||
- { role: influxdb, when: update_revision|int < 10 }
|
||||
|
|
|
@ -1,12 +1,154 @@
|
|||
FROM nginx:alpine
|
||||
FROM alpine:3.5
|
||||
|
||||
RUN rm -f /etc/nginx/conf.d/default.conf
|
||||
LABEL maintainer="NGINX Docker Maintainers <docker-maint@nginx.com>"
|
||||
|
||||
ENV NGINX_VERSION 1.13.4
|
||||
ENV NCHAN_VERSION 1.1.3
|
||||
|
||||
RUN GPG_KEYS=B0F4253373F8F6F510D42178520A9993A1C052F8 \
|
||||
&& CONFIG="\
|
||||
--prefix=/etc/nginx \
|
||||
--sbin-path=/usr/sbin/nginx \
|
||||
--modules-path=/usr/lib/nginx/modules \
|
||||
--conf-path=/etc/nginx/nginx.conf \
|
||||
--error-log-path=/var/log/nginx/error.log \
|
||||
--http-log-path=/var/log/nginx/access.log \
|
||||
--pid-path=/var/run/nginx.pid \
|
||||
--lock-path=/var/run/nginx.lock \
|
||||
--http-client-body-temp-path=/var/cache/nginx/client_temp \
|
||||
--http-proxy-temp-path=/var/cache/nginx/proxy_temp \
|
||||
--http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp \
|
||||
--http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp \
|
||||
--http-scgi-temp-path=/var/cache/nginx/scgi_temp \
|
||||
--user=nginx \
|
||||
--group=nginx \
|
||||
--with-http_ssl_module \
|
||||
--with-http_realip_module \
|
||||
--with-http_addition_module \
|
||||
--with-http_sub_module \
|
||||
--with-http_dav_module \
|
||||
--with-http_flv_module \
|
||||
--with-http_mp4_module \
|
||||
--with-http_gunzip_module \
|
||||
--with-http_gzip_static_module \
|
||||
--with-http_random_index_module \
|
||||
--with-http_secure_link_module \
|
||||
--with-http_stub_status_module \
|
||||
--with-http_auth_request_module \
|
||||
--with-http_xslt_module=dynamic \
|
||||
--with-http_image_filter_module=dynamic \
|
||||
--with-http_geoip_module=dynamic \
|
||||
--with-threads \
|
||||
--with-stream \
|
||||
--with-stream_ssl_module \
|
||||
--with-stream_ssl_preread_module \
|
||||
--with-stream_realip_module \
|
||||
--with-stream_geoip_module=dynamic \
|
||||
--with-http_slice_module \
|
||||
--with-mail \
|
||||
--with-mail_ssl_module \
|
||||
--with-compat \
|
||||
--with-file-aio \
|
||||
--with-http_v2_module \
|
||||
--add-module=/usr/src/nchan-${NCHAN_VERSION} \
|
||||
" \
|
||||
&& addgroup -S nginx \
|
||||
&& adduser -D -S -h /var/cache/nginx -s /sbin/nologin -G nginx nginx \
|
||||
&& apk add --no-cache --virtual .build-deps \
|
||||
gcc \
|
||||
libc-dev \
|
||||
make \
|
||||
openssl-dev \
|
||||
pcre-dev \
|
||||
zlib-dev \
|
||||
linux-headers \
|
||||
curl \
|
||||
gnupg \
|
||||
libxslt-dev \
|
||||
gd-dev \
|
||||
geoip-dev \
|
||||
&& curl -fSL http://nginx.org/download/nginx-$NGINX_VERSION.tar.gz -o nginx.tar.gz \
|
||||
&& curl -fSL http://nginx.org/download/nginx-$NGINX_VERSION.tar.gz.asc -o nginx.tar.gz.asc \
|
||||
&& curl -fSL https://github.com/slact/nchan/archive/v${NCHAN_VERSION}.tar.gz -o nchan.tar.gz \
|
||||
&& export GNUPGHOME="$(mktemp -d)" \
|
||||
&& found=''; \
|
||||
for server in \
|
||||
ha.pool.sks-keyservers.net \
|
||||
hkp://keyserver.ubuntu.com:80 \
|
||||
hkp://p80.pool.sks-keyservers.net:80 \
|
||||
pgp.mit.edu \
|
||||
; do \
|
||||
echo "Fetching GPG key $GPG_KEYS from $server"; \
|
||||
gpg --keyserver "$server" --keyserver-options timeout=10 --recv-keys "$GPG_KEYS" && found=yes && break; \
|
||||
done; \
|
||||
test -z "$found" && echo >&2 "error: failed to fetch GPG key $GPG_KEYS" && exit 1; \
|
||||
gpg --batch --verify nginx.tar.gz.asc nginx.tar.gz \
|
||||
&& rm -r "$GNUPGHOME" nginx.tar.gz.asc \
|
||||
&& mkdir -p /usr/src \
|
||||
&& tar -zxC /usr/src -f nginx.tar.gz \
|
||||
&& tar -zxC /usr/src -f nchan.tar.gz \
|
||||
&& rm nginx.tar.gz \
|
||||
&& rm nchan.tar.gz \
|
||||
&& cd /usr/src/nginx-$NGINX_VERSION \
|
||||
&& ./configure $CONFIG --with-debug \
|
||||
&& make -j$(getconf _NPROCESSORS_ONLN) \
|
||||
&& mv objs/nginx objs/nginx-debug \
|
||||
&& mv objs/ngx_http_xslt_filter_module.so objs/ngx_http_xslt_filter_module-debug.so \
|
||||
&& mv objs/ngx_http_image_filter_module.so objs/ngx_http_image_filter_module-debug.so \
|
||||
&& mv objs/ngx_http_geoip_module.so objs/ngx_http_geoip_module-debug.so \
|
||||
&& mv objs/ngx_stream_geoip_module.so objs/ngx_stream_geoip_module-debug.so \
|
||||
&& ./configure $CONFIG \
|
||||
&& make -j$(getconf _NPROCESSORS_ONLN) \
|
||||
&& make install \
|
||||
&& rm -rf /etc/nginx/html/ \
|
||||
&& mkdir /etc/nginx/conf.d/ \
|
||||
&& mkdir -p /usr/share/nginx/html/ \
|
||||
&& install -m644 html/index.html /usr/share/nginx/html/ \
|
||||
&& install -m644 html/50x.html /usr/share/nginx/html/ \
|
||||
&& install -m755 objs/nginx-debug /usr/sbin/nginx-debug \
|
||||
&& install -m755 objs/ngx_http_xslt_filter_module-debug.so /usr/lib/nginx/modules/ngx_http_xslt_filter_module-debug.so \
|
||||
&& install -m755 objs/ngx_http_image_filter_module-debug.so /usr/lib/nginx/modules/ngx_http_image_filter_module-debug.so \
|
||||
&& install -m755 objs/ngx_http_geoip_module-debug.so /usr/lib/nginx/modules/ngx_http_geoip_module-debug.so \
|
||||
&& install -m755 objs/ngx_stream_geoip_module-debug.so /usr/lib/nginx/modules/ngx_stream_geoip_module-debug.so \
|
||||
&& ln -s ../../usr/lib/nginx/modules /etc/nginx/modules \
|
||||
&& strip /usr/sbin/nginx* \
|
||||
&& strip /usr/lib/nginx/modules/*.so \
|
||||
&& rm -rf /usr/src/nginx-$NGINX_VERSION \
|
||||
\
|
||||
# Bring in gettext so we can get `envsubst`, then throw
|
||||
# the rest away. To do this, we need to install `gettext`
|
||||
# then move `envsubst` out of the way so `gettext` can
|
||||
# be deleted completely, then move `envsubst` back.
|
||||
&& apk add --no-cache --virtual .gettext gettext \
|
||||
&& mv /usr/bin/envsubst /tmp/ \
|
||||
\
|
||||
&& runDeps="$( \
|
||||
scanelf --needed --nobanner /usr/sbin/nginx /usr/lib/nginx/modules/*.so /tmp/envsubst \
|
||||
| awk '{ gsub(/,/, "\nso:", $2); print "so:" $2 }' \
|
||||
| sort -u \
|
||||
| xargs -r apk info --installed \
|
||||
| sort -u \
|
||||
)" \
|
||||
&& apk add --no-cache --virtual .nginx-rundeps $runDeps \
|
||||
&& apk del .build-deps \
|
||||
&& apk del .gettext \
|
||||
&& mv /tmp/envsubst /usr/local/bin/ \
|
||||
\
|
||||
# forward request and error logs to docker log collector
|
||||
&& ln -sf /dev/stdout /var/log/nginx/access.log \
|
||||
&& ln -sf /dev/stderr /var/log/nginx/error.log
|
||||
|
||||
EXPOSE 80 443
|
||||
|
||||
STOPSIGNAL SIGTERM
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
|
||||
COPY ./nginx.conf /etc/nginx/nginx.conf
|
||||
COPY ./nginx_azuracast.conf /etc/nginx/conf.d/azuracast.conf
|
||||
|
||||
RUN apk update && \
|
||||
apk add openssl
|
||||
|
||||
RUN mkdir -p /etc/nginx/ssl/
|
||||
RUN openssl req -new -nodes -x509 -subj "/C=US/ST=Texas/L=Austin/O=IT/CN=localhost" -days 3650 -keyout /etc/nginx/ssl/server.key -out /etc/nginx/ssl/server.crt -extensions v3_ca
|
||||
|
||||
RUN sed -i "s/sendfile on/sendfile off/" /etc/nginx/nginx.conf
|
||||
RUN openssl req -new -nodes -x509 -subj "/C=US/ST=Texas/L=Austin/O=IT/CN=localhost" -days 3650 -keyout /etc/nginx/ssl/server.key -out /etc/nginx/ssl/server.crt -extensions v3_ca
|
31
util/docker/nginx/nginx.conf
Normal file
31
util/docker/nginx/nginx.conf
Normal file
|
@ -0,0 +1,31 @@
|
|||
user nginx;
|
||||
worker_processes 1;
|
||||
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log /var/log/nginx/access.log main;
|
||||
|
||||
sendfile off;
|
||||
#tcp_nopush on;
|
||||
|
||||
keepalive_timeout 65;
|
||||
|
||||
#gzip on;
|
||||
|
||||
client_max_body_size 50M;
|
||||
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
}
|
|
@ -1,4 +1,18 @@
|
|||
client_max_body_size 50M;
|
||||
nchan_redis_url "redis://redis:6379";
|
||||
|
||||
server {
|
||||
listen 9010;
|
||||
|
||||
location ~ /pub/(\w+)$ {
|
||||
nchan_publisher;
|
||||
nchan_use_redis on;
|
||||
|
||||
nchan_channel_id $1;
|
||||
|
||||
nchan_message_buffer_length 1;
|
||||
nchan_message_timeout "30s";
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
|
@ -54,6 +68,21 @@ server {
|
|||
proxy_pass http://stations:$1/$3;
|
||||
}
|
||||
|
||||
# pub/sub endpoints
|
||||
location /api/live/nowplaying {
|
||||
nchan_subscriber;
|
||||
nchan_use_redis on;
|
||||
|
||||
nchan_channel_id nowplaying_all;
|
||||
}
|
||||
|
||||
location ~ /api/live/nowplaying/(\d+)$ {
|
||||
nchan_subscriber;
|
||||
nchan_use_redis on;
|
||||
|
||||
nchan_channel_id "nowplaying_$1";
|
||||
}
|
||||
|
||||
# deny access to .htaccess files, if Apache's document root
|
||||
# concurs with nginx's one
|
||||
#
|
||||
|
|
|
@ -1,7 +1,15 @@
|
|||
FROM ubuntu:xenial
|
||||
|
||||
RUN mkdir -p /var/azuracast/servers/shoutcast2 && \
|
||||
mkdir -p /var/azuracast/servers/icecast2 && \
|
||||
mkdir -p /var/azuracast/servers/station-watcher && \
|
||||
mkdir -p /var/azuracast/stations
|
||||
|
||||
RUN adduser --home /var/azuracast --disabled-password --gecos "" azuracast && \
|
||||
chown -R azuracast:azuracast /var/azuracast
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -q -y curl
|
||||
apt-get install -q -y wget git gcc pkg-config curl libssl-dev libcurl4-openssl-dev
|
||||
|
||||
# Install supervisord
|
||||
RUN mkdir -p /var/log/supervisor
|
||||
|
@ -11,28 +19,28 @@ RUN apt-get update && \
|
|||
|
||||
ADD ./supervisord.conf /etc/supervisor/supervisord.conf
|
||||
|
||||
# Set up broadcast sections
|
||||
RUN mkdir -p /var/azuracast/servers/shoutcast2 && \
|
||||
mkdir -p /var/azuracast/servers/icecast2 && \
|
||||
mkdir -p /var/azuracast/stations
|
||||
# Install Rust and the station-watcher app
|
||||
WORKDIR /var/azuracast/servers/station-watcher
|
||||
|
||||
RUN adduser --home /var/azuracast --disabled-password --gecos "" azuracast && \
|
||||
chown -R azuracast:azuracast /var/azuracast
|
||||
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y && \
|
||||
git clone https://github.com/AzuraCast/station-watcher ./src && \
|
||||
cd src && \
|
||||
~/.cargo/bin/cargo build --release && \
|
||||
cp target/release/cast-watcher /var/azuracast/servers/station-watcher/ && \
|
||||
rm -rf /var/azuracast/servers/station-watcher/src && \
|
||||
rm -rf ~/.cargo
|
||||
|
||||
# Download Shoutcast 2
|
||||
WORKDIR /var/azuracast/servers/shoutcast2
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y wget && \
|
||||
wget http://download.nullsoft.com/shoutcast/tools/sc_serv2_linux_x64-latest.tar.gz && \
|
||||
RUN wget http://download.nullsoft.com/shoutcast/tools/sc_serv2_linux_x64-latest.tar.gz && \
|
||||
tar -xzf sc_serv2_linux_x64-latest.tar.gz
|
||||
|
||||
# Download and build IceCast-KH
|
||||
WORKDIR /var/azuracast/servers/icecast2
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends libxml2 libxslt1-dev libvorbis-dev libssl-dev libcurl4-openssl-dev \
|
||||
gcc pkg-config && \
|
||||
apt-get install -q -y --no-install-recommends libxml2 libxslt1-dev libvorbis-dev && \
|
||||
wget https://github.com/karlheyes/icecast-kh/archive/icecast-2.4.0-kh5.tar.gz && \
|
||||
tar --strip-components=1 -xzf icecast-2.4.0-kh5.tar.gz && \
|
||||
./configure && \
|
||||
|
@ -49,7 +57,7 @@ ADD ./xml2json.xslt /usr/local/share/icecast/web/xml2json.xslt
|
|||
RUN sed -e 's#main#main contrib non-free#' -i /etc/apt/sources.list
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends ocaml git rsync opam libpcre3-dev libfdk-aac-dev libmad0-dev \
|
||||
apt-get install -y --no-install-recommends ocaml rsync opam libpcre3-dev libfdk-aac-dev libmad0-dev \
|
||||
libmp3lame-dev libtag1-dev libfaad-dev libflac-dev m4 aspcud
|
||||
|
||||
USER azuracast
|
||||
|
|
|
@ -1 +1 @@
|
|||
{"js/radio.js":"js/radio.js?2c18564955716845","js/app.min.js":"js/app.min.js?f3e29a2e4ed3bf96","js/app.js":"js/app.js?2422c19d99917bc5","css/light.css":"css/light.css?5eb77e59bac195af","css/dark.css":"css/dark.css?d76a38e41049bf09"}
|
||||
{"js/radio.js":"js/radio.js?2c18564955716845","js/nchan.js":"js/nchan.js?c512cda73ce7b123","js/app.min.js":"js/app.min.js?f3e29a2e4ed3bf96","js/app.js":"js/app.js?2422c19d99917bc5","css/light.css":"css/light.css?5eb77e59bac195af","css/dark.css":"css/dark.css?d76a38e41049bf09"}
|
859
web/static/js/nchan.js
Normal file
859
web/static/js/nchan.js
Normal file
|
@ -0,0 +1,859 @@
|
|||
/*
|
||||
* NchanSubscriber
|
||||
* usage: var sub = new NchanSubscriber(url, opt);
|
||||
*
|
||||
* opt = {
|
||||
* subscriber: 'longpoll', 'eventsource', or 'websocket',
|
||||
* //or an array of the above indicating subscriber type preference
|
||||
* reconnect: undefined or 'session' or 'persist'
|
||||
* //if the HTML5 sessionStore or localStore should be used to resume
|
||||
* //connections interrupted by a page load
|
||||
* shared: true or undefined
|
||||
* //share connection to same subscriber url between browser
|
||||
* //windows and tabs using localStorage. In shared mode,
|
||||
* //only 1 running subscriber is allowed per url per window/tab.
|
||||
* }
|
||||
*
|
||||
* sub.on("message", function(message, message_metadata) {
|
||||
* // message is a string
|
||||
* // message_metadata may contain 'id' and 'content-type'
|
||||
* });
|
||||
*
|
||||
* sub.on('connect', function(evt) {
|
||||
* //fired when first connected.
|
||||
* });
|
||||
*
|
||||
* sub.on('disconnect', function(evt) {
|
||||
* // when disconnected.
|
||||
* });
|
||||
*
|
||||
* sub.on('error', function(code, message) {
|
||||
* //error callback. not sure about the parameters yet
|
||||
* });
|
||||
*
|
||||
* sub.reconnect; // should subscriber try to reconnect? true by default.
|
||||
* sub.reconnectTimeout; //how long to wait to reconnect? does not apply to EventSource, which reconnects on its own.
|
||||
* sub.lastMessageId; //last message id. useful for resuming a connection without loss or repetition.
|
||||
*
|
||||
* sub.start(); // begin (or resume) subscribing
|
||||
* sub.stop(); // stop subscriber. do not reconnect.
|
||||
*/
|
||||
|
||||
//Thanks Darren Whitlen ( @prawnsalad ) for your feedback
|
||||
|
||||
|
||||
;(function (global, moduleName, factory) { // eslint-disable-line
|
||||
"use strict";
|
||||
/* eslint-disable no-undef */
|
||||
var newModule = factory(global);
|
||||
if (typeof module === "object" && module != null && module.exports) {
|
||||
module.exports = newModule;
|
||||
} else if (typeof define === "function" && define.amd) {
|
||||
define(function () { return newModule; });
|
||||
} else {
|
||||
global[moduleName] = newModule;
|
||||
}
|
||||
/* eslint-enable no-undef */
|
||||
})(typeof window !== "undefined" ? window : this, "NchanSubscriber", function factory(global, undefined) { // eslint-disable-line
|
||||
|
||||
// https://github.com/yanatan16/nanoajax
|
||||
var nanoajax={};
|
||||
(function(){var e=["responseType","withCredentials","timeout","onprogress"];nanoajax.ajax=function(r,o){var a=r.headers||{},u=r.body,s=r.method||(u?"POST":"GET"),i=false;var f=t(r.cors);function l(e,t){return function(){if(!i){o(f.status===undefined?e:f.status,f.status===0?"Error":f.response||f.responseText||t,f);i=true}}}f.open(s,r.url,true);var d=f.onload=l(200);f.onreadystatechange=function(){if(f.readyState===4)d()};f.onerror=l(null,"Error");f.ontimeout=l(null,"Timeout");f.onabort=l(null,"Abort");if(u){n(a,"X-Requested-With","XMLHttpRequest");if(!global.FormData||!(u instanceof global.FormData)){n(a,"Content-Type","application/x-www-form-urlencoded")}}for(var p=0,c=e.length,g;p<c;p++){g=e[p];if(r[g]!==undefined)f[g]=r[g]}for(var g in a)f.setRequestHeader(g,a[g]);f.send(u);return f};function t(e){if(e&&global.XDomainRequest&&!/MSIE 1/.test(navigator.userAgent))return new XDomainRequest;if(global.XMLHttpRequest)return new XMLHttpRequest}function n(e,t,n){e[t]=e[t]||n}})(); // eslint-disable-line
|
||||
|
||||
|
||||
// https://github.com/component/emitter
|
||||
function Emitter(t){return t?mixin(t):void 0}function mixin(t){for(var e in Emitter.prototype)t[e]=Emitter.prototype[e];return t}Emitter.prototype.on=Emitter.prototype.addEventListener=function(t,e){return this._callbacks=this._callbacks||{},(this._callbacks["$"+t]=this._callbacks["$"+t]||[]).push(e),this},Emitter.prototype.once=function(t,e){function i(){this.off(t,i),e.apply(this,arguments)}return i.fn=e,this.on(t,i),this},Emitter.prototype.off=Emitter.prototype.removeListener=Emitter.prototype.removeAllListeners=Emitter.prototype.removeEventListener=function(t,e){if(this._callbacks=this._callbacks||{},0==arguments.length)return this._callbacks={},this;var i=this._callbacks["$"+t];if(!i)return this;if(1==arguments.length)return delete this._callbacks["$"+t],this;for(var r,s=0;s<i.length;s++)if(r=i[s],r===e||r.fn===e){i.splice(s,1);break}return this},Emitter.prototype.emit=function(t){this._callbacks=this._callbacks||{};var e=[].slice.call(arguments,1),i=this._callbacks["$"+t];if(i){i=i.slice(0);for(var r=0,s=i.length;s>r;++r)i[r].apply(this,e)}return this},Emitter.prototype.listeners=function(t){return this._callbacks=this._callbacks||{},this._callbacks["$"+t]||[]},Emitter.prototype.hasListeners=function(t){return!!this.listeners(t).length};// eslint-disable-line
|
||||
|
||||
var ughbind = (Function.prototype.bind
|
||||
? function ughbind(fn, thisObj) {
|
||||
return fn.bind(thisObj);
|
||||
}
|
||||
: function ughbind(fn, thisObj) {
|
||||
return function() {
|
||||
fn.apply(thisObj, arguments);
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
var sharedSubscriberTable={};
|
||||
|
||||
"use strict";
|
||||
function NchanSubscriber(url, opt) {
|
||||
if(this === window) {
|
||||
throw "use 'new NchanSubscriber(...)' to initialize";
|
||||
}
|
||||
|
||||
this.url = url;
|
||||
opt = opt || {};
|
||||
|
||||
//which transport should i use?
|
||||
if(typeof opt === "string") {
|
||||
opt = {subscriber: opt};
|
||||
}
|
||||
if(opt.transport && !opt.subscriber) {
|
||||
opt.subscriber = opt.transport;
|
||||
}
|
||||
if(typeof opt.subscriber === "string") {
|
||||
opt.subscriber = [ opt.subscriber ];
|
||||
}
|
||||
this.desiredTransport = opt.subscriber;
|
||||
|
||||
if(opt.shared) {
|
||||
if (!("localStorage" in global)) {
|
||||
throw "localStorage unavailable for use in shared NchanSubscriber";
|
||||
}
|
||||
|
||||
var pre = "NchanSubscriber:" + this.url + ":shared:";
|
||||
var sharedKey = function(key) { return pre + key; };
|
||||
var localStorage = global.localStorage;
|
||||
this.shared = {
|
||||
id: "" + Math.random() + Math.random(),
|
||||
key: sharedKey,
|
||||
get: function(key) {
|
||||
return localStorage.getItem(sharedKey(key));
|
||||
},
|
||||
set: function(key, val) {
|
||||
return localStorage.setItem(sharedKey(key), val);
|
||||
},
|
||||
setWithId: ughbind(function(key, val) {
|
||||
return this.shared.set(key, "##" + this.shared.id + ":" + val);
|
||||
}, this),
|
||||
getWithId: ughbind(function(key) {
|
||||
return this.shared.stripIdFromVal(this.shared.get(key));
|
||||
}, this),
|
||||
stripIdFromVal: function(val) {
|
||||
if(!val) {
|
||||
return val;
|
||||
}
|
||||
var sep = val.indexOf(":");
|
||||
if(val[0]!=val[1] || val[0]!="#" || !sep) {
|
||||
//throw "not an event value with id";
|
||||
return val; //for backwards-compatibility
|
||||
}
|
||||
return val.substring(sep+1, val.length);
|
||||
},
|
||||
remove: function(key) {
|
||||
return localStorage.removeItem(sharedKey(key));
|
||||
},
|
||||
matchEventKey: ughbind(function(ev, key) {
|
||||
if(ev.storageArea && ev.storageArea != localStorage){
|
||||
return false;
|
||||
}
|
||||
return ev.key == sharedKey(key);
|
||||
}, this),
|
||||
matchEventKeyWithId: ughbind(function(ev, key) {
|
||||
if(this.shared.matchEventKey(ev, key)) {
|
||||
var val = ev.newValue;
|
||||
var sep = val.indexOf(":");
|
||||
if(val[0]!=val[1] || val[0]!="#" || !sep) {
|
||||
//throw "not an event value with id";
|
||||
return true; //for backwards-compatibility
|
||||
}
|
||||
var id = val.substring(2, sep);
|
||||
return (id != this.shared.id); //ignore own events (accomodations for IE. Fuckin' IE, even after all these years...)
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}, this),
|
||||
setRole: ughbind(function(role) {
|
||||
//console.log(this.url, "set shared role to ", role);
|
||||
if(role == "master" && this.shared.role != "master") {
|
||||
var now = new Date().getTime()/1000;
|
||||
this.shared.setWithId("master:created", now);
|
||||
this.shared.setWithId("master:lastSeen", now);
|
||||
}
|
||||
if(role == "slave" && !this.lastMessageId) {
|
||||
this.lastMessageId = this.shared.get("msg:id");
|
||||
}
|
||||
this.shared.role = role;
|
||||
return this;
|
||||
}, this),
|
||||
|
||||
demoteToSlave: ughbind(function() {
|
||||
//console.log("demote to slave");
|
||||
if(this.shared.role != "master") {
|
||||
throw "can't demote non-master to slave";
|
||||
}
|
||||
if(this.running) {
|
||||
this.stop();
|
||||
this.shared.setRole("slave");
|
||||
this.initializeTransport();
|
||||
this.start();
|
||||
}
|
||||
else {
|
||||
this.initializeTransport();
|
||||
}
|
||||
}, this),
|
||||
|
||||
maybePromoteToMaster: ughbind(function() {
|
||||
if(!(this.running || this.starting)) {
|
||||
//console.log(this.url, "stopped Subscriber won't be promoted to master");
|
||||
return this;
|
||||
}
|
||||
if(this.shared.maybePromotingToMaster) {
|
||||
//console.log(this.url, " already maybePromotingToMaster");
|
||||
return;
|
||||
}
|
||||
this.shared.maybePromotingToMaster = true;
|
||||
|
||||
//console.log(this.url, "maybe promote to master");
|
||||
var processRoll;
|
||||
|
||||
var lotteryRoundDuration = 2000;
|
||||
var currentContenders = 0;
|
||||
|
||||
//roll the dice
|
||||
var roll = Math.random();
|
||||
var bestRoll = roll;
|
||||
|
||||
var checkRollInterval;
|
||||
var checkRoll = ughbind(function(dontProcess) {
|
||||
var latestSharedRollTime = parseFloat(this.shared.getWithId("lotteryTime"));
|
||||
var latestSharedRoll = parseFloat(this.shared.getWithId("lottery"));
|
||||
var notStale = !latestSharedRollTime || (latestSharedRollTime > (new Date().getTime() - lotteryRoundDuration * 2));
|
||||
if(notStale && latestSharedRoll && (!bestRoll || latestSharedRoll > bestRoll)) {
|
||||
bestRoll = latestSharedRoll;
|
||||
}
|
||||
if(!dontProcess) {
|
||||
processRoll();
|
||||
}
|
||||
}, this);
|
||||
|
||||
checkRoll(true);
|
||||
this.shared.setWithId("lottery", roll);
|
||||
this.shared.setWithId("lotteryTime", new Date().getTime() / 1000);
|
||||
|
||||
var rollCallback = ughbind(function(ev) {
|
||||
if(this.shared.matchEventKeyWithId(ev, "lottery") && ev.newValue) {
|
||||
currentContenders += 1;
|
||||
var newVal = parseFloat(this.shared.stripIdFromVal(ev.newValue));
|
||||
var oldVal = parseFloat(this.shared.stripIdFromVal(ev.oldValue));
|
||||
if(oldVal > newVal) {
|
||||
this.shared.setWithId("lottery", oldVal);
|
||||
}
|
||||
|
||||
if(!bestRoll || newVal >= bestRoll) {
|
||||
//console.log("new bestRoll", newVal);
|
||||
bestRoll = newVal;
|
||||
}
|
||||
}
|
||||
}, this);
|
||||
global.addEventListener("storage", rollCallback);
|
||||
|
||||
var finish = ughbind(function() {
|
||||
//console.log("finish");
|
||||
this.shared.maybePromotingToMaster = false;
|
||||
//console.log(this.url, this.shared.role);
|
||||
global.removeEventListener("storage", rollCallback);
|
||||
if(checkRollInterval) {
|
||||
clearInterval(checkRollInterval);
|
||||
}
|
||||
if(this.shared && this.shared.role == "master") {
|
||||
this.shared.remove("lottery");
|
||||
this.shared.remove("lotteryTime");
|
||||
}
|
||||
if(this.running) {
|
||||
this.stop();
|
||||
this.initializeTransport();
|
||||
this.start();
|
||||
}
|
||||
else {
|
||||
this.initializeTransport();
|
||||
if(this.starting) {
|
||||
this.start();
|
||||
}
|
||||
}
|
||||
}, this);
|
||||
|
||||
processRoll = ughbind(function() {
|
||||
//console.log("roll, bestroll", roll, bestRoll);
|
||||
if(roll < bestRoll) {
|
||||
//console.log(this.url, "loser");
|
||||
this.shared.setRole("slave");
|
||||
finish();
|
||||
}
|
||||
else if(roll >= bestRoll) {
|
||||
//var now = new Date().getTime() / 1000;
|
||||
//var lotteryTime = parseFloat(this.shared.getWithId("lotteryTime"));
|
||||
//console.log(lotteryTime, now - lotteryRoundDuration/1000);
|
||||
if(currentContenders == 0) {
|
||||
//console.log("winner, no more contenders!");
|
||||
this.shared.setRole("master");
|
||||
finish();
|
||||
}
|
||||
else {
|
||||
//console.log("winning, but have contenders", currentContenders);
|
||||
currentContenders = 0;
|
||||
}
|
||||
}
|
||||
}, this);
|
||||
|
||||
checkRollInterval = global.setInterval(checkRoll, lotteryRoundDuration);
|
||||
}, this),
|
||||
masterCheckInterval: 10000
|
||||
};
|
||||
}
|
||||
|
||||
this.lastMessageId = opt.id || opt.msgId;
|
||||
this.reconnect = typeof opt.reconnect == "undefined" ? true : opt.reconnect;
|
||||
this.reconnectTimeout = opt.reconnectTimeout || 1000;
|
||||
|
||||
|
||||
var saveConnectionState;
|
||||
if(!opt.reconnect) {
|
||||
saveConnectionState = function() {};
|
||||
}
|
||||
else {
|
||||
var index = "NchanSubscriber:" + url + ":lastMessageId";
|
||||
var storage;
|
||||
if(opt.reconnect == "persist") {
|
||||
storage = ("localStorage" in global) && global.localStorage;
|
||||
if(!storage)
|
||||
throw "can't use reconnect: 'persist' option: localStorage not available";
|
||||
}
|
||||
else if(opt.reconnect == "session") {
|
||||
storage = ("sessionStorage" in global) && global.sessionStorage;
|
||||
if(!storage)
|
||||
throw "can't use reconnect: 'session' option: sessionStorage not available";
|
||||
}
|
||||
else {
|
||||
throw "invalid 'reconnect' option value " + opt.reconnect;
|
||||
}
|
||||
saveConnectionState = ughbind(function(msgid) {
|
||||
if(this.shared && this.shared.role == "slave") return;
|
||||
storage.setItem(index, msgid);
|
||||
}, this);
|
||||
this.lastMessageId = storage.getItem(index);
|
||||
}
|
||||
|
||||
var onUnloadEvent = ughbind(function() {
|
||||
if(this.running) {
|
||||
this.stop();
|
||||
}
|
||||
if(this.shared && this.shared.role == "master") {
|
||||
this.shared.setWithId('status', "disconnected");
|
||||
}
|
||||
}, this);
|
||||
global.addEventListener('beforeunload', onUnloadEvent, false);
|
||||
// swap `beforeunload` to `unload` after DOM is loaded
|
||||
global.addEventListener('DOMContentLoaded', function() {
|
||||
global.removeEventListener('beforeunload', onUnloadEvent, false);
|
||||
global.addEventListener('unload', onUnloadEvent, false);
|
||||
}, false);
|
||||
|
||||
|
||||
var notifySharedSubscribers;
|
||||
if(this.shared) {
|
||||
notifySharedSubscribers = ughbind(function(name, data) {
|
||||
if(this.shared.role != "master") {
|
||||
return;
|
||||
}
|
||||
|
||||
if(name == "message") {
|
||||
this.shared.set("msg:id", data[1] && data[1].id || "");
|
||||
this.shared.set("msg:content-type", data[1] && data[1]["content-type"] || "");
|
||||
this.shared.set("msg", data[0]);
|
||||
}
|
||||
else if(name == "error") {
|
||||
//TODO
|
||||
}
|
||||
else if(name == "connecting") {
|
||||
this.shared.setWithId("status", "connecting");
|
||||
}
|
||||
else if(name == "connect") {
|
||||
this.shared.setWithId("status", "connected");
|
||||
}
|
||||
else if(name == "reconnect") {
|
||||
this.shared.setWithId("status", "reconnecting");
|
||||
}
|
||||
else if(name == "disconnect") {
|
||||
this.shared.setWithId("status", "disconnected");
|
||||
}
|
||||
}, this);
|
||||
}
|
||||
else {
|
||||
notifySharedSubscribers = function(){};
|
||||
}
|
||||
|
||||
var restartTimeoutIndex;
|
||||
var stopHandler = ughbind(function() {
|
||||
if(!restartTimeoutIndex && this.running && this.reconnect && !this.transport.reconnecting && !this.transport.doNotReconnect) {
|
||||
//console.log("stopHAndler reconnect plz", this.running, this.reconnect);
|
||||
notifySharedSubscribers("reconnect");
|
||||
restartTimeoutIndex = global.setTimeout(ughbind(function() {
|
||||
restartTimeoutIndex = null;
|
||||
this.stop();
|
||||
this.start();
|
||||
}, this), this.reconnectTimeout);
|
||||
}
|
||||
else {
|
||||
notifySharedSubscribers("disconnect");
|
||||
}
|
||||
}, this);
|
||||
|
||||
this.on("message", function msg(msg, meta) {
|
||||
this.lastMessageId=meta.id;
|
||||
if(meta.id) {
|
||||
saveConnectionState(meta.id);
|
||||
}
|
||||
notifySharedSubscribers("message", [msg, meta]);
|
||||
//console.log(msg, meta);
|
||||
});
|
||||
this.on("error", function fail(code, text) {
|
||||
stopHandler(code, text);
|
||||
notifySharedSubscribers("error", [code, text]);
|
||||
//console.log("failure", code, text);
|
||||
});
|
||||
this.on("connect", function() {
|
||||
this.connected = true;
|
||||
notifySharedSubscribers("connect");
|
||||
});
|
||||
this.on("__disconnect", function fail(code, text) {
|
||||
this.connected = false;
|
||||
this.emit("disconnect", code, text);
|
||||
stopHandler(code, text);
|
||||
//console.log("__disconnect", code, text);
|
||||
});
|
||||
}
|
||||
|
||||
Emitter(NchanSubscriber.prototype);
|
||||
|
||||
NchanSubscriber.prototype.initializeTransport = function(possibleTransports) {
|
||||
if(possibleTransports) {
|
||||
this.desiredTransport = possibleTransports;
|
||||
}
|
||||
if(this.shared && this.shared.role == "slave") {
|
||||
this.transport = new this.SubscriberClass["__slave"](ughbind(this.emit, this)); //try it
|
||||
}
|
||||
else {
|
||||
var tryInitializeTransport = ughbind(function(name) {
|
||||
if(!this.SubscriberClass[name]) {
|
||||
throw "unknown subscriber type " + name;
|
||||
}
|
||||
try {
|
||||
this.transport = new this.SubscriberClass[name](ughbind(this.emit, this)); //try it
|
||||
return this.transport;
|
||||
} catch(err) { /*meh...*/ }
|
||||
}, this);
|
||||
|
||||
var i;
|
||||
if(this.desiredTransport) {
|
||||
for(i=0; i<this.desiredTransport.length; i++) {
|
||||
if(tryInitializeTransport(this.desiredTransport[i])) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
for(i in this.SubscriberClass) {
|
||||
if (this.SubscriberClass.hasOwnProperty(i) && i[0] != "_" && tryInitializeTransport(i)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if(! this.transport) {
|
||||
throw "can't use any transport type";
|
||||
}
|
||||
};
|
||||
|
||||
var storageEventListener;
|
||||
|
||||
NchanSubscriber.prototype.start = function() {
|
||||
if(this.running)
|
||||
throw "Can't start NchanSubscriber, it's already started.";
|
||||
|
||||
this.starting = true;
|
||||
|
||||
if(this.shared) {
|
||||
if(sharedSubscriberTable[this.url] && sharedSubscriberTable[this.url] != this) {
|
||||
throw "Only 1 shared subscriber allowed per url per window/tab.";
|
||||
}
|
||||
sharedSubscriberTable[this.url] = this;
|
||||
|
||||
if(!this.shared.role) {
|
||||
var status = this.shared.getWithId("status");
|
||||
storageEventListener = ughbind(function(ev) {
|
||||
if(this.shared.matchEventKeyWithId(ev, "status")) {
|
||||
var newValue = this.shared.stripIdFromVal(ev.newValue);
|
||||
if(newValue == "disconnected") {
|
||||
if(this.shared.role == "slave") {
|
||||
//play the promotion lottery
|
||||
//console.log(this.url, "status changed to disconnected, maybepromotetomaster", ev.newValue, ev.oldValue);
|
||||
this.shared.maybePromoteToMaster();
|
||||
}
|
||||
else if(this.shared.role == "master") {
|
||||
//do nothing
|
||||
//console.log(this.url, "current role is master, do nothing?...");
|
||||
}
|
||||
}
|
||||
}
|
||||
else if(this.shared.role == "master" && this.shared.matchEventKeyWithId(ev, "master:created") && ev.newValue) {
|
||||
//a new master has arrived. demote to slave.
|
||||
this.shared.demoteToSlave();
|
||||
}
|
||||
}, this);
|
||||
global.addEventListener("storage", storageEventListener);
|
||||
if(status == "disconnected") {
|
||||
//console.log(this.url, "status == disconnected, maybepromotetomaster");
|
||||
this.shared.maybePromoteToMaster();
|
||||
}
|
||||
else {
|
||||
this.shared.setRole(status ? "slave" : "master");
|
||||
this.initializeTransport();
|
||||
}
|
||||
}
|
||||
|
||||
if(this.shared.role == "master") {
|
||||
this.shared.setWithId("status", "connecting");
|
||||
this.transport.listen(this.url, this.lastMessageId);
|
||||
this.running = true;
|
||||
delete this.starting;
|
||||
|
||||
//master checkin interval
|
||||
this.shared.masterIntervalCheckID = global.setInterval(ughbind(function() {
|
||||
this.shared.setWithId("master:lastSeen", new Date().getTime() / 1000);
|
||||
}, this), this.shared.masterCheckInterval * 0.8);
|
||||
}
|
||||
else if(this.shared.role == "slave") {
|
||||
this.transport.listen(this.url, this.shared);
|
||||
this.running = true;
|
||||
delete this.starting;
|
||||
|
||||
//slave check if master is around
|
||||
this.shared.masterIntervalCheckID = global.setInterval(ughbind(function() {
|
||||
var lastCheckin = parseFloat(this.shared.getWithId("master:lastSeen"));
|
||||
if(!lastCheckin || lastCheckin < (new Date().getTime() / 1000) - this.shared.masterCheckInterval / 1000) {
|
||||
//master hasn't checked in for too long. assume it's gone.
|
||||
this.shared.maybePromoteToMaster();
|
||||
}
|
||||
}, this), this.shared.masterCheckInterval);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if(!this.transport) {
|
||||
this.initializeTransport();
|
||||
}
|
||||
this.transport.listen(this.url, this.lastMessageId);
|
||||
this.running = true;
|
||||
delete this.starting;
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
NchanSubscriber.prototype.stop = function() {
|
||||
if(!this.running)
|
||||
throw "Can't stop NchanSubscriber, it's not running.";
|
||||
|
||||
this.running = false;
|
||||
if(storageEventListener) {
|
||||
global.removeEventListener("storage", storageEventListener);
|
||||
}
|
||||
this.transport.cancel();
|
||||
if(this.shared) {
|
||||
delete sharedSubscriberTable[this.url];
|
||||
if(this.shared.masterIntervalCheckID) {
|
||||
clearInterval(this.shared.masterIntervalCheckID);
|
||||
delete this.shared.masterIntervalCheckID;
|
||||
}
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
function addLastMsgIdToQueryString(url, msgid) {
|
||||
if(msgid) {
|
||||
var m = url.match(/(\?.*)$/);
|
||||
url += (m ? "&" : "?") + "last_event_id=" + encodeURIComponent(msgid);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
NchanSubscriber.prototype.SubscriberClass = {
|
||||
'websocket': (function() {
|
||||
function WSWrapper(emit) {
|
||||
WebSocket;
|
||||
this.emit = emit;
|
||||
}
|
||||
|
||||
WSWrapper.prototype.websocketizeURL = function(url) {
|
||||
var m = url.match(/^((\w+:)?\/\/([^\/]+))?(\/)?(.*)/);
|
||||
var protocol = m[2];
|
||||
var host = m[3];
|
||||
var absolute = m[4];
|
||||
var path = m[5];
|
||||
|
||||
var loc;
|
||||
if(typeof window == "object") {
|
||||
loc = window.location;
|
||||
}
|
||||
|
||||
if(!protocol && loc) {
|
||||
protocol = loc.protocol;
|
||||
}
|
||||
if(protocol == "https:") {
|
||||
protocol = "wss:";
|
||||
}
|
||||
else if(protocol == "http:") {
|
||||
protocol = "ws:";
|
||||
}
|
||||
else {
|
||||
protocol = "wss:"; //default setting: secure
|
||||
}
|
||||
|
||||
if(!host && loc) {
|
||||
host = loc.host;
|
||||
}
|
||||
|
||||
if(!absolute) {
|
||||
path = loc ? loc.pathname.match(/(.*\/)[^/]*/)[1] + path : "/" + path;
|
||||
}
|
||||
else {
|
||||
path = "/" + path;
|
||||
}
|
||||
|
||||
return protocol + "//" + host + path;
|
||||
};
|
||||
|
||||
WSWrapper.prototype.listen = function(url, msgid) {
|
||||
url = this.websocketizeURL(url);
|
||||
url = addLastMsgIdToQueryString(url, msgid);
|
||||
//console.log(url);
|
||||
if(this.listener) {
|
||||
throw "websocket already listening";
|
||||
}
|
||||
this.listener = new WebSocket(url, 'ws+meta.nchan');
|
||||
var l = this.listener;
|
||||
l.onmessage = ughbind(function(evt) {
|
||||
var m = evt.data.match(/^id: (.*)\n(content-type: (.*)\n)?\n/m);
|
||||
this.emit('message', evt.data.substr(m[0].length), {'id': m[1], 'content-type': m[3]});
|
||||
}, this);
|
||||
|
||||
l.onopen = ughbind(function(evt) {
|
||||
this.emit('connect', evt);
|
||||
//console.log("connect", evt);
|
||||
}, this);
|
||||
|
||||
l.onerror = ughbind(function(evt) {
|
||||
//console.log("error", evt);
|
||||
this.emit('error', evt, l);
|
||||
delete this.listener;
|
||||
}, this);
|
||||
|
||||
l.onclose = ughbind(function(evt) {
|
||||
this.emit('__disconnect', evt);
|
||||
delete this.listener;
|
||||
}, this);
|
||||
};
|
||||
|
||||
WSWrapper.prototype.cancel = function() {
|
||||
if(this.listener) {
|
||||
this.listener.close();
|
||||
delete this.listener;
|
||||
}
|
||||
};
|
||||
|
||||
return WSWrapper;
|
||||
})(),
|
||||
|
||||
'eventsource': (function() {
|
||||
function ESWrapper(emit) {
|
||||
EventSource;
|
||||
this.emit = emit;
|
||||
}
|
||||
|
||||
ESWrapper.prototype.listen= function(url, msgid) {
|
||||
url = addLastMsgIdToQueryString(url, msgid);
|
||||
if(this.listener) {
|
||||
throw "there's a ES listener running already";
|
||||
}
|
||||
this.listener = new EventSource(url);
|
||||
var l = this.listener;
|
||||
l.onmessage = ughbind(function(evt){
|
||||
//console.log("message", evt);
|
||||
this.emit('message', evt.data, {id: evt.lastEventId});
|
||||
}, this);
|
||||
|
||||
l.onopen = ughbind(function(evt) {
|
||||
this.reconnecting = false;
|
||||
//console.log("connect", evt);
|
||||
this.emit('connect', evt);
|
||||
}, this);
|
||||
|
||||
l.onerror = ughbind(function(evt) {
|
||||
//EventSource will try to reconnect by itself
|
||||
//console.log("onerror", this.listener.readyState, evt);
|
||||
if(this.listener.readyState == EventSource.CONNECTING && !this.reconnecting) {
|
||||
if(!this.reconnecting) {
|
||||
this.reconnecting = true;
|
||||
this.emit('__disconnect', evt);
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.emit('__disconnect', evt);
|
||||
//console.log('other __disconnect', evt);
|
||||
}
|
||||
}, this);
|
||||
};
|
||||
|
||||
ESWrapper.prototype.cancel= function() {
|
||||
if(this.listener) {
|
||||
this.listener.close();
|
||||
delete this.listener;
|
||||
}
|
||||
};
|
||||
|
||||
return ESWrapper;
|
||||
})(),
|
||||
|
||||
'longpoll': (function () {
|
||||
function Longpoll(emit) {
|
||||
this.headers = {};
|
||||
this.longPollStartTime = null;
|
||||
this.maxLongPollTime = 5*60*1000; //5 minutes
|
||||
this.emit = emit;
|
||||
}
|
||||
|
||||
Longpoll.prototype.listen = function(url, msgid) {
|
||||
if(this.req) {
|
||||
throw "already listening";
|
||||
}
|
||||
if(url) { this.url=url; }
|
||||
var setHeader = ughbind(function(incoming, name) {
|
||||
if(incoming) { this.headers[name]= incoming; }
|
||||
}, this);
|
||||
|
||||
if(msgid) {
|
||||
this.headers = {"Etag": msgid};
|
||||
}
|
||||
|
||||
this.reqStartTime = new Date().getTime();
|
||||
|
||||
var requestCallback;
|
||||
requestCallback = ughbind(function (code, response_text, req) {
|
||||
setHeader(req.getResponseHeader('Last-Modified'), 'If-Modified-Since');
|
||||
setHeader(req.getResponseHeader('Etag'), 'If-None-Match');
|
||||
|
||||
if(code >= 200 && code <= 210) {
|
||||
//legit reply
|
||||
var content_type = req.getResponseHeader('Content-Type');
|
||||
if (!this.parseMultipartMixedMessage(content_type, response_text, req)) {
|
||||
this.emit("message", response_text || "", {'content-type': content_type, 'id': this.msgIdFromResponseHeaders(req)});
|
||||
}
|
||||
|
||||
this.reqStartTime = new Date().getTime();
|
||||
this.req = nanoajax.ajax({url: this.url, headers: this.headers}, requestCallback);
|
||||
}
|
||||
else if((code == 0 && response_text == "Error" && req.readyState == 4) || (code === null && response_text != "Abort")) {
|
||||
//console.log("abort!!!");
|
||||
this.emit("__disconnect", code || 0, response_text);
|
||||
delete this.req;
|
||||
}
|
||||
else if(code !== null) {
|
||||
//HTTP error
|
||||
this.emit("error", code, response_text);
|
||||
delete this.req;
|
||||
}
|
||||
else {
|
||||
//don't care about abortions
|
||||
delete this.req;
|
||||
this.emit("__disconnect");
|
||||
//console.log("abort!");
|
||||
}
|
||||
}, this);
|
||||
|
||||
this.reqStartTime = new Date().getTime();
|
||||
this.req = nanoajax.ajax({url: this.url, headers: this.headers}, requestCallback);
|
||||
this.emit("connect");
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
Longpoll.prototype.parseMultipartMixedMessage = function(content_type, text, req) {
|
||||
var m = content_type && content_type.match(/^multipart\/mixed;\s+boundary=(.*)$/);
|
||||
if(!m) {
|
||||
return false;
|
||||
}
|
||||
var boundary = m[1];
|
||||
|
||||
var msgs = text.split("--" + boundary);
|
||||
if(msgs[0] != "" || !msgs[msgs.length-1].match(/--\r?\n/)) { throw "weird multipart/mixed split"; }
|
||||
|
||||
msgs = msgs.slice(1, -1);
|
||||
for(var i in msgs) {
|
||||
m = msgs[i].match(/^(.*)\r?\n\r?\n([\s\S]*)\r?\n$/m);
|
||||
var hdrs = m[1].split("\n");
|
||||
|
||||
var meta = {};
|
||||
for(var j in hdrs) {
|
||||
var hdr = hdrs[j].match(/^([^:]+):\s+(.*)/);
|
||||
if(hdr && hdr[1] == "Content-Type") {
|
||||
meta["content-type"] = hdr[2];
|
||||
}
|
||||
}
|
||||
|
||||
if(i == msgs.length - 1) {
|
||||
meta["id"] = this.msgIdFromResponseHeaders(req);
|
||||
}
|
||||
this.emit('message', m[2], meta);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
Longpoll.prototype.msgIdFromResponseHeaders = function(req) {
|
||||
var lastModified, etag;
|
||||
lastModified = req.getResponseHeader('Last-Modified');
|
||||
etag = req.getResponseHeader('Etag');
|
||||
if(lastModified) {
|
||||
return "" + Date.parse(lastModified)/1000 + ":" + (etag || "0");
|
||||
}
|
||||
else if(etag) {
|
||||
return etag;
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
Longpoll.prototype.cancel = function() {
|
||||
if(this.req) {
|
||||
this.req.abort();
|
||||
delete this.req;
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
return Longpoll;
|
||||
})(),
|
||||
|
||||
'__slave': (function() {
|
||||
function LocalStoreSlaveTransport(emit) {
|
||||
this.emit = emit;
|
||||
this.doNotReconnect = true;
|
||||
}
|
||||
|
||||
LocalStoreSlaveTransport.prototype.listen = function(url, shared) {
|
||||
this.shared = shared;
|
||||
this.statusChangeChecker = ughbind(function(ev) {
|
||||
if(this.shared.matchEventKey(ev, "msg")) {
|
||||
var msgId = this.shared.get("msg:id");
|
||||
var contentType = this.shared.get("msg:content-type");
|
||||
var msg = this.shared.get("msg");
|
||||
this.emit('message', msg, {'id': msgId == "" ? undefined : msgId, 'content-type': contentType == "" ? undefined : contentType});
|
||||
}
|
||||
}, this);
|
||||
global.addEventListener("storage", this.statusChangeChecker);
|
||||
};
|
||||
|
||||
LocalStoreSlaveTransport.prototype.cancel = function() {
|
||||
global.removeEventListener("storage", this.statusChangeChecker);
|
||||
};
|
||||
|
||||
return LocalStoreSlaveTransport;
|
||||
})()
|
||||
|
||||
};
|
||||
|
||||
return NchanSubscriber;
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user