Adapter simplification:

- Remove the stub "None"/"Remote" adapters; return null for disabled adapters
  - Add capability checks to adapter enums
  - Remove the Remote proxy
This commit is contained in:
Buster "Silver Eagle" Neece 2022-06-21 20:48:32 -05:00
parent 80400dc28f
commit ad3d2b2c69
No known key found for this signature in database
GPG Key ID: F1D2E64A0005E80E
47 changed files with 313 additions and 581 deletions

View File

@ -13,13 +13,14 @@ return function (App\Event\BuildStationMenu $e) {
$backendConfig = $station->getBackendConfig();
$router = $request->getRouter();
$backend = $request->getStationBackend();
$frontend = $request->getStationFrontend();
$backendEnum = $station->getBackendTypeEnum();
$frontendEnum = $station->getFrontendTypeEnum();
$willDisconnectMessage = __('Restart broadcasting? This will disconnect any current listeners.');
$willNotDisconnectMessage = __('Reload broadcasting? Current listeners will not be disconnected.');
$reloadSupported = $frontend->supportsReload();
$reloadSupported = $frontendEnum->supportsReload();
$reloadMessage = $reloadSupported ? $willNotDisconnectMessage : $willDisconnectMessage;
$settings = $e->getSettings();
@ -67,28 +68,28 @@ return function (App\Event\BuildStationMenu $e) {
'label' => __('Music Files'),
'icon' => 'library_music',
'url' => (string)$router->fromHere('stations:files:index'),
'visible' => $backend->supportsMedia(),
'visible' => $backendEnum->isEnabled(),
'permission' => StationPermissions::Media,
],
'reports_duplicates' => [
'label' => __('Duplicate Songs'),
'class' => 'text-muted',
'url' => (string)$router->fromHere('stations:files:index') . '#special:duplicates',
'visible' => $backend->supportsMedia(),
'visible' => $backendEnum->isEnabled(),
'permission' => StationPermissions::Media,
],
'reports_unprocessable' => [
'label' => __('Unprocessable Files'),
'class' => 'text-muted',
'url' => (string)$router->fromHere('stations:files:index') . '#special:unprocessable',
'visible' => $backend->supportsMedia(),
'visible' => $backendEnum->isEnabled(),
'permission' => StationPermissions::Media,
],
'reports_unassigned' => [
'label' => __('Unassigned Files'),
'class' => 'text-muted',
'url' => (string)$router->fromHere('stations:files:index') . '#special:unassigned',
'visible' => $backend->supportsMedia(),
'visible' => $backendEnum->isEnabled(),
'permission' => StationPermissions::Media,
],
'ondemand' => [
@ -110,7 +111,7 @@ return function (App\Event\BuildStationMenu $e) {
'label' => __('Bulk Media Import/Export'),
'class' => 'text-muted',
'url' => (string)$router->fromHere('stations:bulk-media'),
'visible' => $backend->supportsMedia(),
'visible' => $backendEnum->isEnabled(),
'permission' => StationPermissions::Media,
],
],
@ -120,7 +121,7 @@ return function (App\Event\BuildStationMenu $e) {
'label' => __('Playlists'),
'icon' => 'queue_music',
'url' => (string)$router->fromHere('stations:playlists:index'),
'visible' => $backend->supportsMedia(),
'visible' => $backendEnum->isEnabled(),
'permission' => StationPermissions::Media,
],
@ -139,7 +140,7 @@ return function (App\Event\BuildStationMenu $e) {
'label' => __('Streamer/DJ Accounts'),
'icon' => 'mic',
'url' => (string)$router->fromHere('stations:streamers:index'),
'visible' => $backend->supportsStreamers() && $station->getEnableStreamers(),
'visible' => $backendEnum->isEnabled() && $station->getEnableStreamers(),
'permission' => StationPermissions::Streamers,
],
@ -203,13 +204,13 @@ return function (App\Event\BuildStationMenu $e) {
'label' => __('Mount Points'),
'icon' => 'wifi_tethering',
'url' => (string)$router->fromHere('stations:mounts:index'),
'visible' => $frontend->supportsMounts(),
'visible' => $frontendEnum->supportsMounts(),
'permission' => StationPermissions::MountPoints,
],
'hls_streams' => [
'label' => __('HLS Streams'),
'url' => (string)$router->fromHere('stations:hls_streams:index'),
'visible' => $backend->supportsHls() && $station->getEnableHls(),
'visible' => $backendEnum->isEnabled() && $station->getEnableHls(),
'permission' => StationPermissions::MountPoints,
],
'remotes' => [
@ -228,8 +229,7 @@ return function (App\Event\BuildStationMenu $e) {
'label' => __('Edit Liquidsoap Configuration'),
'class' => 'text-muted',
'url' => (string)$router->fromHere('stations:util:ls_config'),
'visible' => $settings->getEnableAdvancedFeatures()
&& $backend instanceof App\Radio\Backend\Liquidsoap,
'visible' => $settings->getEnableAdvancedFeatures() && $backendEnum->isEnabled(),
'permission' => StationPermissions::Broadcasting,
],
'stations:stereo_tool_config' => [
@ -237,7 +237,7 @@ return function (App\Event\BuildStationMenu $e) {
'class' => 'text-muted',
'url' => (string)$router->fromHere('stations:stereo_tool_config'),
'visible' => $settings->getEnableAdvancedFeatures()
&& $backend instanceof App\Radio\Backend\Liquidsoap
&& App\Radio\Enums\BackendAdapters::Liquidsoap === $backendEnum
&& AudioProcessingMethods::StereoTool === $backendConfig->getAudioProcessingMethodEnum(),
'permission' => StationPermissions::Broadcasting,
],

View File

@ -35,10 +35,12 @@ class OnSslRenewal extends CommandAbstract
foreach ($stations as $station) {
/** @var Entity\Station $station */
$frontend = $this->adapters->getFrontendAdapter($station);
if ($frontend->supportsReload()) {
$frontend->write($station);
$frontend->reload($station);
if ($station->getFrontendTypeEnum()->supportsReload()) {
$frontend = $this->adapters->getFrontendAdapter($station);
if (null !== $frontend) {
$frontend->write($station);
$frontend->reload($station);
}
}
}

View File

@ -4,9 +4,10 @@ declare(strict_types=1);
namespace App\Controller\Admin\Debug;
use App\Exception\StationUnsupportedException;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Radio\Backend\Liquidsoap;
use App\Radio\Adapters;
use Monolog\Handler\TestHandler;
use Monolog\Level;
use Monolog\Logger;
@ -15,7 +16,8 @@ use Psr\Http\Message\ResponseInterface;
final class TelnetAction
{
public function __construct(
private readonly Logger $logger
private readonly Logger $logger,
private readonly Adapters $adapters
) {
}
@ -28,20 +30,22 @@ final class TelnetAction
$this->logger->pushHandler($testHandler);
$station = $request->getStation();
$backend = $request->getStationBackend();
$backend = $this->adapters->getBackendAdapter($station);
if ($backend instanceof Liquidsoap) {
$command = $request->getParam('command');
$telnetResponse = $backend->command($station, $command);
$this->logger->debug(
'Telnet Command Response',
[
'response' => $telnetResponse,
]
);
if (null === $backend) {
throw new StationUnsupportedException();
}
$command = $request->getParam('command');
$telnetResponse = $backend->command($station, $command);
$this->logger->debug(
'Telnet Command Response',
[
'response' => $telnetResponse,
]
);
$this->logger->popHandler();
return $request->getView()->renderToResponse(

View File

@ -52,8 +52,6 @@ final class RelaysController
$return = [];
foreach ($stations as $station) {
$fa = $this->adapters->getFrontendAdapter($station);
$row = new Entity\Api\Admin\Relay();
$row->id = $station->getIdRequired();
$row->name = $station->getName();
@ -70,7 +68,9 @@ final class RelaysController
$row->admin_pw = $frontend_config->getAdminPassword();
$mounts = [];
if ($station->getMounts()->count() > 0) {
$fa = $this->adapters->getFrontendAdapter($station);
if (null !== $fa && $station->getMounts()->count() > 0) {
foreach ($station->getMounts() as $mount) {
/** @var Entity\StationMount $mount */
$mounts[] = $mount->api($fa);

View File

@ -10,7 +10,6 @@ use App\Entity;
use App\Environment;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Radio\Adapters;
use App\Radio\Configuration;
use DeepCopy;
use Doctrine\Common\Collections\Collection;
@ -36,7 +35,6 @@ final class CloneAction extends StationsController
Entity\Repository\StationRepository $stationRepo,
Entity\Repository\StorageLocationRepository $storageLocationRepo,
Entity\Repository\StationQueueRepository $queueRepo,
Adapters $adapters,
Configuration $configuration,
ReloadableEntityManagerInterface $reloadableEm,
Serializer $serializer,
@ -47,7 +45,6 @@ final class CloneAction extends StationsController
$stationRepo,
$storageLocationRepo,
$queueRepo,
$adapters,
$configuration,
$reloadableEm,
$serializer,
@ -164,10 +161,7 @@ final class CloneAction extends StationsController
$this->cloneCollection($record->getMounts(), $newStation, $copier);
} else {
$newStation = $this->reloadableEm->refetch($newStation);
// Create default mountpoints if station supports them.
$frontendAdapter = $this->adapters->getFrontendAdapter($newStation);
$this->stationRepo->resetMounts($newStation, $frontendAdapter);
$this->stationRepo->resetMounts($newStation);
}
if (in_array(self::CLONE_REMOTES, $toClone, true)) {

View File

@ -11,7 +11,6 @@ use App\Exception\ValidationException;
use App\Http\Response;
use App\Http\ServerRequest;
use App\OpenApi;
use App\Radio\Adapters;
use App\Radio\Configuration;
use InvalidArgumentException;
use OpenApi\Attributes as OA;
@ -147,7 +146,6 @@ class StationsController extends AbstractAdminApiCrudController
protected Entity\Repository\StationRepository $stationRepo,
protected Entity\Repository\StorageLocationRepository $storageLocationRepo,
protected Entity\Repository\StationQueueRepository $queueRepo,
protected Adapters $adapters,
protected Configuration $configuration,
protected ReloadableEntityManagerInterface $reloadableEm,
Serializer $serializer,
@ -323,13 +321,11 @@ class StationsController extends AbstractAdminApiCrudController
$hls_changed = $old_hls !== $station->getEnableHls();
if ($frontend_changed) {
$frontend = $this->adapters->getFrontendAdapter($station);
$this->stationRepo->resetMounts($station, $frontend);
$this->stationRepo->resetMounts($station);
}
if ($hls_changed || $backend_changed) {
$backend = $this->adapters->getBackendAdapter($station);
$this->stationRepo->resetHls($station, $backend);
$this->stationRepo->resetHls($station);
}
if ($adapter_changed || !$station->getIsEnabled()) {
@ -355,8 +351,7 @@ class StationsController extends AbstractAdminApiCrudController
$this->configuration->initializeConfiguration($station);
// Create default mountpoints if station supports them.
$frontend_adapter = $this->adapters->getFrontendAdapter($station);
$this->stationRepo->resetMounts($station, $frontend_adapter);
$this->stationRepo->resetMounts($station);
return $station;
}

View File

@ -101,7 +101,7 @@ final class BatchAction
$fs
);
$this->writePlaylistChanges($request, $affectedPlaylists);
$this->writePlaylistChanges($station, $affectedPlaylists);
return $result;
}
@ -193,7 +193,7 @@ final class BatchAction
$this->em->flush();
$this->writePlaylistChanges($request, $affectedPlaylists);
$this->writePlaylistChanges($station, $affectedPlaylists);
return $result;
}
@ -440,13 +440,11 @@ final class BatchAction
}
private function writePlaylistChanges(
ServerRequest $request,
Entity\Station $station,
array $playlists
): void {
// Write new PLS playlist configuration.
$backend = $request->getStationBackend();
if ($backend instanceof Liquidsoap) {
if ($station->getBackendTypeEnum()->isEnabled()) {
foreach ($playlists as $playlistId => $playlistRow) {
// Instruct the message queue to start a new "write playlist to file" task.
$message = new Message\WritePlaylistFileMessage();

View File

@ -145,8 +145,7 @@ final class HlsStreamsController extends AbstractStationApiCrudController
{
$station = parent::getStation($request);
$backend = $request->getStationBackend();
if (!$backend->supportsHls()) {
if (!$station->getBackendTypeEnum()->isEnabled()) {
throw new StationUnsupportedException();
}

View File

@ -12,6 +12,7 @@ use App\Http\Response;
use App\Http\Router;
use App\Http\ServerRequest;
use App\OpenApi;
use App\Radio\Adapters;
use App\Service\Flow\UploadedFile;
use OpenApi\Attributes as OA;
use Psr\Http\Message\ResponseInterface;
@ -152,7 +153,8 @@ final class MountsController extends AbstractStationApiCrudController
ReloadableEntityManagerInterface $em,
Serializer $serializer,
ValidatorInterface $validator,
private readonly Entity\Repository\StationMountRepository $mountRepo
private readonly Entity\Repository\StationMountRepository $mountRepo,
private readonly Adapters $adapters,
) {
parent::__construct($em, $serializer, $validator);
}
@ -195,20 +197,23 @@ final class MountsController extends AbstractStationApiCrudController
$return = parent::viewRecord($record, $request);
$station = $request->getStation();
$frontend = $request->getStationFrontend();
$router = $request->getRouter();
$frontend = $this->adapters->getFrontendAdapter($station);
$return['links']['intro'] = (string)$router->fromHere(
route_name: 'api:stations:mounts:intro',
route_params: ['id' => $record->getId()],
absolute: true
);
$return['links']['listen'] = (string)Router::resolveUri(
$router->getBaseUrl(),
$frontend->getUrlForMount($station, $record),
true
);
if (null !== $frontend) {
$return['links']['listen'] = (string)Router::resolveUri(
$router->getBaseUrl(),
$frontend->getUrlForMount($station, $record),
true
);
}
return $return;
}
@ -259,8 +264,7 @@ final class MountsController extends AbstractStationApiCrudController
{
$station = parent::getStation($request);
$frontend = $request->getStationFrontend();
if (!$frontend->supportsMounts()) {
if (!$station->getFrontendTypeEnum()->supportsMounts()) {
throw new StationUnsupportedException();
}

View File

@ -7,6 +7,7 @@ namespace App\Controller\Api\Stations;
use App\Entity;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Radio\Adapters;
use GuzzleHttp\Psr7\Uri;
use Psr\Http\Message\ResponseInterface;
@ -15,7 +16,8 @@ final class ProfileAction
public function __construct(
private readonly Entity\Repository\StationScheduleRepository $scheduleRepo,
private readonly Entity\ApiGenerator\NowPlayingApiGenerator $nowPlayingApiGenerator,
private readonly Entity\ApiGenerator\StationApiGenerator $stationApiGenerator
private readonly Entity\ApiGenerator\StationApiGenerator $stationApiGenerator,
private readonly Adapters $adapters,
) {
}
@ -25,8 +27,8 @@ final class ProfileAction
string $station_id
): ResponseInterface {
$station = $request->getStation();
$backend = $request->getStationBackend();
$frontend = $request->getStationFrontend();
$backend = $this->adapters->getBackendAdapter($station);
$frontend = $this->adapters->getFrontendAdapter($station);
$baseUri = new Uri('');
$nowPlayingApi = $this->nowPlayingApiGenerator->currentOrEmpty($station, $baseUri);
@ -38,8 +40,8 @@ final class ProfileAction
$apiResponse->cache = 'database';
$apiResponse->services = new Entity\Api\StationServiceStatus(
$backend->isRunning($station),
$frontend->isRunning($station),
null !== $backend && $backend->isRunning($station),
null !== $frontend && $frontend->isRunning($station),
$station->getHasStarted(),
$station->getNeedsRestart()
);

View File

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

View File

@ -5,12 +5,13 @@ declare(strict_types=1);
namespace App\Controller\Api\Stations;
use App\Entity;
use App\Exception\StationUnsupportedException;
use App\Exception\Supervisor\NotRunningException;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Nginx\Nginx;
use App\OpenApi;
use App\Radio\Backend\Liquidsoap;
use App\Radio\Adapters;
use App\Radio\Configuration;
use Doctrine\ORM\EntityManagerInterface;
use OpenApi\Attributes as OA;
@ -109,6 +110,7 @@ final class ServicesController
private readonly EntityManagerInterface $em,
private readonly Configuration $configuration,
private readonly Nginx $nginx,
private readonly Adapters $adapters,
) {
}
@ -119,13 +121,13 @@ final class ServicesController
): ResponseInterface {
$station = $request->getStation();
$backend = $request->getStationBackend();
$frontend = $request->getStationFrontend();
$backend = $this->adapters->getBackendAdapter($station);
$frontend = $this->adapters->getFrontendAdapter($station);
return $response->withJson(
new Entity\Api\StationServiceStatus(
$backend->isRunning($station),
$frontend->isRunning($station),
null !== $backend && $backend->isRunning($station),
null !== $frontend && $frontend->isRunning($station),
$station->getHasStarted(),
$station->getNeedsRestart()
)
@ -202,7 +204,11 @@ final class ServicesController
string $do = 'restart'
): ResponseInterface {
$station = $request->getStation();
$frontend = $request->getStationFrontend();
$frontend = $this->adapters->getFrontendAdapter($station);
if (null === $frontend) {
throw new StationUnsupportedException();
}
switch ($do) {
case 'stop':
@ -242,20 +248,20 @@ final class ServicesController
string $do = 'restart'
): ResponseInterface {
$station = $request->getStation();
$backend = $request->getStationBackend();
$backend = $this->adapters->getBackendAdapter($station);
if (null === $backend) {
throw new StationUnsupportedException();
}
switch ($do) {
case 'skip':
if ($backend instanceof Liquidsoap) {
$backend->skip($station);
}
$backend->skip($station);
return $response->withJson(new Entity\Api\Status(true, __('Song skipped.')));
case 'disconnect':
if ($backend instanceof Liquidsoap) {
$backend->disconnectStreamer($station);
}
$backend->disconnectStreamer($station);
return $response->withJson(new Entity\Api\Status(true, __('Streamer disconnected.')));

View File

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

View File

@ -5,25 +5,32 @@ declare(strict_types=1);
namespace App\Controller\Api\Stations;
use App\Entity;
use App\Exception\StationUnsupportedException;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Radio\Adapters;
use Psr\Http\Message\ResponseInterface;
use const ARRAY_FILTER_USE_KEY;
final class UpdateMetadataAction
{
public function __construct(
private readonly Adapters $adapters,
) {
}
public function __invoke(
ServerRequest $request,
Response $response,
string $station_id
): ResponseInterface {
$station = $request->getStation();
$backend = $request->getStationBackend();
if (!method_exists($backend, 'updateMetadata')) {
return $response->withStatus(500)
->withJson(new Entity\Api\Error(500, 'This function is not supported on this station.'));
$backend = $this->adapters->getBackendAdapter($station);
if (null === $backend) {
throw new StationUnsupportedException();
}
$allowedMetaFields = [

View File

@ -7,10 +7,16 @@ namespace App\Controller\Frontend\PublicPages;
use App\Entity;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Radio\Adapters;
use Psr\Http\Message\ResponseInterface;
final class PlaylistAction
{
public function __construct(
private readonly Adapters $adapters,
) {
}
public function __invoke(
ServerRequest $request,
Response $response,
@ -22,31 +28,31 @@ final class PlaylistAction
$streams = [];
$stream_urls = [];
$fa = $request->getStationFrontend();
foreach ($station->getMounts() as $mount) {
/** @var Entity\StationMount $mount */
if (!$mount->getIsVisibleOnPublicPages()) {
continue;
$fa = $this->adapters->getFrontendAdapter($station);
if (null !== $fa) {
foreach ($station->getMounts() as $mount) {
/** @var Entity\StationMount $mount */
if (!$mount->getIsVisibleOnPublicPages()) {
continue;
}
$stream_url = $fa->getUrlForMount($station, $mount);
$stream_urls[] = $stream_url;
$streams[] = [
'name' => $station->getName() . ' - ' . $mount->getDisplayName(),
'url' => $stream_url,
];
}
$stream_url = $fa->getUrlForMount($station, $mount);
$stream_urls[] = $stream_url;
$streams[] = [
'name' => $station->getName() . ' - ' . $mount->getDisplayName(),
'url' => $stream_url,
];
}
foreach ($request->getStationRemotes() as $remote_proxy) {
$adapter = $remote_proxy->getAdapter();
$remote = $remote_proxy->getRemote();
foreach ($station->getRemotes() as $remote) {
if (!$remote->getIsVisibleOnPublicPages()) {
continue;
}
$stream_url = $adapter->getPublicUrl($remote);
$stream_url = $this->adapters->getRemoteAdapter($station, $remote)
->getPublicUrl($remote);
$stream_urls[] = $stream_url;
$streams[] = [

View File

@ -9,13 +9,14 @@ use App\Exception\StationNotFoundException;
use App\Exception\StationUnsupportedException;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Radio\Backend\Liquidsoap;
use App\Radio\Adapters;
use Psr\Http\Message\ResponseInterface;
final class WebDjAction
{
public function __construct(
private readonly Assets $assets
private readonly Assets $assets,
private readonly Adapters $adapters,
) {
}
@ -34,9 +35,8 @@ final class WebDjAction
throw new StationUnsupportedException();
}
$backend = $request->getStationBackend();
if (!($backend instanceof Liquidsoap)) {
$backend = $this->adapters->getBackendAdapter($station);
if (null === $backend) {
throw new StationUnsupportedException();
}

View File

@ -27,8 +27,7 @@ final class EditLiquidsoapConfigAction
): ResponseInterface {
$station = $request->getStation();
$backend = $request->getStationBackend();
if (!($backend instanceof Liquidsoap)) {
if (!$station->getBackendTypeEnum()->isEnabled()) {
throw new StationUnsupportedException();
}

View File

@ -40,7 +40,7 @@ final class FilesAction
$router = $request->getRouter();
$backend = $request->getStationBackend();
$backendEnum = $station->getBackendTypeEnum();
return $request->getView()->renderVuePage(
response: $response,
@ -63,7 +63,7 @@ final class FilesAction
'stationTimeZone' => $station->getTimezone(),
'showSftp' => SftpGo::isSupportedForStation($station),
'sftpUrl' => (string)$router->fromHere('stations:sftp_users:index'),
'supportsImmediateQueue' => $backend->supportsImmediateQueue(),
'supportsImmediateQueue' => $backendEnum->isEnabled(),
],
);
}

View File

@ -17,9 +17,8 @@ final class HlsStreamsAction
string $station_id
): ResponseInterface {
$station = $request->getStation();
$backend = $request->getStationBackend();
if (!$backend->supportsHls() || !$station->getEnableHls()) {
if (!$station->getBackendTypeEnum()->isEnabled() || !$station->getEnableHls()) {
throw new StationUnsupportedException();
}

View File

@ -24,8 +24,7 @@ final class PlaylistsAction
): ResponseInterface {
$station = $request->getStation();
$backend = $request->getStationBackend();
if (!$backend->supportsMedia()) {
if (!$station->getBackendTypeEnum()->isEnabled()) {
throw new Exception(__('This feature is not currently supported on this station.'));
}

View File

@ -7,6 +7,7 @@ namespace App\Controller\Stations;
use App\Enums\StationPermissions;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Radio\Adapters;
use App\VueComponent\StationFormComponent;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Http\Message\ResponseInterface;
@ -18,6 +19,7 @@ final class ProfileController
public function __construct(
private readonly EntityManagerInterface $em,
private readonly StationFormComponent $stationFormComponent,
private readonly Adapters $adapters,
) {
}
@ -57,9 +59,9 @@ final class ProfileController
$csrf = $request->getCsrf()->generate(self::CSRF_NAMESPACE);
$backend = $request->getStationBackend();
$frontend = $request->getStationFrontend();
$backendEnum = $station->getBackendTypeEnum();
$frontend = $this->adapters->getFrontendAdapter($station);
$frontendConfig = $station->getFrontendConfig();
$acl = $request->getAcl();
@ -75,8 +77,8 @@ final class ProfileController
'backendType' => $station->getBackendType(),
'frontendType' => $station->getFrontendType(),
'stationTimeZone' => $station->getTimezone(),
'stationSupportsRequests' => $backend->supportsRequests(),
'stationSupportsStreamers' => $backend->supportsStreamers(),
'stationSupportsRequests' => $backendEnum->isEnabled(),
'stationSupportsStreamers' => $backendEnum->isEnabled(),
'enableRequests' => $station->getEnableRequests(),
'enableStreamers' => $station->getEnableStreamers(),
'enablePublicPage' => $station->getEnablePublicPage(),
@ -175,7 +177,7 @@ final class ProfileController
),
// Frontend
'frontendAdminUri' => (string)$frontend->getAdminUrl($station, $router->getBaseUrl()),
'frontendAdminUri' => (string)$frontend?->getAdminUrl($station, $router->getBaseUrl()),
'frontendAdminPassword' => $frontendConfig->getAdminPassword(),
'frontendSourcePassword' => $frontendConfig->getSourcePassword(),
'frontendRelayPassword' => $frontendConfig->getRelayPassword(),

View File

@ -8,6 +8,7 @@ use App\Entity;
use App\Exception\StationUnsupportedException;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Radio\Adapters;
use App\Service\AzuraCastCentral;
use Psr\Http\Message\ResponseInterface;
@ -15,7 +16,8 @@ final class StreamersAction
{
public function __construct(
private readonly AzuraCastCentral $acCentral,
private readonly Entity\Repository\SettingsRepository $settingsRepo
private readonly Entity\Repository\SettingsRepository $settingsRepo,
private readonly Adapters $adapters,
) {
}
@ -25,9 +27,9 @@ final class StreamersAction
string $station_id
): ResponseInterface {
$station = $request->getStation();
$backend = $request->getStationBackend();
$backend = $this->adapters->getBackendAdapter($station);
if (!$backend->supportsStreamers() && !$station->getEnableStreamers()) {
if (null === $backend || !$station->getEnableStreamers()) {
throw new StationUnsupportedException();
}

View File

@ -55,7 +55,7 @@ class Station implements ResolvableUrlInterface
description: 'The full URL to listen to the default mount of the station',
example: 'http://localhost:8000/radio.mp3'
)]
public string|UriInterface $listen_url;
public string|UriInterface|null $listen_url;
#[OA\Property(
description: 'The public URL of the station.',
@ -115,7 +115,9 @@ class Station implements ResolvableUrlInterface
*/
public function resolveUrls(UriInterface $base): void
{
$this->listen_url = (string)Router::resolveUri($base, $this->listen_url, true);
$this->listen_url = (null !== $this->listen_url)
? (string)Router::resolveUri($base, $this->listen_url, true)
: null;
$this->public_player_url = (string)Router::resolveUri($base, $this->public_player_url, true);
$this->playlist_pls_url = (string)Router::resolveUri($base, $this->playlist_pls_url, true);

View File

@ -22,9 +22,8 @@ class StationApiGenerator
?UriInterface $baseUri = null,
bool $showAllMounts = false
): Entity\Api\NowPlaying\Station {
$fa = $this->adapters->getFrontendAdapter($station);
$frontend = $this->adapters->getFrontendAdapter($station);
$backend = $this->adapters->getBackendAdapter($station);
$remoteAdapters = $this->adapters->getRemoteAdapters($station);
$response = new Entity\Api\NowPlaying\Station();
$response->id = (int)$station->getId();
@ -35,7 +34,7 @@ class StationApiGenerator
$response->backend = (string)$station->getBackendType();
$response->url = $station->getUrl();
$response->is_public = $station->getEnablePublicPage();
$response->listen_url = $fa->getStreamUrl($station, $baseUri);
$response->listen_url = $frontend?->getStreamUrl($station, $baseUri);
$response->public_player_url = (string)$this->router->named(
'public:index',
@ -51,26 +50,31 @@ class StationApiGenerator
);
$mounts = [];
if ($fa->supportsMounts() && $station->getMounts()->count() > 0) {
if (
null !== $frontend && $station->getFrontendTypeEnum()->supportsMounts() && $station->getMounts()->count(
) > 0
) {
foreach ($station->getMounts() as $mount) {
if ($showAllMounts || $mount->getIsVisibleOnPublicPages()) {
$mounts[] = $mount->api($fa, $baseUri);
$mounts[] = $mount->api($frontend, $baseUri);
}
}
}
$response->mounts = $mounts;
$remotes = [];
foreach ($remoteAdapters as $ra_proxy) {
$remote = $ra_proxy->getRemote();
foreach ($station->getRemotes() as $remote) {
if ($showAllMounts || $remote->getIsVisibleOnPublicPages()) {
$remotes[] = $remote->api($ra_proxy->getAdapter());
$remotes[] = $remote->api(
$this->adapters->getRemoteAdapter($station, $remote)
);
}
}
$response->remotes = $remotes;
$response->hls_enabled = $backend->supportsHls() && $station->getEnableHls();
$response->hls_url = ($response->hls_enabled)
$response->hls_enabled = $station->getBackendTypeEnum()->isEnabled() && $station->getEnableHls();
$response->hls_url = (null !== $backend && $response->hls_enabled)
? $backend->getHlsUrl($station, $baseUri)
: null;

View File

@ -9,8 +9,7 @@ use App\Doctrine\ReloadableEntityManagerInterface;
use App\Doctrine\Repository;
use App\Entity;
use App\Flysystem\StationFilesystems;
use App\Radio\Backend\AbstractBackend;
use App\Radio\Frontend\AbstractFrontend;
use App\Radio\Enums\StreamFormats;
use App\Service\Flow\UploadedFile;
use Azura\Files\ExtendedFilesystemInterface;
use Closure;
@ -87,47 +86,49 @@ final class StationRepository extends Repository
)->toIterable();
}
/**
* @param string $short_code
*/
public function findByShortCode(string $short_code): ?Entity\Station
{
return $this->repository->findOneBy(['short_name' => $short_code]);
}
/**
* Reset mount points to their adapter defaults (in the event of an adapter change).
*
* @param Entity\Station $station
* @param AbstractFrontend $frontend_adapter
*/
public function resetMounts(Entity\Station $station, AbstractFrontend $frontend_adapter): void
public function resetMounts(Entity\Station $station): void
{
foreach ($station->getMounts() as $mount) {
$this->em->remove($mount);
}
// Create default mountpoints if station supports them.
if ($frontend_adapter->supportsMounts()) {
// Create default mount points.
foreach ($frontend_adapter->getDefaultMounts($station) as $mount) {
$this->em->persist($mount);
}
if ($station->getFrontendTypeEnum()->supportsMounts()) {
$record = new Entity\StationMount($station);
$record->setName('/radio.mp3');
$record->setIsDefault(true);
$record->setEnableAutodj(true);
$record->setAutodjFormat(StreamFormats::Mp3->value);
$record->setAutodjBitrate(128);
$this->em->persist($record);
}
$this->em->flush();
$this->em->refresh($station);
}
public function resetHls(Entity\Station $station, AbstractBackend $backend): void
public function resetHls(Entity\Station $station): void
{
foreach ($station->getHlsStreams() as $hlsStream) {
$this->em->remove($hlsStream);
}
if ($station->getEnableHls() && $backend->supportsHls()) {
foreach ($backend->getDefaultHlsStreams($station) as $hlsStream) {
$this->em->persist($hlsStream);
if ($station->getEnableHls() && $station->getBackendTypeEnum()->isEnabled()) {
$streams = [
'aac_lofi' => 48,
'aac_midfi' => 96,
'aac_hifi' => 192,
];
foreach ($streams as $name => $bitrate) {
$record = new Entity\StationHlsStream($station);
$record->setName($name);
$record->setFormat(StreamFormats::Aac->value);
$record->setBitrate($bitrate);
$this->em->persist($record);
}
}

View File

@ -5,7 +5,8 @@ declare(strict_types=1);
namespace App\Event\Radio;
use App\Entity\Station;
use App\Radio;
use App\Radio\Adapters;
use App\Radio\Frontend\AbstractFrontend;
use NowPlaying\Result\Result;
use Symfony\Contracts\EventDispatcher\Event;
@ -14,9 +15,8 @@ class GenerateRawNowPlaying extends Event
protected ?Result $result = null;
public function __construct(
protected Adapters $adapters,
protected Station $station,
protected Radio\Frontend\AbstractFrontend $frontend,
protected array $remotes,
protected bool $include_clients = false
) {
}
@ -26,17 +26,21 @@ class GenerateRawNowPlaying extends Event
return $this->station;
}
public function getFrontend(): Radio\Frontend\AbstractFrontend
public function getFrontend(): ?AbstractFrontend
{
return $this->frontend;
return $this->adapters->getFrontendAdapter($this->station);
}
/**
* @return Radio\Remote\AdapterProxy[]
*/
public function getRemotes(): array
{
return $this->remotes;
$remotes = [];
foreach ($this->station->getRemotes() as $remote) {
$remotes[] = [
$remote,
$this->adapters->getRemoteAdapter($this->station, $remote),
];
}
return $remotes;
}
public function includeClients(): bool

View File

@ -10,7 +10,6 @@ use App\Customization;
use App\Entity;
use App\Enums\SupportedLocales;
use App\Exception;
use App\Radio;
use App\RateLimit;
use App\Session;
use App\View;
@ -30,9 +29,6 @@ final class ServerRequest extends \Slim\Http\ServerRequest
public const ATTR_CUSTOMIZATION = 'customization';
public const ATTR_AUTH = 'auth';
public const ATTR_STATION = 'station';
public const ATTR_STATION_BACKEND = 'station_backend';
public const ATTR_STATION_FRONTEND = 'station_frontend';
public const ATTR_STATION_REMOTES = 'station_remotes';
public const ATTR_USER = 'user';
public function getView(): View
@ -95,34 +91,6 @@ final class ServerRequest extends \Slim\Http\ServerRequest
return $this->getAttributeOfClass(self::ATTR_STATION, Entity\Station::class);
}
public function getStationFrontend(): Radio\Frontend\AbstractFrontend
{
return $this->getAttributeOfClass(self::ATTR_STATION_FRONTEND, Radio\Frontend\AbstractFrontend::class);
}
public function getStationBackend(): Radio\Backend\AbstractBackend
{
return $this->getAttributeOfClass(self::ATTR_STATION_BACKEND, Radio\Backend\AbstractBackend::class);
}
/**
* @return Radio\Remote\AdapterProxy[]
* @throws Exception\InvalidRequestAttribute
*/
public function getStationRemotes(): array
{
$remotes = $this->serverRequest->getAttribute(self::ATTR_STATION_REMOTES);
if (null === $remotes) {
throw new Exception\InvalidRequestAttribute(sprintf(
'Attribute "%s" was not set.',
self::ATTR_STATION_REMOTES
));
}
return $remotes;
}
/**
* @param string $attr
* @param string $class_name

View File

@ -7,7 +7,6 @@ namespace App\Middleware;
use App\Entity;
use App\Entity\Repository\StationRepository;
use App\Http\ServerRequest;
use App\Radio\Adapters;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
@ -20,8 +19,7 @@ use Slim\Routing\RouteContext;
class GetStation implements MiddlewareInterface
{
public function __construct(
protected StationRepository $station_repo,
protected Adapters $adapters
protected StationRepository $station_repo
) {
}
@ -35,15 +33,7 @@ class GetStation implements MiddlewareInterface
$record = $this->station_repo->findByIdentifier($id);
if ($record instanceof Entity\Station) {
$backend = $this->adapters->getBackendAdapter($record);
$frontend = $this->adapters->getFrontendAdapter($record);
$remotes = $this->adapters->getRemoteAdapters($record);
$request = $request
->withAttribute(ServerRequest::ATTR_STATION, $record)
->withAttribute(ServerRequest::ATTR_STATION_BACKEND, $backend)
->withAttribute(ServerRequest::ATTR_STATION_FRONTEND, $frontend)
->withAttribute(ServerRequest::ATTR_STATION_REMOTES, $remotes);
$request = $request->withAttribute(ServerRequest::ATTR_STATION, $record);
}
}

View File

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

View File

@ -29,14 +29,9 @@ class Stations
$view = $request->getView();
$station = $request->getStation();
$backend = $request->getStationBackend();
$frontend = $request->getStationFrontend();
$view->addData(
[
'station' => $station,
'frontend' => $frontend,
'backend' => $backend,
]
);

View File

@ -18,7 +18,7 @@ use Supervisor\Exception\Fault;
use Supervisor\Exception\SupervisorException as SupervisorLibException;
use Supervisor\SupervisorInterface;
abstract class AbstractAdapter
abstract class AbstractLocalAdapter
{
public function __construct(
protected Environment $environment,
@ -155,14 +155,6 @@ abstract class AbstractAdapter
$this->start($station);
}
/**
* @return bool Whether this adapter supports a non-destructive reload.
*/
public function supportsReload(): bool
{
return false;
}
/**
* Execute a non-destructive reload if the adapter supports it.
*

View File

@ -6,6 +6,7 @@ namespace App\Radio;
use App\Entity;
use App\Exception\NotFoundException;
use App\Radio\Backend\Liquidsoap;
use App\Radio\Enums\AdapterTypeInterface;
use App\Radio\Enums\BackendAdapters;
use App\Radio\Enums\FrontendAdapters;
@ -22,19 +23,13 @@ class Adapters
) {
}
/**
* @param Entity\Station $station
*
* @throws NotFoundException
*/
public function getFrontendAdapter(Entity\Station $station): Frontend\AbstractFrontend
public function getFrontendAdapter(Entity\Station $station): ?Frontend\AbstractFrontend
{
$class_name = $station->getFrontendTypeEnum()->getClass();
if ($this->adapters->has($class_name)) {
return $this->adapters->get($class_name);
}
$className = $station->getFrontendTypeEnum()->getClass();
throw new NotFoundException('Adapter not found: ' . $class_name);
return (null !== $className && $this->adapters->has($className))
? $this->adapters->get($className)
: null;
}
/**
@ -46,19 +41,13 @@ class Adapters
return $this->listAdaptersFromEnum(FrontendAdapters::cases(), $checkInstalled);
}
/**
* @param Entity\Station $station
*
* @throws NotFoundException
*/
public function getBackendAdapter(Entity\Station $station): Backend\AbstractBackend
public function getBackendAdapter(Entity\Station $station): ?Liquidsoap
{
$class_name = $station->getBackendTypeEnum()->getClass();
if ($this->adapters->has($class_name)) {
return $this->adapters->get($class_name);
}
$className = $station->getBackendTypeEnum()->getClass();
throw new NotFoundException('Adapter not found: ' . $class_name);
return (null !== $className && $this->adapters->has($className))
? $this->adapters->get($className)
: null;
}
/**
@ -70,21 +59,6 @@ class Adapters
return $this->listAdaptersFromEnum(BackendAdapters::cases(), $checkInstalled);
}
/**
* @param Entity\Station $station
*
* @return Remote\AdapterProxy[]
* @throws NotFoundException
*/
public function getRemoteAdapters(Entity\Station $station): array
{
$remote_adapters = [];
foreach ($station->getRemotes() as $remote) {
$remote_adapters[] = new Remote\AdapterProxy($this->getRemoteAdapter($station, $remote), $remote);
}
return $remote_adapters;
}
public function getRemoteAdapter(Entity\Station $station, Entity\StationRemote $remote): Remote\AbstractRemote
{
$class_name = $remote->getTypeEnum()->getClass();
@ -113,8 +87,8 @@ class Adapters
$adapters = [];
foreach ($cases as $adapter) {
$adapters[$adapter->getValue()] = [
'enum' => $adapter,
'name' => $adapter->getName(),
'enum' => $adapter,
'name' => $adapter->getName(),
'class' => $adapter->getClass(),
];
}
@ -123,7 +97,11 @@ class Adapters
return array_filter(
$adapters,
function ($adapter_info) {
/** @var AbstractAdapter $adapter */
if (null === $adapter_info['class']) {
return true;
}
/** @var AbstractLocalAdapter $adapter */
$adapter = $this->adapters->get($adapter_info['class']);
return $adapter->isInstalled();
}

View File

@ -69,7 +69,9 @@ class Annotations implements EventSubscriberInterface
$event->setSongPath('media:' . ltrim($media->getPath(), '/'));
$backend = $this->adapters->getBackendAdapter($event->getStation());
$event->addAnnotations($backend->annotateMedia($media));
if (null !== $backend) {
$event->addAnnotations($backend->annotateMedia($media));
}
} else {
$queue = $event->getQueue();
if ($queue instanceof Entity\StationQueue) {

View File

@ -1,93 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Radio\Backend;
use App\Entity;
use App\Nginx\CustomUrls;
use App\Radio\AbstractAdapter;
use App\Radio\Enums\StreamFormats;
use Psr\Http\Message\UriInterface;
use RuntimeException;
abstract class AbstractBackend extends AbstractAdapter
{
public function supportsMedia(): bool
{
return false;
}
public function supportsRequests(): bool
{
return false;
}
public function supportsStreamers(): bool
{
return false;
}
public function supportsWebStreaming(): bool
{
return false;
}
public function supportsImmediateQueue(): bool
{
return false;
}
public function supportsHls(): bool
{
return false;
}
public function getDefaultHlsStreams(Entity\Station $station): array
{
return array_map(
function (string $name, int $bitrate) use ($station) {
$record = new Entity\StationHlsStream($station);
$record->setName($name);
$record->setFormat(StreamFormats::Aac->value);
$record->setBitrate($bitrate);
return $record;
},
['aac_lofi', 'aac_midfi', 'aac_hifi'],
[48, 96, 192]
);
}
public function getHlsUrl(Entity\Station $station, UriInterface $baseUrl = null): UriInterface
{
if (!$this->supportsHls()) {
throw new RuntimeException('Cannot generate HLS URL.');
}
$baseUrl ??= $this->router->getBaseUrl();
return $baseUrl->withPath(
$baseUrl->getPath() . CustomUrls::getHlsUrl($station) . '/live.m3u8'
);
}
public function getStreamPort(Entity\Station $station): ?int
{
return null;
}
/**
* @param Entity\StationMedia $media
*
* @return mixed[]
*/
public function annotateMedia(Entity\StationMedia $media): array
{
return [];
}
public function getProgramName(Entity\Station $station): string
{
return 'station_' . $station->getId() . ':station_' . $station->getId() . '_backend';
}
}

View File

@ -8,43 +8,14 @@ use App\Entity;
use App\Event\Radio\WriteLiquidsoapConfiguration;
use App\Exception;
use App\Nginx\CustomUrls;
use App\Radio\AbstractLocalAdapter;
use App\Radio\Enums\LiquidsoapQueues;
use LogicException;
use Psr\Http\Message\UriInterface;
use Symfony\Component\Process\Process;
class Liquidsoap extends AbstractBackend
class Liquidsoap extends AbstractLocalAdapter
{
public function supportsMedia(): bool
{
return true;
}
public function supportsRequests(): bool
{
return true;
}
public function supportsStreamers(): bool
{
return true;
}
public function supportsWebStreaming(): bool
{
return true;
}
public function supportsImmediateQueue(): bool
{
return true;
}
public function supportsHls(): bool
{
return true;
}
/**
* @inheritDoc
*/
@ -234,6 +205,14 @@ class Liquidsoap extends AbstractBackend
: null;
}
public function getHlsUrl(Entity\Station $station, UriInterface $baseUrl = null): UriInterface
{
$baseUrl ??= $this->router->getBaseUrl();
return $baseUrl->withPath(
$baseUrl->getPath() . CustomUrls::getHlsUrl($station) . '/live.m3u8'
);
}
public function isQueueEmpty(
Entity\Station $station,
LiquidsoapQueues $queue
@ -337,4 +316,9 @@ class Liquidsoap extends AbstractBackend
throw new LogicException($process->getOutput());
}
}
public function getProgramName(Entity\Station $station): string
{
return 'station_' . $station->getIdRequired() . ':station_' . $station->getIdRequired() . '_backend';
}
}

View File

@ -1,23 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Radio\Backend;
use App\Entity;
class None extends AbstractBackend
{
public function isInstalled(): bool
{
return true;
}
public function start(Entity\Station $station): void
{
$this->logger->error(
'Cannot start process; AutoDJ is currently disabled.',
['station_id' => $station->getId(), 'station_name' => $station->getName()]
);
}
}

View File

@ -113,51 +113,52 @@ class Configuration
throw new RuntimeException('Station is disabled.');
}
$frontendEnum = $station->getFrontendTypeEnum();
$backendEnum = $station->getBackendTypeEnum();
$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)) {
if (
(null === $frontend || !$frontend->hasCommand($station))
&& (null === $backend || !$backend->hasCommand($station))
) {
$this->unlinkAndStopStation($station, $reloadSupervisor);
throw new RuntimeException('Station has no local services.');
}
// If using AutoDJ and there is no media, don't spin up services.
if (
BackendAdapters::None !== $station->getBackendTypeEnum()
$backendEnum->isEnabled()
&& !$this->stationPlaylistRepo->stationHasActivePlaylists($station)
) {
$this->unlinkAndStopStation($station, $reloadSupervisor);
throw new RuntimeException('Station has no media assigned to playlists.');
}
// Get group information
$backend_name = $backend->getProgramName($station);
[$backend_group, $backend_program] = explode(':', $backend_name);
$frontend_name = $frontend->getProgramName($station);
[, $frontend_program] = explode(':', $frontend_name);
// Write group section of config
$programs = [];
if ($backend->hasCommand($station)) {
$programs[] = $backend_program;
if (null !== $backend && $backend->hasCommand($station)) {
$programs[] = (explode(':', $backend->getProgramName($station)))[2];
}
if ($frontend->hasCommand($station)) {
$programs[] = $frontend_program;
if (null !== $frontend && $frontend->hasCommand($station)) {
$programs[] = (explode(':', $frontend->getProgramName($station)))[2];
}
$supervisorConfig[] = '[group:' . $backend_group . ']';
$stationGroup = 'station_' . $station->getIdRequired();
$supervisorConfig[] = '[group:' . $stationGroup . ']';
$supervisorConfig[] = 'programs=' . implode(',', $programs);
$supervisorConfig[] = '';
// Write frontend
if ($frontend->hasCommand($station)) {
if (null !== $frontend && $frontend->hasCommand($station)) {
$supervisorConfig[] = $this->writeConfigurationSection($station, $frontend, 90);
}
// Write backend
if ($backend->hasCommand($station)) {
if (null !== $backend && $backend->hasCommand($station)) {
$supervisorConfig[] = $this->writeConfigurationSection($station, $backend, 100);
}
@ -166,22 +167,22 @@ class Configuration
file_put_contents($supervisorConfigFile, $supervisor_config_data);
// Write supporting configurations.
$frontend->write($station);
$backend->write($station);
$frontend?->write($station);
$backend?->write($station);
// Reload Supervisord and process groups
if ($reloadSupervisor) {
$affected_groups = $this->reloadSupervisor();
$was_restarted = in_array($backend_group, $affected_groups, true);
$was_restarted = in_array($stationGroup, $affected_groups, true);
if (!$was_restarted && $forceRestart) {
try {
if ($attemptReload && ($backend->supportsReload() || $frontend->supportsReload())) {
$backend->reload($station);
$frontend->reload($station);
if ($attemptReload && ($backendEnum->isEnabled() || $frontendEnum->supportsReload())) {
$backend?->reload($station);
$frontend?->reload($station);
} else {
$this->supervisor->stopProcessGroup($backend_group);
$this->supervisor->startProcessGroup($backend_group);
$this->supervisor->stopProcessGroup($stationGroup);
$this->supervisor->startProcessGroup($stationGroup);
}
} catch (SupervisorException) {
}
@ -310,8 +311,8 @@ class Configuration
public function assignRadioPorts(Station $station, bool $force = false): void
{
if (
FrontendAdapters::Remote !== $station->getFrontendTypeEnum()
|| BackendAdapters::Liquidsoap !== $station->getBackendTypeEnum()
$station->getFrontendTypeEnum()->isEnabled()
|| $station->getBackendTypeEnum()->isEnabled()
) {
$frontend_config = $station->getFrontendConfig();
$backend_config = $station->getBackendConfig();
@ -444,7 +445,7 @@ class Configuration
protected function writeConfigurationSection(
Station $station,
AbstractAdapter $adapter,
AbstractLocalAdapter $adapter,
?int $priority
): string {
[, $program_name] = explode(':', $adapter->getProgramName($station));

View File

@ -10,5 +10,5 @@ interface AdapterTypeInterface
public function getName(): string;
public function getClass(): string;
public function getClass(): ?string;
}

View File

@ -7,7 +7,6 @@ declare(strict_types=1);
namespace App\Radio\Enums;
use App\Radio\Backend\Liquidsoap;
use App\Radio\Backend\None;
enum BackendAdapters: string implements AdapterTypeInterface
{
@ -27,14 +26,19 @@ enum BackendAdapters: string implements AdapterTypeInterface
};
}
public function getClass(): string
public function getClass(): ?string
{
return match ($this) {
self::Liquidsoap => Liquidsoap::class,
self::None => None::class,
default => null,
};
}
public function isEnabled(): bool
{
return self::None !== $this;
}
public static function default(): self
{
return self::Liquidsoap;

View File

@ -7,7 +7,6 @@ declare(strict_types=1);
namespace App\Radio\Enums;
use App\Radio\Frontend\Icecast;
use App\Radio\Frontend\Remote;
use App\Radio\Frontend\Shoutcast;
enum FrontendAdapters: string implements AdapterTypeInterface
@ -30,15 +29,33 @@ enum FrontendAdapters: string implements AdapterTypeInterface
};
}
public function getClass(): string
public function getClass(): ?string
{
return match ($this) {
self::Icecast => Icecast::class,
self::Shoutcast => Shoutcast::class,
self::Remote => Remote::class,
default => null
};
}
public function isEnabled(): bool
{
return self::Remote !== $this;
}
public function supportsMounts(): bool
{
return match ($this) {
self::Shoutcast, self::Icecast => true,
default => false
};
}
public function supportsReload(): bool
{
return self::Icecast === $this;
}
public static function default(): self
{
return self::Icecast;

View File

@ -8,8 +8,7 @@ use App\Entity;
use App\Environment;
use App\Http\Router;
use App\Nginx\CustomUrls;
use App\Radio\AbstractAdapter;
use App\Radio\Enums\StreamFormats;
use App\Radio\AbstractLocalAdapter;
use App\Xml\Reader;
use Doctrine\ORM\EntityManagerInterface;
use Exception;
@ -24,7 +23,7 @@ use Psr\Http\Message\UriInterface;
use Psr\Log\LoggerInterface;
use Supervisor\SupervisorInterface;
abstract class AbstractFrontend extends AbstractAdapter
abstract class AbstractFrontend extends AbstractLocalAdapter
{
public function __construct(
Environment $environment,
@ -41,31 +40,6 @@ abstract class AbstractFrontend extends AbstractAdapter
parent::__construct($environment, $em, $supervisor, $dispatcher, $logger, $router);
}
/**
* @return bool Whether the station supports multiple mount points per station
*/
public function supportsMounts(): bool
{
return false;
}
/**
* Get the default mounts when resetting or initializing a station.
*
* @return Entity\StationMount[]
*/
public function getDefaultMounts(Entity\Station $station): array
{
$record = new Entity\StationMount($station);
$record->setName('/radio.mp3');
$record->setIsDefault(true);
$record->setEnableAutodj(true);
$record->setAutodjFormat(StreamFormats::Mp3->value);
$record->setAutodjBitrate(128);
return [$record];
}
/**
* @inheritdoc
*/

View File

@ -22,16 +22,6 @@ class Icecast extends AbstractFrontend
public const LOGLEVEL_WARN = 2;
public const LOGLEVEL_ERROR = 1;
public function supportsMounts(): bool
{
return true;
}
public function supportsReload(): bool
{
return true;
}
public function reload(Entity\Station $station): void
{
if ($this->hasCommand($station)) {

View File

@ -1,35 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Radio\Frontend;
use App\Entity;
use GuzzleHttp\Psr7\Uri;
use Psr\Http\Message\UriInterface;
class Remote extends AbstractFrontend
{
public function isInstalled(): bool
{
return true;
}
public function getStreamUrl(Entity\Station $station, UriInterface $base_url = null): UriInterface
{
return new Uri('');
}
/**
* @inheritDoc
*/
public function getStreamUrls(Entity\Station $station, UriInterface $base_url = null): array
{
return [];
}
public function getAdminUrl(Entity\Station $station, UriInterface $base_url = null): UriInterface
{
return new Uri('');
}
}

View File

@ -13,11 +13,6 @@ use Symfony\Component\Process\Process;
class Shoutcast extends AbstractFrontend
{
public function supportsMounts(): bool
{
return true;
}
/**
* @inheritDoc
*/

View File

@ -1,26 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Radio\Remote;
use App\Entity;
class AdapterProxy
{
public function __construct(
protected AbstractRemote $adapter,
protected Entity\StationRemote $remote
) {
}
public function getAdapter(): AbstractRemote
{
return $this->adapter;
}
public function getRemote(): Entity\StationRemote
{
return $this->remote;
}
}

View File

@ -176,13 +176,13 @@ final class Acme
$this->nginx->reload();
foreach ($this->stationRepo->iterateEnabledStations() as $station) {
if (!$station->getHasStarted()) {
$frontendType = $station->getFrontendTypeEnum();
if (!$station->getHasStarted() || !$frontendType->supportsReload()) {
continue;
}
$frontend = $this->adapters->getFrontendAdapter($station);
if ($frontend->supportsReload() && $frontend->isRunning($station)) {
if (null !== $frontend && $frontend->isRunning($station)) {
$frontend->reload($station);
}
}

View File

@ -63,15 +63,11 @@ class NowPlayingTask implements NowPlayingTaskInterface, EventSubscriberInterfac
$include_clients = $this->settingsRepo->readSettings()->isAnalyticsEnabled();
$frontend_adapter = $this->adapters->getFrontendAdapter($station);
$remote_adapters = $this->adapters->getRemoteAdapters($station);
// Build the new "raw" NowPlaying data.
try {
$event = new GenerateRawNowPlaying(
$this->adapters,
$station,
$frontend_adapter,
$remote_adapters,
$include_clients
);
$this->eventDispatcher->dispatch($event);
@ -121,15 +117,13 @@ class NowPlayingTask implements NowPlayingTaskInterface, EventSubscriberInterfac
public function loadRawFromFrontend(GenerateRawNowPlaying $event): void
{
try {
$result = $event
->getFrontend()
->getNowPlaying($event->getStation(), $event->includeClients());
$result = $event->getFrontend()?->getNowPlaying($event->getStation(), $event->includeClients());
if (null !== $result) {
$event->setResult($result);
}
} catch (Exception $e) {
$this->logger->error(sprintf('NowPlaying adapter error: %s', $e->getMessage()));
return;
}
$event->setResult($result);
}
public function addToRawFromRemotes(GenerateRawNowPlaying $event): void
@ -137,11 +131,11 @@ class NowPlayingTask implements NowPlayingTaskInterface, EventSubscriberInterfac
$result = $event->getResult();
// Loop through all remotes and update NP data accordingly.
foreach ($event->getRemotes() as $ra_proxy) {
foreach ($event->getRemotes() as [$remote, $adapter]) {
try {
$result = $ra_proxy->getAdapter()->updateNowPlaying(
$result = $adapter->updateNowPlaying(
$result,
$ra_proxy->getRemote(),
$remote,
$event->includeClients()
);
} catch (Exception $e) {