Refactor adapters; move some static functions to be non-static and implement a better change tracking system.

This commit is contained in:
Buster "Silver Eagle" Neece 2021-01-19 11:52:45 -06:00
parent 686f480d7c
commit 4ccddeb5f3
No known key found for this signature in database
GPG Key ID: 6D9E12FF03411F4E
30 changed files with 553 additions and 665 deletions

View File

@ -6,13 +6,17 @@ use App\Entity\StationFrontendConfiguration;
use App\Entity\StationMountInterface;
use App\Radio\Adapters;
$frontends = Adapters::listFrontendAdapters(true);
/**
* @var Adapters $adapters
*/
$frontends = $adapters->listFrontendAdapters(true);
$frontend_types = [];
foreach ($frontends as $adapter_nickname => $adapter_info) {
$frontend_types[$adapter_nickname] = $adapter_info['name'];
}
$backends = Adapters::listBackendAdapters(true);
$backends = $adapters->listBackendAdapters(true);
$backend_types = [];
foreach ($backends as $adapter_nickname => $adapter_info) {
$backend_types[$adapter_nickname] = $adapter_info['name'];

View File

@ -60,21 +60,21 @@ return function (App\Event\BuildStationMenu $e) {
'label' => __('Music Files'),
'icon' => 'library_music',
'url' => $router->fromHere('stations:files:index'),
'visible' => $backend::supportsMedia(),
'visible' => $backend->supportsMedia(),
'permission' => Acl::STATION_MEDIA,
],
'playlists' => [
'label' => __('Playlists'),
'icon' => 'queue_music',
'url' => $router->fromHere('stations:playlists:index'),
'visible' => $backend::supportsMedia(),
'visible' => $backend->supportsMedia(),
'permission' => Acl::STATION_MEDIA,
],
'streamers' => [
'label' => __('Streamer/DJ Accounts'),
'icon' => 'mic',
'url' => $router->fromHere('stations:streamers:index'),
'visible' => $backend::supportsStreamers(),
'visible' => $backend->supportsStreamers(),
'permission' => Acl::STATION_STREAMERS,
],
'web_dj' => [
@ -89,7 +89,7 @@ return function (App\Event\BuildStationMenu $e) {
'label' => __('Mount Points'),
'icon' => 'wifi_tethering',
'url' => $router->fromHere('stations:mounts:index'),
'visible' => $frontend::supportsMounts(),
'visible' => $frontend->supportsMounts(),
'permission' => Acl::STATION_MOUNTS,
],
'remotes' => [
@ -116,7 +116,7 @@ return function (App\Event\BuildStationMenu $e) {
'reports_listeners' => [
'label' => __('Listeners'),
'url' => $router->fromHere('stations:reports:listeners'),
'visible' => $frontend::supportsListenerDetail(),
'visible' => $frontend->supportsListenerDetail(),
],
'reports_requests' => [
'label' => __('Song Requests'),
@ -130,22 +130,22 @@ return function (App\Event\BuildStationMenu $e) {
'reports_performance' => [
'label' => __('Song Listener Impact'),
'url' => $router->fromHere('stations:reports:performance'),
'visible' => $backend::supportsMedia(),
'visible' => $backend->supportsMedia(),
],
'reports_duplicates' => [
'label' => __('Duplicate Songs'),
'url' => $router->fromHere('stations:files:index') . '#special:duplicates',
'visible' => $backend::supportsMedia(),
'visible' => $backend->supportsMedia(),
],
'reports_unprocessable' => [
'label' => __('Unprocessable Files'),
'url' => $router->fromHere('stations:files:index') . '#special:unprocessable',
'visible' => $backend::supportsMedia(),
'visible' => $backend->supportsMedia(),
],
'reports_soundexchange' => [
'label' => __('SoundExchange Royalties'),
'url' => $router->fromHere('stations:reports:soundexchange'),
'visible' => $frontend::supportsListenerDetail(),
'visible' => $frontend->supportsListenerDetail(),
],
],
],
@ -162,7 +162,7 @@ return function (App\Event\BuildStationMenu $e) {
'automation' => [
'label' => __('Automated Assignment'),
'url' => $router->fromHere('stations:automation:index'),
'visible' => $backend::supportsMedia(),
'visible' => $backend->supportsMedia(),
'permission' => Acl::STATION_AUTOMATION,
],
'ls_config' => [

View File

@ -36,17 +36,10 @@ class RestartRadioCommand extends CommandAbstract
$io->progressStart(count($stations));
foreach ($stations as $station) {
$configuration->writeConfiguration($station, false, true);
$station->setHasStarted(true);
$station->setNeedsRestart(false);
$em->persist($station);
$configuration->writeConfiguration($station, true);
$io->progressAdvance();
}
$em->flush();
$io->progressFinish();
return 0;
}

View File

@ -19,9 +19,14 @@ class InstallShoutcastController
{
protected array $form_config;
public function __construct(Config $config)
{
protected SHOUTcast $adapter;
public function __construct(
Config $config,
SHOUTcast $adapter
) {
$this->form_config = $config->get('forms/install_shoutcast');
$this->adapter = $adapter;
}
public function __invoke(
@ -31,7 +36,7 @@ class InstallShoutcastController
): ResponseInterface {
$form_config = $this->form_config;
$version = SHOUTcast::getVersion();
$version = $this->adapter->getVersion();
if (null !== $version) {
$form_config['groups'][0]['elements']['current_version'][1]['markup'] = '<p class="text-success">' . __(
@ -58,11 +63,14 @@ class InstallShoutcastController
$import_file->moveTo($sc_tgz_path);
$process = new Process([
'tar',
'xvzf',
$sc_tgz_path,
], $sc_base_dir);
$process = new Process(
[
'tar',
'xvzf',
$sc_tgz_path,
],
$sc_base_dir
);
$process->mustRun();
@ -77,10 +85,14 @@ class InstallShoutcastController
}
}
return $request->getView()->renderToResponse($response, 'system/form_page', [
'form' => $form,
'render_mode' => 'edit',
'title' => __('Install SHOUTcast'),
]);
return $request->getView()->renderToResponse(
$response,
'system/form_page',
[
'form' => $form,
'render_mode' => 'edit',
'title' => __('Install SHOUTcast'),
]
);
}
}

View File

@ -104,7 +104,7 @@ class MountsController extends AbstractStationApiCrudController
$station = parent::getStation($request);
$frontend = $request->getStationFrontend();
if (!$frontend::supportsMounts()) {
if (!$frontend->supportsMounts()) {
throw new StationUnsupportedException();
}

View File

@ -59,7 +59,7 @@ class RequestsController
// Verify that the station supports requests.
$ba = $request->getStationBackend();
if (!$ba::supportsRequests() || !$station->getEnableRequests()) {
if (!$ba->supportsRequests() || !$station->getEnableRequests()) {
return $response->withStatus(403)
->withJson(new Entity\Api\Error(403, __('This station does not accept requests currently.')));
}
@ -166,7 +166,7 @@ class RequestsController
// Verify that the station supports requests.
$ba = $request->getStationBackend();
if (!$ba::supportsRequests() || !$station->getEnableRequests()) {
if (!$ba->supportsRequests() || !$station->getEnableRequests()) {
return $response->withStatus(403)
->withJson(new Entity\Api\Error(403, __('This station does not accept requests currently.')));
}

View File

@ -49,10 +49,12 @@ class ServicesController
$backend = $request->getStationBackend();
$frontend = $request->getStationFrontend();
return $response->withJson(new Entity\Api\StationServiceStatus(
$backend->isRunning($station),
$frontend->isRunning($station)
));
return $response->withJson(
new Entity\Api\StationServiceStatus(
$backend->isRunning($station),
$frontend->isRunning($station)
)
);
}
/**
@ -71,7 +73,7 @@ class ServicesController
public function restartAction(ServerRequest $request, Response $response): ResponseInterface
{
$station = $request->getStation();
$this->configuration->writeConfiguration($station, false, true);
$this->configuration->writeConfiguration($station, true);
return $response->withJson(new Entity\Api\Status(true, __('Station restarted.')));
}

View File

@ -179,7 +179,7 @@ class StreamersController extends AbstractScheduledEntityController
$station = parent::getStation($request);
$backend = $request->getStationBackend();
if (!$backend::supportsStreamers()) {
if (!$backend->supportsStreamers()) {
throw new StationUnsupportedException();
}

View File

@ -23,15 +23,19 @@ class MountsController extends AbstractStationCrudController
$station = $request->getStation();
$frontend = $request->getStationFrontend();
if (!$frontend::supportsMounts()) {
if (!$frontend->supportsMounts()) {
throw new StationUnsupportedException(__('This feature is not currently supported on this station.'));
}
return $request->getView()->renderToResponse($response, 'stations/mounts/index', [
'frontend_type' => $station->getFrontendType(),
'mounts' => $station->getMounts(),
'csrf' => $request->getCsrf()->generate($this->csrf_namespace),
]);
return $request->getView()->renderToResponse(
$response,
'stations/mounts/index',
[
'frontend_type' => $station->getFrontendType(),
'mounts' => $station->getMounts(),
'csrf' => $request->getCsrf()->generate($this->csrf_namespace),
]
);
}
public function editAction(ServerRequest $request, Response $response, $id = null): ResponseInterface
@ -41,11 +45,15 @@ class MountsController extends AbstractStationCrudController
return $response->withRedirect($request->getRouter()->fromHere('stations:mounts:index'));
}
return $request->getView()->renderToResponse($response, 'stations/mounts/edit', [
'form' => $this->form,
'render_mode' => 'edit',
'title' => $id ? __('Edit Mount Point') : __('Add Mount Point'),
]);
return $request->getView()->renderToResponse(
$response,
'stations/mounts/edit',
[
'form' => $this->form,
'render_mode' => 'edit',
'title' => $id ? __('Edit Mount Point') : __('Add Mount Point'),
]
);
}
public function deleteAction(

View File

@ -14,12 +14,16 @@ class PlaylistsController
$station = $request->getStation();
$backend = $request->getStationBackend();
if (!$backend::supportsMedia()) {
if (!$backend->supportsMedia()) {
throw new Exception(__('This feature is not currently supported on this station.'));
}
return $request->getView()->renderToResponse($response, 'stations/playlists/index', [
'station_tz' => $station->getTimezone(),
]);
return $request->getView()->renderToResponse(
$response,
'stations/playlists/index',
[
'station_tz' => $station->getTimezone(),
]
);
}
}

View File

@ -34,7 +34,7 @@ class StreamersController
$station = $request->getStation();
$backend = $request->getStationBackend();
if (!$backend::supportsStreamers()) {
if (!$backend->supportsStreamers()) {
throw new StationUnsupportedException();
}

View File

@ -34,7 +34,7 @@ class StationApiGenerator
$response->listen_url = $fa->getStreamUrl($station, $baseUri);
$mounts = [];
if ($fa::supportsMounts() && $station->getMounts()->count() > 0) {
if ($fa->supportsMounts() && $station->getMounts()->count() > 0) {
foreach ($station->getMounts() as $mount) {
if ($showAllMounts || $mount->isVisibleOnPublicPages()) {
$mounts[] = $mount->api($fa, $baseUri);

View File

@ -173,9 +173,9 @@ class StationRepository extends Repository
}
// Create default mountpoints if station supports them.
if ($frontend_adapter::supportsMounts()) {
if ($frontend_adapter->supportsMounts()) {
// Create default mount points.
$mount_points = $frontend_adapter::getDefaultMounts();
$mount_points = $frontend_adapter->getDefaultMounts();
foreach ($mount_points as $mount_point) {
$mount_record = new Entity\StationMount($station);
@ -231,15 +231,8 @@ class StationRepository extends Repository
*/
public function create(Entity\Station $station): Entity\Station
{
// Create path for station.
$station->ensureDirectoriesExist();
$this->em->persist($station);
$this->em->persist($station->getMediaStorageLocation());
$this->em->persist($station->getRecordingsStorageLocation());
// Generate station ID.
$this->em->flush();
$station->generateAdapterApiKey();
$this->configuration->initializeConfiguration($station);
// Scan directory for any existing files.
set_time_limit(600);
@ -253,22 +246,10 @@ class StationRepository extends Repository
/** @var Entity\Station $station */
$station = $this->em->find(Entity\Station::class, $station->getId());
// Load adapters.
$frontend_adapter = $this->adapters->getFrontendAdapter($station);
// Create default mountpoints if station supports them.
$frontend_adapter = $this->adapters->getFrontendAdapter($station);
$this->resetMounts($station, $frontend_adapter);
// Load configuration from adapter to pull source and admin PWs.
$frontend_adapter->read($station);
// Write the adapter configurations and update supervisord.
$this->configuration->writeConfiguration($station, true);
// Save changes and continue to the last setup step.
$this->em->persist($station);
$this->em->flush();
return $station;
}

View File

@ -488,31 +488,7 @@ class Station
$frontend_config = $config;
}
$config = $frontend_config->toArray();
if ($this->frontend_config != $config) {
$this->setNeedsRestart(true);
}
$this->frontend_config = $config;
}
/**
* Set frontend configuration but do not overwrite existing values.
*
* @param array $default_config
*/
public function setFrontendConfigDefaults(array $default_config): void
{
$frontend_config = (array)$this->frontend_config;
foreach ($default_config as $config_key => $config_value) {
if (empty($frontend_config[$config_key])) {
$frontend_config[$config_key] = $config_value;
}
}
$this->frontend_config = $frontend_config;
$this->frontend_config = $frontend_config->toArray();
}
public function getBackendType(): ?string
@ -572,13 +548,7 @@ class Station
$backend_config = $config;
}
$config = $backend_config->toArray();
if ($this->backend_config != $config) {
$this->setNeedsRestart(true);
}
$this->backend_config = $config;
$this->backend_config = $backend_config->toArray();
}
public function getAdapterApiKey(): ?string

View File

@ -2,10 +2,22 @@
namespace App\Entity;
use App\Utilities\Strings;
use Doctrine\Common\Collections\ArrayCollection;
class StationFrontendConfiguration extends ArrayCollection
{
public function __construct(array $elements = [])
{
// Generate defaults if not set.
$elements[self::SOURCE_PASSWORD] ??= Strings::generatePassword();
$elements[self::ADMIN_PASSWORD] ??= Strings::generatePassword();
$elements[self::RELAY_PASSWORD] ??= Strings::generatePassword();
$elements[self::STREAMER_PASSWORD] ??= Strings::generatePassword();
parent::__construct($elements);
}
public const CUSTOM_CONFIGURATION = 'custom_config';
public function getCustomConfiguration(): ?string
@ -20,48 +32,48 @@ class StationFrontendConfiguration extends ArrayCollection
public const SOURCE_PASSWORD = 'source_pw';
public function getSourcePassword(): ?string
public function getSourcePassword(): string
{
return $this->get(self::SOURCE_PASSWORD);
}
public function setSourcePassword(?string $pw): void
public function setSourcePassword(string $pw): void
{
$this->set(self::SOURCE_PASSWORD, $pw);
}
public const ADMIN_PASSWORD = 'admin_pw';
public function getAdminPassword(): ?string
public function getAdminPassword(): string
{
return $this->get(self::ADMIN_PASSWORD);
}
public function setAdminPassword(?string $pw): void
public function setAdminPassword(string $pw): void
{
$this->set(self::ADMIN_PASSWORD, $pw);
}
public const RELAY_PASSWORD = 'relay_pw';
public function getRelayPassword(): ?string
public function getRelayPassword(): string
{
return $this->get(self::RELAY_PASSWORD);
}
public function setRelayPassword(?string $pw): void
public function setRelayPassword(string $pw): void
{
$this->set(self::RELAY_PASSWORD, $pw);
}
public const STREAMER_PASSWORD = 'streamer_pw';
public function getStreamerPassword(): ?string
public function getStreamerPassword(): string
{
return $this->get(self::STREAMER_PASSWORD);
}
public function setStreamerPassword(?string $pw): void
public function setStreamerPassword(string $pw): void
{
$this->set(self::STREAMER_PASSWORD, $pw);
}

View File

@ -7,6 +7,7 @@ use App\Entity;
use App\Environment;
use App\Flysystem\FilesystemManager;
use App\Http\ServerRequest;
use App\Radio\Adapters;
use App\Radio\Configuration;
use App\Sync\Task\CheckMediaTask;
use DeepCopy;
@ -30,10 +31,11 @@ class StationCloneForm extends StationForm
ValidatorInterface $validator,
Entity\Repository\StationRepository $station_repo,
Entity\Repository\StorageLocationRepository $storageLocationRepo,
Configuration $configuration,
CheckMediaTask $media_sync,
Config $config,
Environment $environment,
Adapters $adapters,
Configuration $configuration,
CheckMediaTask $media_sync,
FilesystemManager $filesystem
) {
parent::__construct(
@ -43,7 +45,8 @@ class StationCloneForm extends StationForm
$station_repo,
$storageLocationRepo,
$config,
$environment
$environment,
$adapters
);
$form_config = $config->get('forms/station_clone');

View File

@ -7,7 +7,7 @@ use App\Config;
use App\Entity;
use App\Environment;
use App\Http\ServerRequest;
use App\Radio\Frontend\SHOUTcast;
use App\Radio\Adapters;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Validator\ConstraintViolation;
@ -21,6 +21,8 @@ class StationForm extends EntityForm
protected Environment $environment;
protected Adapters $adapters;
public function __construct(
EntityManagerInterface $em,
Serializer $serializer,
@ -28,14 +30,21 @@ class StationForm extends EntityForm
Entity\Repository\StationRepository $station_repo,
Entity\Repository\StorageLocationRepository $storageLocationRepo,
Config $config,
Environment $environment
Environment $environment,
Adapters $adapters
) {
$this->entityClass = Entity\Station::class;
$this->station_repo = $station_repo;
$this->storageLocationRepo = $storageLocationRepo;
$this->environment = $environment;
$this->adapters = $adapters;
$form_config = $config->get('forms/station');
$form_config = $config->get(
'forms/station',
[
'adapters' => $adapters,
]
);
parent::__construct($em, $serializer, $validator, $form_config);
}
@ -73,7 +82,8 @@ class StationForm extends EntityForm
unset($this->options['groups']['admin']);
}
if (!SHOUTcast::isInstalled()) {
$installedFrontends = $this->adapters->listFrontendAdapters(true);
if (!isset($installedFrontends[Adapters::FRONTEND_SHOUTCAST])) {
$frontendDesc = __(
'Want to use SHOUTcast 2? <a href="%s" target="_blank">Install it here</a>, then reload this page.',
$request->getRouter()->named('admin:install_shoutcast:index')

View File

@ -15,7 +15,7 @@ class StationFiles
public function __invoke(ServerRequest $request, RequestHandlerInterface $handler): ResponseInterface
{
$backend = $request->getStationBackend();
if (!$backend::supportsMedia()) {
if (!$backend->supportsMedia()) {
throw new Exception(__('This feature is not currently supported on this station.'));
}

View File

@ -18,12 +18,12 @@ use Supervisor\Supervisor;
abstract class AbstractAdapter
{
protected Supervisor $supervisor;
protected Environment $environment;
protected EntityManagerInterface $em;
protected Supervisor $supervisor;
protected EventDispatcher $dispatcher;
protected LoggerInterface $logger;
@ -42,31 +42,84 @@ abstract class AbstractAdapter
$this->logger = $logger;
}
/**
* Write configuration from Station object to the external service.
*
* @param Entity\Station $station
*
* @return bool Whether the newly written configuration differs from what was already on disk.
*/
public function write(Entity\Station $station): bool
{
return $this->compareConfiguration($station, true);
}
/**
* Generate the configuration for this adapter as it would exist with current database settings.
*
* @param Entity\Station $station
*
*/
public function getCurrentConfiguration(Entity\Station $station): ?string
{
return null;
}
/**
* Check whether the configuration currently generated differs from what is currently stored on disk.
*
* @param Entity\Station $station
* @param bool $writeNewConfig
*
* @return bool Whether configuration generated now differs from what is on disk.
*/
public function compareConfiguration(Entity\Station $station, bool $writeNewConfig = false): bool
{
$configPath = $this->getConfigurationPath($station);
if (null === $configPath) {
return false;
}
$currentConfig = (file_exists($configPath))
? file_get_contents($configPath)
: null;
$newConfig = $this->getCurrentConfiguration($station);
if ($writeNewConfig) {
file_put_contents($configPath, $newConfig);
}
return 0 !== strcmp($currentConfig, $newConfig);
}
/**
* Returns the main path where configuration data is stored for this adapter.
*
*/
public function getConfigurationPath(Entity\Station $station): ?string
{
return null;
}
/**
* Indicate if the adapter in question is installed on the server.
*/
public static function isInstalled(): bool
public function isInstalled(): bool
{
return (static::getBinary() !== false);
return (null !== $this->getBinary());
}
/**
* Return the binary executable location for this item.
*
* @return string|bool Returns either the path to the binary if it exists or a boolean for error/success
* @return string|null Returns either the path to the binary if it exists or null for no binary.
*/
public static function getBinary()
public function getBinary(): ?string
{
return true;
return null;
}
/**
* Write configuration from Station object to the external service.
*
* @param Entity\Station $station
*/
abstract public function write(Entity\Station $station): bool;
/**
* Check if the service is running.
*
@ -74,16 +127,16 @@ abstract class AbstractAdapter
*/
public function isRunning(Entity\Station $station): bool
{
if ($this->hasCommand($station)) {
$program_name = $this->getProgramName($station);
$process = $this->supervisor->getProcess($program_name);
if ($process instanceof Process) {
return $process->isRunning();
}
if (!$this->hasCommand($station)) {
return true;
}
return false;
$program_name = $this->getProgramName($station);
$process = $this->supervisor->getProcess($program_name);
return ($process instanceof Process)
? $process->isRunning()
: false;
}
/**
@ -117,14 +170,29 @@ abstract class AbstractAdapter
*/
abstract public function getProgramName(Entity\Station $station): string;
public function supportsSoftReload(): bool
{
return false;
}
/**
* Soft-reloads an adapter, if it's supported by the adapter.
*
* @param Entity\Station $station
*/
public function reload(Entity\Station $station): void
{
if (!$this->supportsSoftReload()) {
$this->restart($station);
}
throw new \RuntimeException('Feature not implemented!');
}
/**
* Restart the executable service.
*
* @param Entity\Station $station
*
* @throws SupervisorException
* @throws AlreadyRunningException
* @throws NotRunningException
*/
public function restart(Entity\Station $station): void
{
@ -196,7 +264,7 @@ abstract class AbstractAdapter
*/
protected function handleSupervisorException(
SupervisorLibException $e,
$program_name,
string $program_name,
Entity\Station $station
): void {
$class_parts = explode('\\', static::class);

View File

@ -40,7 +40,7 @@ class Adapters
*/
public function getFrontendAdapter(Entity\Station $station): Frontend\AbstractFrontend
{
$adapters = self::listFrontendAdapters();
$adapters = $this->listFrontendAdapters();
$frontend_type = $station->getFrontendType();
@ -62,33 +62,32 @@ class Adapters
*
* @return mixed[]
*/
public static function listFrontendAdapters($check_installed = false): array
public function listFrontendAdapters(bool $check_installed = false): array
{
static $adapters;
if ($adapters === null) {
$adapters = [
self::FRONTEND_ICECAST => [
'name' => __('Use <b>%s</b> on this server', 'Icecast 2.4'),
'class' => Frontend\Icecast::class,
],
self::FRONTEND_SHOUTCAST => [
'name' => __('Use <b>%s</b> on this server', 'SHOUTcast DNAS 2'),
'class' => Frontend\SHOUTcast::class,
],
self::FRONTEND_REMOTE => [
'name' => __('Connect to a <b>remote radio server</b>'),
'class' => Frontend\Remote::class,
],
];
}
$adapters = [
self::FRONTEND_ICECAST => [
'name' => __('Use <b>%s</b> on this server', 'Icecast 2.4'),
'class' => Frontend\Icecast::class,
],
self::FRONTEND_SHOUTCAST => [
'name' => __('Use <b>%s</b> on this server', 'SHOUTcast DNAS 2'),
'class' => Frontend\SHOUTcast::class,
],
self::FRONTEND_REMOTE => [
'name' => __('Connect to a <b>remote radio server</b>'),
'class' => Frontend\Remote::class,
],
];
if ($check_installed) {
return array_filter($adapters, function ($adapter_info) {
/** @var AbstractAdapter $adapter_class */
$adapter_class = $adapter_info['class'];
return $adapter_class::isInstalled();
});
return array_filter(
$adapters,
function ($adapter_info) {
/** @var AbstractAdapter $adapter */
$adapter = $this->adapters->get($adapter_info['class']);
return $adapter->isInstalled();
}
);
}
return $adapters;
@ -101,7 +100,7 @@ class Adapters
*/
public function getBackendAdapter(Entity\Station $station): Backend\AbstractBackend
{
$adapters = self::listBackendAdapters();
$adapters = $this->listBackendAdapters();
$backend_type = $station->getBackendType();
@ -123,29 +122,28 @@ class Adapters
*
* @return mixed[]
*/
public static function listBackendAdapters($check_installed = false): array
public function listBackendAdapters(bool $check_installed = false): array
{
static $adapters;
if ($adapters === null) {
$adapters = [
self::BACKEND_LIQUIDSOAP => [
'name' => __('Use <b>%s</b> on this server', 'Liquidsoap'),
'class' => Backend\Liquidsoap::class,
],
self::BACKEND_NONE => [
'name' => __('<b>Do not use</b> an AutoDJ service'),
'class' => Backend\None::class,
],
];
}
$adapters = [
self::BACKEND_LIQUIDSOAP => [
'name' => __('Use <b>%s</b> on this server', 'Liquidsoap'),
'class' => Backend\Liquidsoap::class,
],
self::BACKEND_NONE => [
'name' => __('<b>Do not use</b> an AutoDJ service'),
'class' => Backend\None::class,
],
];
if ($check_installed) {
return array_filter($adapters, function ($adapter_info) {
/** @var AbstractAdapter $adapter_class */
$adapter_class = $adapter_info['class'];
return $adapter_class::isInstalled();
});
return array_filter(
$adapters,
function ($adapter_info) {
/** @var AbstractAdapter $adapter */
$adapter = $this->adapters->get($adapter_info['class']);
return $adapter->isInstalled();
}
);
}
return $adapters;
@ -178,7 +176,7 @@ class Adapters
*/
public function getRemoteAdapter(Entity\Station $station, Entity\StationRemote $remote): Remote\AbstractRemote
{
$adapters = self::listRemoteAdapters();
$adapters = $this->listRemoteAdapters();
$remote_type = $remote->getType();
@ -198,7 +196,7 @@ class Adapters
/**
* @return mixed[]
*/
public static function listRemoteAdapters(): array
public function listRemoteAdapters(): array
{
return [
self::REMOTE_SHOUTCAST1 => [

View File

@ -7,24 +7,24 @@ use App\Radio\AbstractAdapter;
abstract class AbstractBackend extends AbstractAdapter
{
public static function supportsMedia(): bool
public function supportsMedia(): bool
{
return true;
return false;
}
public static function supportsRequests(): bool
public function supportsRequests(): bool
{
return true;
return false;
}
public static function supportsStreamers(): bool
public function supportsStreamers(): bool
{
return true;
return false;
}
public static function supportsWebStreaming(): bool
public function supportsWebStreaming(): bool
{
return true;
return false;
}
public function getStreamPort(Entity\Station $station): ?int
@ -33,6 +33,8 @@ abstract class AbstractBackend extends AbstractAdapter
}
/**
* @param Entity\StationMedia $media
*
* @return mixed[]
*/
public function annotateMedia(Entity\StationMedia $media): array

View File

@ -30,30 +30,50 @@ class Liquidsoap extends AbstractBackend
$this->streamerRepo = $streamerRepo;
}
/**
* Write configuration from Station object to the external service.
*
* Special thanks to the team of PonyvilleFM for assisting with Liquidsoap configuration and debugging.
*
* @param Entity\Station $station
*/
public function write(Entity\Station $station): bool
public function supportsMedia(): bool
{
$event = new WriteLiquidsoapConfiguration($station);
$this->dispatcher->dispatch($event);
$ls_config_contents = $event->buildConfiguration();
$config_path = $station->getRadioConfigDir();
$ls_config_path = $config_path . '/liquidsoap.liq';
file_put_contents($ls_config_path, $ls_config_contents);
return true;
}
public function supportsRequests(): bool
{
return true;
}
public function supportsStreamers(): bool
{
return true;
}
public function supportsWebStreaming(): bool
{
return true;
}
/**
* @inheritDoc
*/
public function getConfigurationPath(Entity\Station $station): ?string
{
return $station->getRadioConfigDir() . '/liquidsoap.liq';
}
/**
* @inheritDoc
*/
public function getCurrentConfiguration(Entity\Station $station): ?string
{
return $this->doGetConfiguration($station, false);
}
public function getEditableConfiguration(Entity\Station $station): string
{
$event = new WriteLiquidsoapConfiguration($station, true);
return $this->doGetConfiguration($station, true);
}
protected function doGetConfiguration(Entity\Station $station, bool $forEditing = false): string
{
$event = new WriteLiquidsoapConfiguration($station, $forEditing);
$this->dispatcher->dispatch($event);
return $event->buildConfiguration();
@ -208,18 +228,18 @@ class Liquidsoap extends AbstractBackend
*/
public function getCommand(Entity\Station $station): ?string
{
if ($binary = self::getBinary()) {
if ($binary = $this->getBinary()) {
$config_path = $station->getRadioConfigDir() . '/liquidsoap.liq';
return $binary . ' ' . $config_path;
}
return '/bin/false';
return null;
}
/**
* @inheritDoc
*/
public static function getBinary()
public function getBinary(): ?string
{
// Docker revisions 3 and later use the `radio` container.
$environment = Environment::getInstance();

View File

@ -6,36 +6,6 @@ use App\Entity;
class None extends AbstractBackend
{
public static function supportsMedia(): bool
{
return false;
}
public static function supportsStreamers(): bool
{
return false;
}
public static function supportsWebStreaming(): bool
{
return false;
}
public static function supportsRequests(): bool
{
return false;
}
public function write(Entity\Station $station): bool
{
return true;
}
public function isRunning(Entity\Station $station): bool
{
return true;
}
public function start(Entity\Station $station): void
{
$this->logger->error(

View File

@ -39,59 +39,95 @@ class Configuration
$this->environment = $environment;
}
/**
* Write all configuration changes to the filesystem and reload supervisord.
*
* @param Station $station
* @param bool $regen_auth_key
* @param bool $force_restart Always restart this station's supervisor instances, even if nothing changed.
*/
public function writeConfiguration(Station $station, $regen_auth_key = false, $force_restart = false): void
public function initializeConfiguration(Station $station): void
{
// Ensure default values for frontend/backend config exist.
$station->setFrontendConfig($station->getFrontendConfig());
$station->setBackendConfig($station->getBackendConfig());
// Ensure port configuration exists
$this->assignRadioPorts($station);
// Clear station caches and generate API adapter key if none exists.
if (empty($station->getAdapterApiKey())) {
$station->generateAdapterApiKey();
}
// Ensure all directories exist.
$station->ensureDirectoriesExist();
$this->em->persist($station);
$this->em->persist($station->getMediaStorageLocation());
$this->em->persist($station->getRecordingsStorageLocation());
$this->em->flush();
}
public function handleConfigurationChange(Station $station): void
{
if ($this->environment->isTesting()) {
return;
}
// Initialize adapters.
$supervisor_config = [];
$supervisor_config_path = $this->getSupervisorConfigFile($station);
$this->initializeConfiguration($station);
if (!$station->isEnabled()) {
@unlink($supervisor_config_path);
$this->reloadSupervisorForStation($station, false);
if (!$station->isEnabled() || !$station->getHasStarted()) {
return;
}
// Ensure port configuration exists
$this->assignRadioPorts($station, false);
$frontend = $this->adapters->getFrontendAdapter($station);
$backend = $this->adapters->getBackendAdapter($station);
// Clear station caches and generate API adapter key if none exists.
if ($regen_auth_key || empty($station->getAdapterApiKey())) {
$station->generateAdapterApiKey();
if (!$frontend->hasCommand($station) && !$backend->hasCommand($station)) {
return;
}
$station->clearCache();
$frontendChanged = $frontend->compareConfiguration($station);
$backendChanged = $backend->compareConfiguration($station);
$this->em->persist($station);
$this->em->flush();
if ($frontendChanged || $backendChanged) {
$station->setNeedsRestart(true);
$this->em->persist($station);
$this->em->flush();
}
}
/**
* Write all configuration changes to the filesystem and reload supervisord.
*
* @param Station $station
* @param bool $forceRestart Always restart this station's supervisor instances, even if nothing changed.
*/
public function writeConfiguration(
Station $station,
bool $forceRestart = false
): void {
if ($this->environment->isTesting()) {
return;
}
$this->initializeConfiguration($station);
// Initialize adapters.
$supervisorConfig = [];
$supervisorConfigFile = $this->getSupervisorConfigFile($station);
if (!$station->isEnabled()) {
@unlink($supervisorConfigFile);
$this->reloadSupervisorForStation($station);
return;
}
$frontend = $this->adapters->getFrontendAdapter($station);
$backend = $this->adapters->getBackendAdapter($station);
// If no processes need to be managed, remove any existing config.
if (!$frontend->hasCommand($station) && !$backend->hasCommand($station)) {
@unlink($supervisor_config_path);
$this->reloadSupervisorForStation($station, false);
@unlink($supervisorConfigFile);
$this->reloadSupervisorForStation($station);
return;
}
// Ensure all directories exist.
$station->ensureDirectoriesExist();
// Write config files for both backend and frontend.
$frontend->write($station);
$backend->write($station);
// Get group information
$backend_name = $backend->getProgramName($station);
[$backend_group, $backend_program] = explode(':', $backend_name);
@ -108,25 +144,29 @@ class Configuration
$programs[] = $frontend_program;
}
$supervisor_config[] = '[group:' . $backend_group . ']';
$supervisor_config[] = 'programs=' . implode(',', $programs);
$supervisor_config[] = '';
$supervisorConfig[] = '[group:' . $backend_group . ']';
$supervisorConfig[] = 'programs=' . implode(',', $programs);
$supervisorConfig[] = '';
// Write frontend
if ($frontend->hasCommand($station)) {
$supervisor_config[] = $this->writeConfigurationSection($station, $frontend, 90);
$supervisorConfig[] = $this->writeConfigurationSection($station, $frontend, 90);
}
// Write backend
if ($backend->hasCommand($station)) {
$supervisor_config[] = $this->writeConfigurationSection($station, $backend, 100);
$supervisorConfig[] = $this->writeConfigurationSection($station, $backend, 100);
}
// Write config contents
$supervisor_config_data = implode("\n", $supervisor_config);
file_put_contents($supervisor_config_path, $supervisor_config_data);
$supervisor_config_data = implode("\n", $supervisorConfig);
file_put_contents($supervisorConfigFile, $supervisor_config_data);
$this->reloadSupervisorForStation($station, $force_restart);
// Write supporting configurations.
$frontend->write($station);
$backend->write($station);
$this->reloadSupervisorForStation($station, $forceRestart);
}
/**
@ -145,7 +185,7 @@ class Configuration
* @param Station $station
* @param bool $force_restart
*/
protected function reloadSupervisorForStation(Station $station, $force_restart = false): void
protected function reloadSupervisorForStation(Station $station, $force_restart = false): bool
{
$station_group = 'station_' . $station->getId();
$affected_groups = $this->reloadSupervisor();
@ -165,10 +205,13 @@ class Configuration
if ($was_restarted) {
$station->setHasStarted(true);
$station->setNeedsRestart(false);
$station->clearCache();
$this->em->persist($station);
$this->em->flush();
}
return $was_restarted;
}
/**

View File

@ -54,12 +54,28 @@ abstract class AbstractFrontend extends AbstractAdapter
$this->settings = $settingsRepo->readSettings();
}
/**
* @return bool Whether the station supports multiple mount points per station
*/
public function supportsMounts(): bool
{
return false;
}
/**
* @return bool Whether the station supports enhanced listener detail (per-client records)
*/
public function supportsListenerDetail(): bool
{
return false;
}
/**
* Get the default mounts when resetting or initializing a station.
*
* @return mixed[]
*/
public static function getDefaultMounts(): array
public function getDefaultMounts(): array
{
return [
[
@ -72,29 +88,6 @@ abstract class AbstractFrontend extends AbstractAdapter
];
}
/**
* @return bool Whether the station supports multiple mount points per station
*/
public static function supportsMounts(): bool
{
return true;
}
/**
* @return bool Whether the station supports enhanced listener detail (per-client records)
*/
public static function supportsListenerDetail(): bool
{
return true;
}
/**
* Read configuration from external service to Station object.
*
* @param Entity\Station $station
*/
abstract public function read(Entity\Station $station): bool;
/**
* @inheritdoc
*/
@ -156,8 +149,8 @@ abstract class AbstractFrontend extends AbstractAdapter
if (
$use_radio_proxy
|| (!$this->environment->isProduction() && !$this->environment->isDocker())
|| 'https' === $base_url->getScheme()
|| (!$this->environment->isProduction() && !$this->environment->isDocker())
) {
// Web proxy support.
return $base_url
@ -194,9 +187,11 @@ abstract class AbstractFrontend extends AbstractAdapter
}
/**
* @param string|null $custom_config_raw
*
* @return mixed[]|bool
*/
protected function processCustomConfig($custom_config_raw)
protected function processCustomConfig(?string $custom_config_raw)
{
$custom_config = [];
@ -210,11 +205,6 @@ abstract class AbstractFrontend extends AbstractAdapter
return $custom_config;
}
protected function getRadioPort(Entity\Station $station): int
{
return (8000 + (($station->getId() - 1) * 10));
}
protected function writeIpBansFile(Entity\Station $station): string
{
$ips = [];

View File

@ -3,10 +3,8 @@
namespace App\Radio\Frontend;
use App\Entity;
use App\Environment;
use App\Radio\CertificateLocator;
use App\Utilities;
use App\Xml\Reader;
use App\Xml\Writer;
use Exception;
use GuzzleHttp\Psr7\Uri;
@ -21,6 +19,27 @@ class Icecast extends AbstractFrontend
public const LOGLEVEL_WARN = 2;
public const LOGLEVEL_ERROR = 1;
public function supportsMounts(): bool
{
return true;
}
public function supportsListenerDetail(): bool
{
return true;
}
public function supportsSoftReload(): bool
{
return true;
}
public function reload(Entity\Station $station): void
{
$this->write($station);
// TODO: Implement soft-reload functionality.
}
public function getNowPlaying(Entity\Station $station, bool $includeClients = true): Result
{
$feConfig = $station->getFrontendConfig();
@ -66,43 +85,15 @@ class Icecast extends AbstractFrontend
return $defaultResult;
}
public function read(Entity\Station $station): bool
public function getConfigurationPath(Entity\Station $station): ?string
{
$config = $this->getConfig($station);
$station->setFrontendConfigDefaults($this->loadFromConfig($station, $config));
return true;
return $station->getRadioConfigDir() . '/icecast.xml';
}
/**
* @return mixed[]
*/
protected function getConfig(Entity\Station $station): array
public function getCurrentConfiguration(Entity\Station $station): ?string
{
$config_path = $station->getRadioConfigDir();
$icecast_path = $config_path . '/icecast.xml';
$defaults = $this->getDefaults($station);
if (file_exists($icecast_path)) {
$reader = new Reader();
$data = $reader->fromFile($icecast_path);
return self::arrayMergeRecursiveDistinct($defaults, $data);
}
return $defaults;
}
/*
* Process Management
*/
/**
* @return mixed[]
*/
protected function getDefaults(Entity\Station $station): array
{
$config_dir = $station->getRadioConfigDir();
$frontendConfig = $station->getFrontendConfig();
$configDir = $station->getRadioConfigDir();
$settingsBaseUrl = $this->settings->getBaseUrl() ?: 'http://localhost';
if (strpos($settingsBaseUrl, 'http') !== 0) {
@ -112,40 +103,38 @@ class Icecast extends AbstractFrontend
$certPaths = CertificateLocator::findCertificate();
$defaults = [
$config = [
'location' => 'AzuraCast',
'admin' => 'icemaster@localhost',
'hostname' => $baseUrl->getHost(),
'limits' => [
'clients' => 2500,
'clients' => $frontendConfig->getMaxListeners() ?? 2500,
'sources' => $station->getMounts()->count(),
// 'threadpool' => 5,
'queue-size' => 524288,
'client-timeout' => 30,
'header-timeout' => 15,
'source-timeout' => 10,
// 'burst-on-connect' => 1,
'burst-size' => 65535,
],
'authentication' => [
'source-password' => Utilities\Strings::generatePassword(),
'relay-password' => Utilities\Strings::generatePassword(),
'source-password' => $frontendConfig->getSourcePassword(),
'relay-password' => $frontendConfig->getRelayPassword(),
'admin-user' => 'admin',
'admin-password' => Utilities\Strings::generatePassword(),
'admin-password' => $frontendConfig->getAdminPassword(),
],
'listen-socket' => [
'port' => $this->getRadioPort($station),
'port' => $frontendConfig->getPort(),
],
'mount' => [],
'fileserve' => 1,
'paths' => [
'basedir' => '/usr/local/share/icecast',
'logdir' => $config_dir,
'logdir' => $configDir,
'webroot' => '/usr/local/share/icecast/web',
'adminroot' => '/usr/local/share/icecast/admin',
'pidfile' => $config_dir . '/icecast.pid',
'pidfile' => $configDir . '/icecast.pid',
'alias' => [
'@source' => '/',
'@dest' => '/status.xsl',
@ -196,14 +185,14 @@ class Icecast extends AbstractFrontend
$mount_conf = $this->processCustomConfig($mount_row->getFrontendConfig());
if (!empty($mount_conf)) {
$mount = self::arrayMergeRecursiveDistinct($mount, $mount_conf);
$mount = Utilities\Arrays::arrayMergeRecursiveDistinct($mount, $mount_conf);
}
}
if ($mount_row->getRelayUrl()) {
$relay_parts = parse_url($mount_row->getRelayUrl());
$defaults['relay'][] = [
$config['relay'][] = [
'server' => $relay_parts['host'],
'port' => $relay_parts['port'],
'mount' => $relay_parts['path'],
@ -211,160 +200,40 @@ class Icecast extends AbstractFrontend
];
}
$defaults['mount'][] = $mount;
$config['mount'][] = $mount;
}
return $defaults;
}
/**
* array_merge_recursive does indeed merge arrays, but it converts values with duplicate
* keys to arrays rather than overwriting the value in the first array with the duplicate
* value in the second array, as array_merge does. I.e., with array_merge_recursive,
* this happens (documented behavior):
*
* array_merge_recursive(array('key' => 'org value'), array('key' => 'new value'));
* => array('key' => array('org value', 'new value'));
*
* array_merge_recursive_distinct does not change the datatypes of the values in the arrays.
* Matching keys' values in the second array overwrite those in the first array, as is the
* case with array_merge, i.e.:
*
* array_merge_recursive_distinct(array('key' => 'org value'), array('key' => 'new value'));
* => array('key' => array('new value'));
*
* Parameters are passed by reference, though only for performance reasons. They're not
* altered by this function.
*
* @param array $array1
* @param array $array2
*
* @return mixed[]
*
* @author Daniel <daniel (at) danielsmedegaardbuus (dot) dk>
* @author Gabriel Sobrinho <gabriel (dot) sobrinho (at) gmail (dot) com>
* @noinspection PhpParameterByRefIsNotUsedAsReferenceInspection
*/
public static function arrayMergeRecursiveDistinct(array &$array1, array &$array2): array
{
$merged = $array1;
foreach ($array2 as $key => &$value) {
if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) {
$merged[$key] = self::arrayMergeRecursiveDistinct($merged[$key], $value);
} else {
$merged[$key] = $value;
}
}
return $merged;
}
/*
* Configuration
*/
/**
* @return mixed[]
*/
protected function loadFromConfig(Entity\Station $station, $config): array
{
$frontend_config = $station->getFrontendConfig();
return [
Entity\StationFrontendConfiguration::CUSTOM_CONFIGURATION => $frontend_config->getCustomConfiguration(),
Entity\StationFrontendConfiguration::SOURCE_PASSWORD => $config['authentication']['source-password'],
Entity\StationFrontendConfiguration::ADMIN_PASSWORD => $config['authentication']['admin-password'],
Entity\StationFrontendConfiguration::RELAY_PASSWORD => $config['authentication']['relay-password'],
Entity\StationFrontendConfiguration::STREAMER_PASSWORD => $config['mount'][0]['password'],
Entity\StationFrontendConfiguration::MAX_LISTENERS => $config['limits']['clients'],
];
}
public function write(Entity\Station $station): bool
{
$config = $this->getDefaults($station);
$frontend_config = $station->getFrontendConfig();
$port = $frontend_config->getPort();
if (null !== $port) {
$config['listen-socket']['port'] = $port;
}
$sourcePw = $frontend_config->getSourcePassword();
if (!empty($sourcePw)) {
$config['authentication']['source-password'] = $sourcePw;
}
$adminPw = $frontend_config->getAdminPassword();
if (!empty($adminPw)) {
$config['authentication']['admin-password'] = $adminPw;
}
$relayPw = $frontend_config->getRelayPassword();
if (!empty($relayPw)) {
$config['authentication']['relay-password'] = $relayPw;
}
$streamerPw = $frontend_config->getStreamerPassword();
if (!empty($streamerPw)) {
foreach ($config['mount'] as &$mount) {
if (!empty($mount['password'])) {
$mount['password'] = $streamerPw;
}
}
}
$maxListeners = $frontend_config->getMaxListeners();
if (null !== $maxListeners) {
$config['limits']['clients'] = $maxListeners;
}
$customConfig = $frontend_config->getCustomConfiguration();
$customConfig = $frontendConfig->getCustomConfiguration();
if (!empty($customConfig)) {
$custom_conf = $this->processCustomConfig($customConfig);
if (!empty($custom_conf)) {
$config = self::arrayMergeRecursiveDistinct($config, $custom_conf);
$config = Utilities\Arrays::arrayMergeRecursiveDistinct($config, $custom_conf);
}
}
// Set any unset values back to the DB config.
$station->setFrontendConfigDefaults($this->loadFromConfig($station, $config));
$this->em->persist($station);
$this->em->flush();
$config_path = $station->getRadioConfigDir();
$icecast_path = $config_path . '/icecast.xml';
$writer = new Writer();
$icecast_config_str = $writer->toString($config, 'icecast');
$configString = (new Writer())->toString($config, 'icecast');
// Strip the first line (the XML charset)
$icecast_config_str = substr($icecast_config_str, strpos($icecast_config_str, "\n") + 1);
file_put_contents($icecast_path, $icecast_config_str);
return true;
return substr($configString, strpos($configString, "\n") + 1);
}
public function getCommand(Entity\Station $station): ?string
{
if ($binary = self::getBinary()) {
$config_path = $station->getRadioConfigDir() . '/icecast.xml';
return $binary . ' -c ' . $config_path;
if ($binary = $this->getBinary()) {
return $binary . ' -c ' . $this->getConfigurationPath($station);
}
return '/bin/false';
return null;
}
/**
* @inheritDoc
*/
public static function getBinary()
public function getBinary(): ?string
{
$new_path = '/usr/local/bin/icecast';
$legacy_path = '/usr/bin/icecast2';
if (Environment::getInstance()->isDocker() || file_exists($new_path)) {
if ($this->environment->isDocker() || file_exists($new_path)) {
return $new_path;
}
@ -372,7 +241,7 @@ class Icecast extends AbstractFrontend
return $legacy_path;
}
return false;
return null;
}
public function getAdminUrl(Entity\Station $station, UriInterface $base_url = null): UriInterface

View File

@ -8,31 +8,6 @@ use Psr\Http\Message\UriInterface;
class Remote extends AbstractFrontend
{
public static function supportsMounts(): bool
{
return false;
}
public static function supportsListenerDetail(): bool
{
return false;
}
public function read(Entity\Station $station): bool
{
return true;
}
public function write(Entity\Station $station): bool
{
return true;
}
public function isRunning(Entity\Station $station): bool
{
return true;
}
public function getStreamUrl(Entity\Station $station, UriInterface $base_url = null): UriInterface
{
return new Uri('');

View File

@ -3,8 +3,6 @@
namespace App\Radio\Frontend;
use App\Entity;
use App\Environment;
use App\Utilities;
use Exception;
use NowPlaying\Adapter\AdapterFactory;
use NowPlaying\Result\Result;
@ -13,9 +11,19 @@ use Symfony\Component\Process\Process;
class SHOUTcast extends AbstractFrontend
{
public static function getVersion(): ?string
public function supportsMounts(): bool
{
$binary_path = self::getBinary();
return true;
}
public function supportsListenerDetail(): bool
{
return true;
}
public function getVersion(): ?string
{
$binary_path = $this->getBinary();
if (!$binary_path) {
return null;
}
@ -34,13 +42,12 @@ class SHOUTcast extends AbstractFrontend
/**
* @inheritDoc
*/
public static function getBinary()
public function getBinary(): string
{
$new_path = '/var/azuracast/servers/shoutcast2/sc_serv';
// Docker versions before 3 included the SC binary across the board.
$environment = Environment::getInstance();
if ($environment->isDocker() && !$environment->isDockerRevisionAtLeast(3)) {
if ($this->environment->isDocker() && !$this->environment->isDockerRevisionAtLeast(3)) {
return $new_path;
}
@ -95,73 +102,29 @@ class SHOUTcast extends AbstractFrontend
return $defaultResult;
}
/*
* Process Management
*/
/**
* @inheritdoc
*/
public function read(Entity\Station $station): bool
public function getConfigurationPath(Entity\Station $station): ?string
{
$config = $this->getConfig($station);
$station->setFrontendConfigDefaults($this->loadFromConfig($config));
return true;
return $station->getRadioConfigDir() . '/sc_serv.conf';
}
/**
* @return string[]|bool Returns either all lines of the config file or false on failure
*/
protected function getConfig(Entity\Station $station)
public function getCurrentConfiguration(Entity\Station $station): ?string
{
$config_dir = $station->getRadioConfigDir();
return @parse_ini_file($config_dir . '/sc_serv.conf', false, INI_SCANNER_RAW);
}
$configPath = $station->getRadioConfigDir();
$frontendConfig = $station->getFrontendConfig();
/*
* Configuration
*/
/**
* @return mixed[]
*/
protected function loadFromConfig($config): array
{
return [
Entity\StationFrontendConfiguration::PORT => $config['portbase'],
Entity\StationFrontendConfiguration::SOURCE_PASSWORD => $config['password'],
Entity\StationFrontendConfiguration::ADMIN_PASSWORD => $config['adminpassword'],
Entity\StationFrontendConfiguration::MAX_LISTENERS => $config['maxuser'],
$config = [
'password' => $frontendConfig->getSourcePassword(),
'adminpassword' => $frontendConfig->getAdminPassword(),
'logfile' => $configPath . '/sc_serv.log',
'w3clog' => $configPath . '/sc_w3c.log',
'banfile' => $this->writeIpBansFile($station),
'ripfile' => $configPath . '/sc_serv.rip',
'maxuser' => $frontendConfig->getMaxListeners() ?? 250,
'portbase' => $frontendConfig->getPort(),
'requirestreamconfigs' => 1,
];
}
public function write(Entity\Station $station): bool
{
$config = $this->getDefaults($station);
$frontend_config = $station->getFrontendConfig();
$port = $frontend_config->getPort();
if (null !== $port) {
$config['portbase'] = $port;
}
$sourcePw = $frontend_config->getSourcePassword();
if (!empty($sourcePw)) {
$config['password'] = $sourcePw;
}
$adminPw = $frontend_config->getAdminPassword();
if (!empty($adminPw)) {
$config['adminpassword'] = $adminPw;
}
$maxListeners = $frontend_config->getMaxListeners();
if (null !== $maxListeners) {
$config['maxuser'] = $maxListeners;
}
$customConfig = $frontend_config->getCustomConfiguration();
$customConfig = $frontendConfig->getCustomConfiguration();
if (!empty($customConfig)) {
$custom_conf = $this->processCustomConfig($customConfig);
if (!empty($custom_conf)) {
@ -185,54 +148,20 @@ class SHOUTcast extends AbstractFrontend
}
}
// Set any unset values back to the DB config.
$station->setFrontendConfigDefaults($this->loadFromConfig($config));
$this->em->persist($station);
$this->em->flush();
$config_path = $station->getRadioConfigDir();
$sc_path = $config_path . '/sc_serv.conf';
$sc_file = '';
$configFileOutput = '';
foreach ($config as $config_key => $config_value) {
$sc_file .= $config_key . '=' . str_replace("\n", '', $config_value) . "\n";
$configFileOutput .= $config_key . '=' . str_replace("\n", '', $config_value) . "\n";
}
file_put_contents($sc_path, $sc_file);
return true;
}
/**
* @return mixed[]
*/
protected function getDefaults(Entity\Station $station): array
{
$config_path = $station->getRadioConfigDir();
return [
'password' => Utilities\Strings::generatePassword(),
'adminpassword' => Utilities\Strings::generatePassword(),
'logfile' => $config_path . '/sc_serv.log',
'w3clog' => $config_path . '/sc_w3c.log',
'banfile' => $this->writeIpBansFile($station),
'ripfile' => $config_path . '/sc_serv.rip',
'maxuser' => 250,
'portbase' => $this->getRadioPort($station),
'requirestreamconfigs' => 1,
];
return $configFileOutput;
}
public function getCommand(Entity\Station $station): ?string
{
if ($binary = self::getBinary()) {
$config_path = $station->getRadioConfigDir();
$sc_config = $config_path . '/sc_serv.conf';
return $binary . ' ' . $sc_config;
if ($binary = $this->getBinary()) {
return $binary . ' ' . $this->getConfigurationPath($station);
}
return '/bin/false';
return null;
}
public function getAdminUrl(Entity\Station $station, UriInterface $base_url = null): UriInterface
@ -241,21 +170,4 @@ class SHOUTcast extends AbstractFrontend
return $public_url
->withPath($public_url->getPath() . '/admin.cgi');
}
protected function getDefaultMountSid(Entity\Station $station): int
{
$default_sid = null;
$sid = 0;
foreach ($station->getMounts() as $mount) {
/** @var Entity\StationMount $mount */
$sid++;
if ($mount->getIsDefault()) {
$default_sid = $sid;
break;
}
}
return $default_sid ?? 1;
}
}

View File

@ -66,4 +66,46 @@ class Arrays
JSON_THROW_ON_ERROR
);
}
/**
* array_merge_recursive does indeed merge arrays, but it converts values with duplicate
* keys to arrays rather than overwriting the value in the first array with the duplicate
* value in the second array, as array_merge does. I.e., with array_merge_recursive,
* this happens (documented behavior):
*
* array_merge_recursive(array('key' => 'org value'), array('key' => 'new value'));
* => array('key' => array('org value', 'new value'));
*
* array_merge_recursive_distinct does not change the datatypes of the values in the arrays.
* Matching keys' values in the second array overwrite those in the first array, as is the
* case with array_merge, i.e.:
*
* array_merge_recursive_distinct(array('key' => 'org value'), array('key' => 'new value'));
* => array('key' => array('new value'));
*
* Parameters are passed by reference, though only for performance reasons. They're not
* altered by this function.
*
* @param array $array1
* @param array $array2
*
* @return mixed[]
*
* @author Daniel <daniel (at) danielsmedegaardbuus (dot) dk>
* @author Gabriel Sobrinho <gabriel (dot) sobrinho (at) gmail (dot) com>
* @noinspection PhpParameterByRefIsNotUsedAsReferenceInspection
*/
public static function arrayMergeRecursiveDistinct(array &$array1, array &$array2): array
{
$merged = $array1;
foreach ($array2 as $key => &$value) {
if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) {
$merged[$key] = self::arrayMergeRecursiveDistinct($merged[$key], $value);
} else {
$merged[$key] = $value;
}
}
return $merged;
}
}

View File

@ -18,8 +18,8 @@ $props = [
// Common
'backendType' => $station->getBackendType(),
'frontendType' => $station->getFrontendType(),
'stationSupportsRequests' => $backend::supportsRequests(),
'stationSupportsStreamers' => $backend::supportsStreamers(),
'stationSupportsRequests' => $backend->supportsRequests(),
'stationSupportsStreamers' => $backend->supportsStreamers(),
'enableRequests' => $station->getEnableRequests(),
'enableStreamers' => $station->getEnableStreamers(),
'enablePublicPage' => $station->getEnablePublicPage(),