2016-05-06 10:57:34 +02:00
|
|
|
<?php
|
2020-10-15 00:19:31 +02:00
|
|
|
|
2021-07-19 07:53:45 +02:00
|
|
|
declare(strict_types=1);
|
|
|
|
|
2018-08-05 00:05:14 +02:00
|
|
|
namespace App\Radio\Frontend;
|
2016-05-06 10:57:34 +02:00
|
|
|
|
2018-08-05 00:05:14 +02:00
|
|
|
use App\Entity;
|
2022-01-26 19:27:31 +01:00
|
|
|
use App\Radio\Enums\StreamFormats;
|
2022-06-09 09:27:19 +02:00
|
|
|
use App\Service\Acme;
|
2019-08-07 06:33:55 +02:00
|
|
|
use App\Utilities;
|
2019-09-04 20:00:51 +02:00
|
|
|
use App\Xml\Writer;
|
2020-07-08 09:03:50 +02:00
|
|
|
use Exception;
|
2020-05-16 23:53:29 +02:00
|
|
|
use GuzzleHttp\Psr7\Uri;
|
2020-07-03 22:24:04 +02:00
|
|
|
use NowPlaying\Result\Result;
|
2018-10-05 01:12:12 +02:00
|
|
|
use Psr\Http\Message\UriInterface;
|
2021-11-16 14:13:43 +01:00
|
|
|
use Supervisor\Exception\SupervisorException as SupervisorLibException;
|
2016-05-06 10:57:34 +02:00
|
|
|
|
2018-12-27 09:24:07 +01:00
|
|
|
class Icecast extends AbstractFrontend
|
2016-05-06 10:57:34 +02:00
|
|
|
{
|
2018-09-09 20:23:03 +02:00
|
|
|
public const LOGLEVEL_DEBUG = 4;
|
|
|
|
public const LOGLEVEL_INFO = 3;
|
|
|
|
public const LOGLEVEL_WARN = 2;
|
|
|
|
public const LOGLEVEL_ERROR = 1;
|
2018-01-21 08:11:49 +01:00
|
|
|
|
2021-01-19 18:52:45 +01:00
|
|
|
public function supportsMounts(): bool
|
|
|
|
{
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2021-11-16 14:13:43 +01:00
|
|
|
public function supportsReload(): bool
|
|
|
|
{
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function reload(Entity\Station $station): void
|
|
|
|
{
|
|
|
|
if ($this->hasCommand($station)) {
|
|
|
|
$program_name = $this->getProgramName($station);
|
|
|
|
|
|
|
|
try {
|
|
|
|
$this->supervisor->signalProcess($program_name, 'HUP');
|
|
|
|
$this->logger->info(
|
|
|
|
'Adapter "' . static::class . '" reloaded.',
|
|
|
|
['station_id' => $station->getId(), 'station_name' => $station->getName()]
|
|
|
|
);
|
|
|
|
} catch (SupervisorLibException $e) {
|
|
|
|
$this->handleSupervisorException($e, $program_name, $station);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-03 22:24:04 +02:00
|
|
|
public function getNowPlaying(Entity\Station $station, bool $includeClients = true): Result
|
2017-01-24 01:17:50 +01:00
|
|
|
{
|
2020-07-03 22:24:04 +02:00
|
|
|
$feConfig = $station->getFrontendConfig();
|
|
|
|
$radioPort = $feConfig->getPort();
|
2018-08-26 05:14:53 +02:00
|
|
|
|
2022-05-07 15:50:48 +02:00
|
|
|
$baseUrl = $this->environment->getLocalUri()
|
2022-01-28 02:33:07 +01:00
|
|
|
->withPort($radioPort);
|
2017-01-24 01:17:50 +01:00
|
|
|
|
2021-04-12 00:33:51 +02:00
|
|
|
$npAdapter = $this->adapterFactory->getIcecastAdapter($baseUrl);
|
2017-01-24 01:17:50 +01:00
|
|
|
|
2020-09-16 13:56:25 +02:00
|
|
|
$npAdapter->setAdminPassword($feConfig->getAdminPassword());
|
|
|
|
|
2020-07-03 22:24:04 +02:00
|
|
|
$defaultResult = Result::blank();
|
|
|
|
$otherResults = [];
|
2017-08-20 09:35:41 +02:00
|
|
|
|
2021-04-16 07:13:42 +02:00
|
|
|
foreach ($station->getMounts() as $mount) {
|
|
|
|
try {
|
2020-07-03 22:24:04 +02:00
|
|
|
$result = $npAdapter->getNowPlaying($mount->getName(), $includeClients);
|
|
|
|
|
2021-01-27 18:42:54 +01:00
|
|
|
if (!empty($result->clients)) {
|
|
|
|
foreach ($result->clients as $client) {
|
|
|
|
$client->mount = 'local_' . $mount->getId();
|
|
|
|
}
|
|
|
|
}
|
2021-04-16 07:13:42 +02:00
|
|
|
} catch (Exception $e) {
|
|
|
|
$this->logger->error(sprintf('NowPlaying adapter error: %s', $e->getMessage()));
|
2021-01-27 18:42:54 +01:00
|
|
|
|
2021-04-16 07:13:42 +02:00
|
|
|
$result = Result::blank();
|
2020-07-03 22:24:04 +02:00
|
|
|
}
|
|
|
|
|
2021-04-16 07:13:42 +02:00
|
|
|
$mount->setListenersTotal($result->listeners->total);
|
2021-04-25 03:53:33 +02:00
|
|
|
$mount->setListenersUnique($result->listeners->unique ?? 0);
|
2021-04-16 07:13:42 +02:00
|
|
|
$this->em->persist($mount);
|
2020-07-03 22:24:04 +02:00
|
|
|
|
2021-04-16 07:13:42 +02:00
|
|
|
if ($mount->getIsDefault()) {
|
|
|
|
$defaultResult = $result;
|
|
|
|
} else {
|
|
|
|
$otherResults[] = $result;
|
2017-05-16 09:46:43 +02:00
|
|
|
}
|
2021-04-16 07:13:42 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
$this->em->flush();
|
|
|
|
|
|
|
|
foreach ($otherResults as $otherResult) {
|
|
|
|
$defaultResult = $defaultResult->merge($otherResult);
|
2017-05-26 10:58:23 +02:00
|
|
|
}
|
2018-10-02 16:53:58 +02:00
|
|
|
|
2020-07-03 22:24:04 +02:00
|
|
|
return $defaultResult;
|
2017-01-24 01:17:50 +01:00
|
|
|
}
|
|
|
|
|
2021-01-19 18:52:45 +01:00
|
|
|
public function getConfigurationPath(Entity\Station $station): ?string
|
2016-05-06 10:57:34 +02:00
|
|
|
{
|
2021-01-19 18:52:45 +01:00
|
|
|
return $station->getRadioConfigDir() . '/icecast.xml';
|
2016-05-06 10:57:34 +02:00
|
|
|
}
|
|
|
|
|
2021-01-19 18:52:45 +01:00
|
|
|
public function getCurrentConfiguration(Entity\Station $station): ?string
|
2016-05-06 10:57:34 +02:00
|
|
|
{
|
2021-01-19 18:52:45 +01:00
|
|
|
$frontendConfig = $station->getFrontendConfig();
|
|
|
|
$configDir = $station->getRadioConfigDir();
|
2020-06-29 23:26:48 +02:00
|
|
|
|
2021-04-23 07:24:12 +02:00
|
|
|
$settings = $this->settingsRepo->readSettings();
|
|
|
|
|
2021-11-25 19:04:06 +01:00
|
|
|
$settingsBaseUrl = $settings->getBaseUrl() ?: '';
|
|
|
|
$baseUrl = Utilities\Urls::getUri($settingsBaseUrl) ?? new Uri('http://localhost');
|
2016-05-06 10:57:34 +02:00
|
|
|
|
2022-06-09 09:27:19 +02:00
|
|
|
[$certPath, $certKey] = Acme::getCertificatePaths();
|
2020-06-29 23:26:48 +02:00
|
|
|
|
2021-01-19 18:52:45 +01:00
|
|
|
$config = [
|
2022-03-09 23:55:30 +01:00
|
|
|
'location' => 'AzuraCast',
|
|
|
|
'admin' => 'icemaster@localhost',
|
|
|
|
'hostname' => $baseUrl->getHost(),
|
|
|
|
'limits' => [
|
|
|
|
'clients' => $frontendConfig->getMaxListeners() ?? 2500,
|
|
|
|
'sources' => $station->getMounts()->count(),
|
|
|
|
'queue-size' => 524288,
|
2016-05-19 16:56:21 +02:00
|
|
|
'client-timeout' => 30,
|
|
|
|
'header-timeout' => 15,
|
|
|
|
'source-timeout' => 10,
|
2022-06-09 09:27:19 +02:00
|
|
|
'burst-size' => 65535,
|
2016-05-19 16:56:21 +02:00
|
|
|
],
|
|
|
|
'authentication' => [
|
2021-01-19 18:52:45 +01:00
|
|
|
'source-password' => $frontendConfig->getSourcePassword(),
|
2022-06-09 09:27:19 +02:00
|
|
|
'relay-password' => $frontendConfig->getRelayPassword(),
|
|
|
|
'admin-user' => 'admin',
|
|
|
|
'admin-password' => $frontendConfig->getAdminPassword(),
|
2016-05-19 16:56:21 +02:00
|
|
|
],
|
2016-09-02 06:53:50 +02:00
|
|
|
|
2016-05-19 16:56:21 +02:00
|
|
|
'listen-socket' => [
|
2021-01-19 18:52:45 +01:00
|
|
|
'port' => $frontendConfig->getPort(),
|
2016-05-19 16:56:21 +02:00
|
|
|
],
|
2016-09-02 06:53:50 +02:00
|
|
|
|
2016-11-17 04:15:34 +01:00
|
|
|
'mount' => [],
|
2016-05-19 16:56:21 +02:00
|
|
|
'fileserve' => 1,
|
|
|
|
'paths' => [
|
2017-05-17 06:11:45 +02:00
|
|
|
'basedir' => '/usr/local/share/icecast',
|
2021-01-19 18:52:45 +01:00
|
|
|
'logdir' => $configDir,
|
2017-05-17 06:11:45 +02:00
|
|
|
'webroot' => '/usr/local/share/icecast/web',
|
|
|
|
'adminroot' => '/usr/local/share/icecast/admin',
|
2021-01-19 18:52:45 +01:00
|
|
|
'pidfile' => $configDir . '/icecast.pid',
|
2016-05-19 16:56:21 +02:00
|
|
|
'alias' => [
|
2021-05-05 09:31:06 +02:00
|
|
|
[
|
|
|
|
'@source' => '/',
|
|
|
|
'@dest' => '/status.xsl',
|
|
|
|
],
|
2016-05-19 16:56:21 +02:00
|
|
|
],
|
2022-06-09 09:27:19 +02:00
|
|
|
'ssl-private-key' => $certKey,
|
|
|
|
'ssl-certificate' => $certPath,
|
2020-10-15 00:19:31 +02:00
|
|
|
// phpcs:disable Generic.Files.LineLength
|
2018-01-02 01:59:54 +01:00
|
|
|
'ssl-allowed-ciphers' => 'ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!MD5:!DSS',
|
2020-10-15 00:19:31 +02:00
|
|
|
// phpcs:enable
|
2020-03-02 01:03:03 +01:00
|
|
|
'deny-ip' => $this->writeIpBansFile($station),
|
2022-02-10 21:40:33 +01:00
|
|
|
'deny-agents' => $this->writeUserAgentBansFile($station),
|
2022-05-03 22:10:16 +02:00
|
|
|
'x-forwarded-for' => '127.0.0.1',
|
2016-05-19 16:56:21 +02:00
|
|
|
],
|
|
|
|
'logging' => [
|
|
|
|
'accesslog' => 'icecast_access.log',
|
2019-03-08 03:01:51 +01:00
|
|
|
'errorlog' => '/dev/stderr',
|
2020-12-06 12:57:39 +01:00
|
|
|
'loglevel' => $this->environment->isProduction() ? self::LOGLEVEL_WARN : self::LOGLEVEL_INFO,
|
2016-05-19 16:56:21 +02:00
|
|
|
'logsize' => 10000,
|
|
|
|
],
|
|
|
|
'security' => [
|
|
|
|
'chroot' => 0,
|
|
|
|
],
|
|
|
|
];
|
2016-11-17 04:15:34 +01:00
|
|
|
|
2018-09-22 13:52:43 +02:00
|
|
|
foreach ($station->getMounts() as $mount_row) {
|
2017-08-17 20:28:48 +02:00
|
|
|
/** @var Entity\StationMount $mount_row */
|
|
|
|
|
2016-11-17 04:15:34 +01:00
|
|
|
$mount = [
|
2017-01-24 01:35:16 +01:00
|
|
|
'@type' => 'normal',
|
2017-08-17 20:28:48 +02:00
|
|
|
'mount-name' => $mount_row->getName(),
|
2017-10-19 23:55:02 +02:00
|
|
|
'charset' => 'UTF8',
|
2018-11-26 09:16:55 +01:00
|
|
|
'stream-name' => $station->getName(),
|
2016-11-17 04:15:34 +01:00
|
|
|
];
|
|
|
|
|
2021-02-05 17:58:38 +01:00
|
|
|
if (!empty($station->getDescription())) {
|
|
|
|
$mount['stream-description'] = $station->getDescription();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!empty($station->getUrl())) {
|
|
|
|
$mount['stream-url'] = $station->getUrl();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!empty($station->getGenre())) {
|
|
|
|
$mount['genre'] = $station->getGenre();
|
|
|
|
}
|
|
|
|
|
2021-11-07 06:02:44 +01:00
|
|
|
if (!$mount_row->getIsVisibleOnPublicPages()) {
|
2021-01-04 09:35:04 +01:00
|
|
|
$mount['hidden'] = 1;
|
|
|
|
}
|
|
|
|
|
2021-08-01 12:00:42 +02:00
|
|
|
if (!empty($mount_row->getIntroPath())) {
|
|
|
|
$introPath = $mount_row->getIntroPath();
|
|
|
|
// The intro path is appended to webroot, hence the 5 ../es. Amazingly, this works!
|
|
|
|
$mount['intro'] = '../../../../../' . $station->getRadioConfigDir() . '/' . $introPath;
|
|
|
|
}
|
|
|
|
|
2017-08-17 20:28:48 +02:00
|
|
|
if (!empty($mount_row->getFallbackMount())) {
|
|
|
|
$mount['fallback-mount'] = $mount_row->getFallbackMount();
|
2016-11-17 04:15:34 +01:00
|
|
|
$mount['fallback-override'] = 1;
|
2022-01-26 19:27:31 +01:00
|
|
|
} elseif ($mount_row->getEnableAutodj()) {
|
|
|
|
$autoDjFormat = $mount_row->getAutodjFormatEnum() ?? StreamFormats::default();
|
|
|
|
$autoDjBitrate = $mount_row->getAutodjBitrate();
|
|
|
|
|
|
|
|
$mount['fallback-mount'] = '/fallback-[' . $autoDjBitrate . '].' . $autoDjFormat->getExtension();
|
|
|
|
$mount['fallback-override'] = 1;
|
2016-11-17 04:15:34 +01:00
|
|
|
}
|
|
|
|
|
2021-06-21 00:22:32 +02:00
|
|
|
if ($mount_row->getMaxListenerDuration()) {
|
|
|
|
$mount['max-listener-duration'] = $mount_row->getMaxListenerDuration();
|
|
|
|
}
|
|
|
|
|
2021-05-05 09:31:06 +02:00
|
|
|
$mountFrontendConfig = trim($mount_row->getFrontendConfig() ?? '');
|
|
|
|
if (!empty($mountFrontendConfig)) {
|
|
|
|
$mount_conf = $this->processCustomConfig($mountFrontendConfig);
|
|
|
|
if (false !== $mount_conf) {
|
2021-01-19 18:52:45 +01:00
|
|
|
$mount = Utilities\Arrays::arrayMergeRecursiveDistinct($mount, $mount_conf);
|
2017-01-24 01:35:16 +01:00
|
|
|
}
|
2016-11-17 19:26:43 +01:00
|
|
|
}
|
|
|
|
|
2021-11-25 19:04:06 +01:00
|
|
|
$mountRelayUri = $mount_row->getRelayUrlAsUri();
|
|
|
|
if (null !== $mountRelayUri) {
|
2021-01-19 18:52:45 +01:00
|
|
|
$config['relay'][] = [
|
2022-06-09 09:27:19 +02:00
|
|
|
'server' => $mountRelayUri->getHost(),
|
|
|
|
'port' => $mountRelayUri->getPort(),
|
|
|
|
'mount' => $mountRelayUri->getPath(),
|
2017-08-17 20:28:48 +02:00
|
|
|
'local-mount' => $mount_row->getName(),
|
2017-04-13 00:19:02 +02:00
|
|
|
];
|
|
|
|
}
|
|
|
|
|
2021-08-14 23:28:24 +02:00
|
|
|
$bannedCountries = $station->getFrontendConfig()->getBannedCountries() ?? [];
|
|
|
|
if (!empty($bannedCountries)) {
|
2022-05-07 15:50:48 +02:00
|
|
|
$mountAuthenticationUrl = $this->environment->getInternalUri()
|
2022-04-09 04:22:53 +02:00
|
|
|
->withPath('/api/internal/' . $station->getIdRequired() . '/listener-auth')
|
|
|
|
->withQuery(
|
|
|
|
http_build_query([
|
|
|
|
'api_auth' => $station->getAdapterApiKey(),
|
|
|
|
])
|
|
|
|
);
|
2021-08-14 23:28:24 +02:00
|
|
|
|
|
|
|
$mount['authentication'][] = [
|
|
|
|
'@type' => 'url',
|
|
|
|
'option' => [
|
|
|
|
[
|
|
|
|
'@name' => 'listener_add',
|
2022-04-09 04:22:53 +02:00
|
|
|
'@value' => (string)$mountAuthenticationUrl,
|
2021-08-14 23:28:24 +02:00
|
|
|
],
|
|
|
|
[
|
|
|
|
'@name' => 'auth_header',
|
|
|
|
'@value' => 'icecast-auth-user: 1',
|
|
|
|
],
|
|
|
|
],
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
2021-01-19 18:52:45 +01:00
|
|
|
$config['mount'][] = $mount;
|
2019-09-04 20:00:51 +02:00
|
|
|
}
|
|
|
|
|
2021-05-05 09:31:06 +02:00
|
|
|
$customConfig = trim($frontendConfig->getCustomConfiguration() ?? '');
|
2020-05-02 09:58:59 +02:00
|
|
|
if (!empty($customConfig)) {
|
2021-06-02 02:53:24 +02:00
|
|
|
$customConfParsed = $this->processCustomConfig($customConfig);
|
2021-05-05 09:31:06 +02:00
|
|
|
|
2021-07-19 07:53:45 +02:00
|
|
|
if (false !== $customConfParsed) {
|
|
|
|
// Special handling for aliases.
|
|
|
|
if (isset($customConfParsed['paths']['alias'])) {
|
|
|
|
$alias = (array)$customConfParsed['paths']['alias'];
|
|
|
|
if (!is_numeric(key($alias))) {
|
|
|
|
$alias = [$alias];
|
|
|
|
}
|
|
|
|
$customConfParsed['paths']['alias'] = $alias;
|
2021-06-02 02:53:24 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
$config = Utilities\Arrays::arrayMergeRecursiveDistinct($config, $customConfParsed);
|
2019-09-04 20:00:51 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-20 12:16:10 +02:00
|
|
|
$configString = Writer::toString($config, 'icecast');
|
2019-09-04 20:00:51 +02:00
|
|
|
|
|
|
|
// Strip the first line (the XML charset)
|
2021-01-19 18:52:45 +01:00
|
|
|
return substr($configString, strpos($configString, "\n") + 1);
|
2019-09-04 20:00:51 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public function getCommand(Entity\Station $station): ?string
|
|
|
|
{
|
2021-01-19 18:52:45 +01:00
|
|
|
if ($binary = $this->getBinary()) {
|
|
|
|
return $binary . ' -c ' . $this->getConfigurationPath($station);
|
2019-09-04 20:00:51 +02:00
|
|
|
}
|
2021-01-19 18:52:45 +01:00
|
|
|
return null;
|
2019-09-04 20:00:51 +02:00
|
|
|
}
|
|
|
|
|
2020-10-15 00:19:31 +02:00
|
|
|
/**
|
|
|
|
* @inheritDoc
|
|
|
|
*/
|
2021-01-19 18:52:45 +01:00
|
|
|
public function getBinary(): ?string
|
2019-09-04 20:00:51 +02:00
|
|
|
{
|
|
|
|
$new_path = '/usr/local/bin/icecast';
|
|
|
|
$legacy_path = '/usr/bin/icecast2';
|
|
|
|
|
2021-01-19 18:52:45 +01:00
|
|
|
if ($this->environment->isDocker() || file_exists($new_path)) {
|
2019-09-04 20:00:51 +02:00
|
|
|
return $new_path;
|
2019-12-07 01:57:50 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if (file_exists($legacy_path)) {
|
2019-09-04 20:00:51 +02:00
|
|
|
return $legacy_path;
|
|
|
|
}
|
2021-01-04 09:35:04 +01:00
|
|
|
|
2021-01-19 18:52:45 +01:00
|
|
|
return null;
|
2019-09-04 20:00:51 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public function getAdminUrl(Entity\Station $station, UriInterface $base_url = null): UriInterface
|
|
|
|
{
|
|
|
|
$public_url = $this->getPublicUrl($station, $base_url);
|
|
|
|
return $public_url
|
|
|
|
->withPath($public_url->getPath() . '/admin.html');
|
|
|
|
}
|
2018-07-15 05:10:04 +02:00
|
|
|
}
|