diff --git a/config/config.example.php b/config/config.example.php index 823b84285..b3a0c2426 100644 --- a/config/config.example.php +++ b/config/config.example.php @@ -145,8 +145,6 @@ /** * enable push notification * - * dependencies must be installed for this setting to take effect - * * see INSTALLATION.md for details */ 'push_enabled' => true, diff --git a/config/config.kzsu.php b/config/config.kzsu.php index 809e75c98..425cdeab0 100644 --- a/config/config.kzsu.php +++ b/config/config.kzsu.php @@ -142,8 +142,6 @@ /** * enable push notification * - * dependencies must be installed for this setting to take effect - * * see INSTALLATION.md for details */ 'push_enabled' => true, diff --git a/controllers/PushServer.php b/controllers/PushServer.php index 5dc5443e6..2812f8ee1 100644 --- a/controllers/PushServer.php +++ b/controllers/PushServer.php @@ -40,251 +40,240 @@ use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; -if(interface_exists(MessageComponentInterface::class) && - Engine::param('push_enabled', true)) { - class NowAiringServer implements MessageComponentInterface { - const TIME_FORMAT_INTERNAL = "Y-m-d Hi"; // eg, 2019-01-01 1234 - - protected $clients; - protected $loop; - protected $timer; - - protected $current; - protected $nextSpin; - - public static function toJson($show, $spin) { - $val['name'] = $show?$show['description']:''; - $val['airname'] = $show?$show['airname']:''; - $val['show_id'] = $show?(int)$show['id']:0; - if($show && isset($show['showdate']) && isset($show['showtime'])) { - $date = $show['showdate']; - list($from, $to) = explode("-", $show['showtime']); - $fromStamp = \DateTime::createFromFormat(self::TIME_FORMAT_INTERNAL, - $date . " " . $from); - $toStamp = \DateTime::createFromFormat(self::TIME_FORMAT_INTERNAL, - $date . " " . $to); - - // if playlist spans midnight, end time is next day - if($toStamp < $fromStamp) - $toStamp->modify("+1 day"); - - $val['show_start'] = $fromStamp->format(DATE_RFC3339); - $val['show_end'] = $toStamp->format(DATE_RFC3339); - } else { - $val['show_start'] = ''; - $val['show_end'] = ''; - } - $val['id'] = $spin?(int)$spin['id']:0; - $val['track_title'] = $spin?$spin['track']:''; - $val['track_artist'] = $spin?$spin['artist']:''; - $val['track_album'] = $spin?$spin['album']:''; - return json_encode($val); +class NowAiringServer implements MessageComponentInterface { + const TIME_FORMAT_INTERNAL = "Y-m-d Hi"; // eg, 2019-01-01 1234 + + protected $clients; + protected $loop; + protected $timer; + + protected $current; + protected $nextSpin; + + public static function toJson($show, $spin) { + $val['name'] = $show?$show['description']:''; + $val['airname'] = $show?$show['airname']:''; + $val['show_id'] = $show?(int)$show['id']:0; + if($show && isset($show['showdate']) && isset($show['showtime'])) { + $date = $show['showdate']; + list($from, $to) = explode("-", $show['showtime']); + $fromStamp = \DateTime::createFromFormat(self::TIME_FORMAT_INTERNAL, + $date . " " . $from); + $toStamp = \DateTime::createFromFormat(self::TIME_FORMAT_INTERNAL, + $date . " " . $to); + + // if playlist spans midnight, end time is next day + if($toStamp < $fromStamp) + $toStamp->modify("+1 day"); + + $val['show_start'] = $fromStamp->format(DATE_RFC3339); + $val['show_end'] = $toStamp->format(DATE_RFC3339); + } else { + $val['show_start'] = ''; + $val['show_end'] = ''; } + $val['id'] = $spin?(int)$spin['id']:0; + $val['track_title'] = $spin?$spin['track']:''; + $val['track_artist'] = $spin?$spin['artist']:''; + $val['track_album'] = $spin?$spin['album']:''; + return json_encode($val); + } - public function __construct($loop) { - $this->clients = new \SplObjectStorage; - $this->loop = $loop; - } + public function __construct($loop) { + $this->clients = new \SplObjectStorage; + $this->loop = $loop; + } - /* - * fetch on-air track from database - * - * @returns true if changed, false otherwise - */ - protected function loadOnNow() { - $changed = false; - $event = null; - $result = Engine::api(IPlaylist::class)->getWhatsOnNow(); - if($show = $result->fetch()) { - $filter = Engine::api(IPlaylist::class)->getTracksWithObserver($show['id'], - (new PlaylistObserver())->onSpin(function($entry) use(&$event) { - $spin = $entry->asArray(); - $spin['artist'] = UI::swapNames($spin['artist']); - $event = $spin; - })->onComment(function($entry) use(&$event) { - $event = null; - })->onLogEvent(function($entry) use(&$event) { - $event = null; - })->onSetSeparator(function($entry) use(&$event) { - $event = null; - }), 0, OnNowFilter::class); - } - DBO::release(); - $current = self::toJSON($show, $event); - if($this->current != $current) { - $this->current = $current; - $changed = true; - } - $this->nextSpin = $show?$filter->peek():null; - return $changed; + /* + * fetch on-air track from database + * + * @returns true if changed, false otherwise + */ + protected function loadOnNow() { + $changed = false; + $event = null; + $result = Engine::api(IPlaylist::class)->getWhatsOnNow(); + if($show = $result->fetch()) { + $filter = Engine::api(IPlaylist::class)->getTracksWithObserver($show['id'], + (new PlaylistObserver())->onSpin(function($entry) use(&$event) { + $spin = $entry->asArray(); + $spin['artist'] = UI::swapNames($spin['artist']); + $event = $spin; + })->onComment(function($entry) use(&$event) { + $event = null; + })->onLogEvent(function($entry) use(&$event) { + $event = null; + })->onSetSeparator(function($entry) use(&$event) { + $event = null; + }), 0, OnNowFilter::class); } - - protected function worker() { - // echo "worker awake\n"; - if($this->loadOnNow()) - $this->sendNotification(); + DBO::release(); + $current = self::toJSON($show, $event); + if($this->current != $current) { + $this->current = $current; + $changed = true; } + $this->nextSpin = $show?$filter->peek():null; + return $changed; + } + + protected function worker() { + // echo "worker awake\n"; + if($this->loadOnNow()) + $this->sendNotification(); + } - protected function scheduleWorker() { - if($this->clients->count() > 0) { - $now = new \DateTime(); - if($this->nextSpin) { - $next = new \DateTime($this->nextSpin->getCreated()); - $timeToNext = $next->getTimestamp() - $now->getTimestamp(); - if($timeToNext < 0 || $timeToNext > 60) - $timeToNext = 0; - } else + protected function scheduleWorker() { + if($this->clients->count() > 0) { + $now = new \DateTime(); + if($this->nextSpin) { + $next = new \DateTime($this->nextSpin->getCreated()); + $timeToNext = $next->getTimestamp() - $now->getTimestamp(); + if($timeToNext < 0 || $timeToNext > 60) $timeToNext = 0; + } else + $timeToNext = 0; - $delta = $timeToNext?($timeToNext + 1): - (61 - (int)$now->format("s")); + $delta = $timeToNext?($timeToNext + 1): + (61 - (int)$now->format("s")); - $this->timer = $this->loop->addTimer($delta, function() { - $this->worker(); - $this->scheduleWorker(); - }); - } + $this->timer = $this->loop->addTimer($delta, function() { + $this->worker(); + $this->scheduleWorker(); + }); } + } - public function refreshOnNow() { - if($this->clients->count() > 0 && $this->loadOnNow()) - $this->sendNotification(); - } + public function refreshOnNow() { + if($this->clients->count() > 0 && $this->loadOnNow()) + $this->sendNotification(); + } - public function onOpen(ConnectionInterface $conn) { - $this->clients->attach($conn); - if($this->clients->count() == 1) { - $this->loadOnNow(); + public function onOpen(ConnectionInterface $conn) { + $this->clients->attach($conn); + if($this->clients->count() == 1) { + $this->loadOnNow(); - // start worker - $this->scheduleWorker(); - } - $this->sendNotification(null, $conn); - echo "New connection {$conn->resourceId}\n"; + // start worker + $this->scheduleWorker(); } + $this->sendNotification(null, $conn); + echo "New connection {$conn->resourceId}\n"; + } - public function sendNotification($msg = null, $client = null) { - if($msg) { - if($this->current != $msg) - $this->current = $msg; - else - return; - } else - $msg = $this->current; + public function sendNotification($msg = null, $client = null) { + if($msg) { + if($this->current != $msg) + $this->current = $msg; + else + return; + } else + $msg = $this->current; - if($client) + if($client) + $client->send($msg); + else { + foreach ($this->clients as $client) $client->send($msg); - else { - foreach ($this->clients as $client) - $client->send($msg); - } } + } - public function onMessage(ConnectionInterface $from, $msg) {} + public function onMessage(ConnectionInterface $from, $msg) {} - public function onClose(ConnectionInterface $conn) { - $this->clients->detach($conn); + public function onClose(ConnectionInterface $conn) { + $this->clients->detach($conn); - if($this->clients->count() == 0 && $this->timer) { - // stop worker - echo "Cancel timer ".spl_object_hash($this->timer)."\n"; - $this->loop->cancelTimer($this->timer); - $this->timer = null; - } - echo "Connection {$conn->resourceId} disconnected\n"; + if($this->clients->count() == 0 && $this->timer) { + // stop worker + echo "Cancel timer ".spl_object_hash($this->timer)."\n"; + $this->loop->cancelTimer($this->timer); + $this->timer = null; } + echo "Connection {$conn->resourceId} disconnected\n"; + } - public function onError(ConnectionInterface $conn, \Exception $e) { - error_log("NowAiringServer: " . $e->getMessage()); - $conn->close(); - } + public function onError(ConnectionInterface $conn, \Exception $e) { + error_log("NowAiringServer: " . $e->getMessage()); + $conn->close(); + } +} + +class PushServer implements IController { + /** + * This is an endpoint for internal use only. There should be + * no need to change it, but if you do, you must also update the + * corresponding URI in .htaccess in the project root directory. + */ + const WSSERVER_HOST = "127.0.0.1"; + const WSSERVER_PORT = 32080; + + public static function sendAsyncNotification($show = null, $spin = null) { + if(!Engine::param('push_enabled', true)) + return; + + $data = ($show != null)?NowAiringServer::toJson($show, $spin):""; + $socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP); + socket_sendto($socket, $data, strlen($data), 0, + PushServer::WSSERVER_HOST, PushServer::WSSERVER_PORT); + socket_close($socket); } - class PushServer implements IController { - /** - * This is an endpoint for internal use only. There should be - * no need to change it, but if you do, you must also update the - * corresponding URI in .htaccess in the project root directory. - */ - const WSSERVER_HOST = "127.0.0.1"; - const WSSERVER_PORT = 32080; - - public static function sendAsyncNotification($show = null, $spin = null) { - $data = ($show != null)?NowAiringServer::toJson($show, $spin):""; - $socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP); - socket_sendto($socket, $data, strlen($data), 0, - PushServer::WSSERVER_HOST, PushServer::WSSERVER_PORT); - socket_close($socket); + public function processRequest($dispatcher) { + if(php_sapi_name() != "cli") { + http_response_code(400); + return; } - public function processRequest($dispatcher) { - if(php_sapi_name() != "cli") { - http_response_code(400); - return; - } + if(!Engine::param('push_enabled', true)) { + echo "Push notification is disabled.\n\n"; + echo "See INSTALLATION.md for more information.\n"; + return; + } - try { - // websocket server for subscribers - $loop = \React\EventLoop\Factory::create(); - $nas = new NowAiringServer($loop); - $wsserver = new \Ratchet\WebSocket\WsServer($nas); - $wsserver->enableKeepAlive($loop, 30); - $routes = new RouteCollection(); - $routes->add('/push/onair', new Route('/push/onair', [ - '_controller' => $wsserver - ])); - $router = new \Ratchet\Http\Router( - new UrlMatcher($routes, new RequestContext())); - new IoServer(new \Ratchet\Http\HttpServer($router), - new \React\Socket\Server(PushServer::WSSERVER_HOST . ":" . - PushServer::WSSERVER_PORT, $loop)); - - // datagram server for application - $dgfact = new \React\Datagram\Factory($loop); - $dgfact->createServer(PushServer::WSSERVER_HOST . ":" . - PushServer::WSSERVER_PORT)->then( - function(\React\Datagram\Socket $client) use($nas) { - $client->on('message', function($message, $addr, $client) use($nas) { - // echo "received $message from $addr\n"; - // empty message means poll database - if($message) - $nas->sendNotification($message); - else - $nas->refreshOnNow(); - }); + try { + // websocket server for subscribers + $loop = \React\EventLoop\Factory::create(); + $nas = new NowAiringServer($loop); + $wsserver = new \Ratchet\WebSocket\WsServer($nas); + $wsserver->enableKeepAlive($loop, 30); + $routes = new RouteCollection(); + $routes->add('/push/onair', new Route('/push/onair', [ + '_controller' => $wsserver + ])); + $router = new \Ratchet\Http\Router( + new UrlMatcher($routes, new RequestContext())); + new IoServer(new \Ratchet\Http\HttpServer($router), + new \React\Socket\Server(PushServer::WSSERVER_HOST . ":" . + PushServer::WSSERVER_PORT, $loop)); + + // datagram server for application + $dgfact = new \React\Datagram\Factory($loop); + $dgfact->createServer(PushServer::WSSERVER_HOST . ":" . + PushServer::WSSERVER_PORT)->then( + function(\React\Datagram\Socket $client) use($nas) { + $client->on('message', function($message, $addr, $client) use($nas) { + // echo "received $message from $addr\n"; + // empty message means poll database + if($message) + $nas->sendNotification($message); + else + $nas->refreshOnNow(); }); - - // push proxy server, if configured - $config = Engine::param('push_proxy'); - if($config) { - foreach($config as $proxy) { - $app = new $proxy['proxy']($loop); - $app->connect($proxy['ws_endpoint'], - $proxy['http_endpoints']); - } + }); + + // push proxy server, if configured + $config = Engine::param('push_proxy'); + if($config) { + foreach($config as $proxy) { + $app = new $proxy['proxy']($loop); + $app->connect($proxy['ws_endpoint'], + $proxy['http_endpoints']); } - - $loop->run(); - } catch(\Exception $e) { - error_log("PushServer: " . $e->getMessage()); } - } - } -} else { - /* - * PushServer stub for push disabled or dependencies missing - */ - class PushServer implements IController { - public static function sendAsyncNotification($show = null, $spin = null) {} - public function processRequest($dispatcher) { - if(php_sapi_name() != "cli") { - http_response_code(400); - return; - } - - echo "Push notification is disabled or dependencies are missing.\n\n"; - echo "See INSTALLATION.md for more information.\n"; + $loop->run(); + } catch(\Exception $e) { + error_log("PushServer: " . $e->getMessage()); } } }