Merge commit 'e6397a893da04c0a28c2306d39233b8a072e54e0'

This commit is contained in:
Buster "Silver Eagle" Neece 2022-01-07 02:26:40 -06:00
parent abaa4a0985
commit b23f28ab2a
No known key found for this signature in database
GPG Key ID: 9FC8B9E008872109
113 changed files with 1684 additions and 1170 deletions

View File

@ -70,20 +70,19 @@ return [
return '$(function () { ' . implode('', $notifies) . ' });';
},
function (Request $request) {
/** @var App\Locale|null $locale */
$localeObj = $request->getAttribute(ServerRequest::ATTR_LOCALE);
$locale = ($localeObj instanceof App\Locale)
? (string)$localeObj
: App\Locale::DEFAULT_LOCALE;
$locale = ($localeObj instanceof App\Enums\SupportedLocales)
? $localeObj->value
: App\Enums\SupportedLocales::default()->value;
$locale = explode('.', $locale, 2)[0];
$localeShort = substr($locale, 0, 2);
$localeWithDashes = str_replace('_', '-', $locale);
$app = [
'lang' => [
'confirm' => __('Are you sure?'),
'lang' => [
'confirm' => __('Are you sure?'),
'advanced' => __('Advanced'),
],
'locale' => $locale,

View File

@ -3,7 +3,7 @@
* Administrative dashboard configuration.
*/
use App\Acl;
use App\Enums\GlobalPermissions;
return function (App\Event\BuildAdminMenu $e) {
$request = $e->getRequest();
@ -18,32 +18,32 @@ return function (App\Event\BuildAdminMenu $e) {
'settings' => [
'label' => __('System Settings'),
'url' => (string)$router->named('admin:settings:index'),
'permission' => Acl::GLOBAL_SETTINGS,
'permission' => GlobalPermissions::Settings,
],
'branding' => [
'label' => __('Custom Branding'),
'url' => (string)$router->named('admin:branding:index'),
'permission' => Acl::GLOBAL_SETTINGS,
'permission' => GlobalPermissions::Settings,
],
'logs' => [
'label' => __('System Logs'),
'url' => (string)$router->named('admin:logs:index'),
'permission' => Acl::GLOBAL_LOGS,
'permission' => GlobalPermissions::Logs,
],
'storage_locations' => [
'label' => __('Storage Locations'),
'url' => (string)$router->named('admin:storage_locations:index'),
'permission' => Acl::GLOBAL_STORAGE_LOCATIONS,
'permission' => GlobalPermissions::StorageLocations,
],
'backups' => [
'label' => __('Backups'),
'url' => (string)$router->named('admin:backups:index'),
'permission' => Acl::GLOBAL_BACKUPS,
'permission' => GlobalPermissions::Backups,
],
'debug' => [
'label' => __('System Debugger'),
'url' => (string)$router->named('admin:debug:index'),
'permission' => Acl::GLOBAL_ALL,
'permission' => GlobalPermissions::All,
],
],
],
@ -54,22 +54,22 @@ return function (App\Event\BuildAdminMenu $e) {
'manage_users' => [
'label' => __('User Accounts'),
'url' => (string)$router->named('admin:users:index'),
'permission' => Acl::GLOBAL_ALL,
'permission' => GlobalPermissions::All,
],
'permissions' => [
'label' => __('Roles & Permissions'),
'url' => (string)$router->named('admin:permissions:index'),
'permission' => Acl::GLOBAL_ALL,
'permission' => GlobalPermissions::All,
],
'auditlog' => [
'label' => __('Audit Log'),
'url' => (string)$router->named('admin:auditlog:index'),
'permission' => Acl::GLOBAL_LOGS,
'permission' => GlobalPermissions::Logs,
],
'api_keys' => [
'label' => __('API Keys'),
'url' => (string)$router->named('admin:api:index'),
'permission' => Acl::GLOBAL_API_KEYS,
'permission' => GlobalPermissions::ApiKeys,
],
],
],
@ -80,27 +80,27 @@ return function (App\Event\BuildAdminMenu $e) {
'manage_stations' => [
'label' => __('Stations'),
'url' => (string)$router->named('admin:stations:index'),
'permission' => Acl::GLOBAL_STATIONS,
'permission' => GlobalPermissions::Stations,
],
'custom_fields' => [
'label' => __('Custom Fields'),
'url' => (string)$router->named('admin:custom_fields:index'),
'permission' => Acl::GLOBAL_CUSTOM_FIELDS,
'permission' => GlobalPermissions::CustomFields,
],
'relays' => [
'label' => __('Connected AzuraRelays'),
'url' => (string)$router->named('admin:relays:index'),
'permission' => Acl::GLOBAL_STATIONS,
'permission' => GlobalPermissions::Stations,
],
'shoutcast' => [
'label' => __('Install SHOUTcast'),
'url' => (string)$router->named('admin:install_shoutcast:index'),
'permission' => Acl::GLOBAL_ALL,
'permission' => GlobalPermissions::All,
],
'geolite' => [
'label' => __('Install GeoLite IP Database'),
'url' => (string)$router->named('admin:install_geolite:index'),
'permission' => Acl::GLOBAL_ALL,
'permission' => GlobalPermissions::All,
],
],
],

View File

@ -3,7 +3,7 @@
* Administrative dashboard configuration.
*/
use App\Acl;
use App\Enums\StationPermissions;
return function (App\Event\BuildStationMenu $e) {
$request = $e->getRequest();
@ -25,7 +25,7 @@ return function (App\Event\BuildStationMenu $e) {
'class' => 'api-call text-success',
'confirm' => __('Restart broadcasting? This will disconnect any current listeners.'),
'visible' => !$station->getHasStarted(),
'permission' => Acl::STATION_BROADCASTING,
'permission' => StationPermissions::Broadcasting,
],
'restart_station' => [
'label' => __('Restart to Apply Changes'),
@ -36,7 +36,7 @@ return function (App\Event\BuildStationMenu $e) {
. (!$station->getNeedsRestart() ? 'd-none' : ''),
'confirm' => __('Restart broadcasting? This will disconnect any current listeners.'),
'visible' => $station->getHasStarted(),
'permission' => Acl::STATION_BROADCASTING,
'permission' => StationPermissions::Broadcasting,
],
'profile' => [
'label' => __('Profile'),
@ -62,27 +62,27 @@ return function (App\Event\BuildStationMenu $e) {
'icon' => 'library_music',
'url' => (string)$router->fromHere('stations:files:index'),
'visible' => $backend->supportsMedia(),
'permission' => Acl::STATION_MEDIA,
'permission' => StationPermissions::Media,
],
'playlists' => [
'label' => __('Playlists'),
'icon' => 'queue_music',
'url' => (string)$router->fromHere('stations:playlists:index'),
'visible' => $backend->supportsMedia(),
'permission' => Acl::STATION_MEDIA,
'permission' => StationPermissions::Media,
],
'podcasts' => [
'label' => __('Podcasts'),
'icon' => 'cast',
'url' => (string)$router->fromHere('stations:podcasts:index'),
'permission' => Acl::STATION_PODCASTS,
'permission' => StationPermissions::Podcasts,
],
'streamers' => [
'label' => __('Streamer/DJ Accounts'),
'icon' => 'mic',
'url' => (string)$router->fromHere('stations:streamers:index'),
'visible' => $backend->supportsStreamers(),
'permission' => Acl::STATION_STREAMERS,
'permission' => StationPermissions::Streamers,
],
'web_dj' => [
'label' => __('Web DJ'),
@ -97,24 +97,24 @@ return function (App\Event\BuildStationMenu $e) {
'icon' => 'wifi_tethering',
'url' => (string)$router->fromHere('stations:mounts:index'),
'visible' => $frontend->supportsMounts(),
'permission' => Acl::STATION_MOUNTS,
'permission' => StationPermissions::MountPoints,
],
'remotes' => [
'label' => __('Remote Relays'),
'icon' => 'router',
'url' => (string)$router->fromHere('stations:remotes:index'),
'permission' => Acl::STATION_REMOTES,
'permission' => StationPermissions::RemoteRelays,
],
'webhooks' => [
'label' => __('Web Hooks'),
'icon' => 'code',
'url' => (string)$router->fromHere('stations:webhooks:index'),
'permission' => Acl::STATION_WEB_HOOKS,
'permission' => StationPermissions::WebHooks,
],
'reports' => [
'label' => __('Reports'),
'icon' => 'assignment',
'permission' => Acl::STATION_REPORTS,
'permission' => StationPermissions::Reports,
'items' => [
'reports_overview' => [
'label' => __('Statistics Overview'),
@ -162,37 +162,37 @@ return function (App\Event\BuildStationMenu $e) {
'label' => __('SFTP Users'),
'url' => (string)$router->fromHere('stations:sftp_users:index'),
'visible' => App\Service\SftpGo::isSupportedForStation($station),
'permission' => Acl::STATION_MEDIA,
'permission' => StationPermissions::Media,
],
'automation' => [
'label' => __('Automated Assignment'),
'url' => (string)$router->fromHere('stations:automation:index'),
'visible' => $backend->supportsMedia(),
'permission' => Acl::STATION_AUTOMATION,
'permission' => StationPermissions::Automation,
],
'ls_config' => [
'label' => __('Edit Liquidsoap Configuration'),
'url' => (string)$router->fromHere('stations:util:ls_config'),
'visible' => $settings->getEnableAdvancedFeatures()
&& $backend instanceof App\Radio\Backend\Liquidsoap,
'permission' => Acl::STATION_BROADCASTING,
'permission' => StationPermissions::Broadcasting,
],
'logs' => [
'label' => __('Log Viewer'),
'url' => (string)$router->fromHere('stations:logs:index'),
'permission' => Acl::STATION_LOGS,
'permission' => StationPermissions::Logs,
],
'queue' => [
'label' => __('Upcoming Song Queue'),
'url' => (string)$router->fromHere('stations:queue:index'),
'permission' => Acl::STATION_BROADCASTING,
'permission' => StationPermissions::Broadcasting,
],
'restart' => [
'label' => __('Restart Broadcasting'),
'url' => (string)$router->fromHere('api:stations:restart'),
'class' => 'api-call',
'confirm' => __('Restart broadcasting? This will disconnect any current listeners.'),
'permission' => Acl::STATION_BROADCASTING,
'permission' => StationPermissions::Broadcasting,
],
],
],

View File

@ -1,7 +1,7 @@
<?php
use App\Acl;
use App\Controller;
use App\Enums\GlobalPermissions;
use App\Middleware;
use Slim\Routing\RouteCollectorProxy;
@ -55,7 +55,7 @@ return static function (RouteCollectorProxy $app) {
}
)->add(Middleware\GetStation::class);
}
)->add(new Middleware\Permissions(Acl::GLOBAL_ALL));
)->add(new Middleware\Permissions(GlobalPermissions::All));
$group->group(
'/install',
@ -66,27 +66,27 @@ return static function (RouteCollectorProxy $app) {
$group->get('/geolite', Controller\Admin\GeoLiteAction::class)
->setName('admin:install_geolite:index');
}
)->add(new Middleware\Permissions(Acl::GLOBAL_SETTINGS));
)->add(new Middleware\Permissions(GlobalPermissions::Settings));
$group->get('/auditlog', Controller\Admin\AuditLogAction::class)
->setName('admin:auditlog:index')
->add(new Middleware\Permissions(Acl::GLOBAL_LOGS));
->add(new Middleware\Permissions(GlobalPermissions::Logs));
$group->get('/api-keys', Controller\Admin\ApiKeysAction::class)
->setName('admin:api:index')
->add(new Middleware\Permissions(Acl::GLOBAL_API_KEYS));
->add(new Middleware\Permissions(GlobalPermissions::ApiKeys));
$group->get('/backups', Controller\Admin\BackupsAction::class)
->setName('admin:backups:index')
->add(new Middleware\Permissions(Acl::GLOBAL_BACKUPS));
->add(new Middleware\Permissions(GlobalPermissions::Backups));
$group->get('/branding', Controller\Admin\BrandingAction::class)
->setName('admin:branding:index')
->add(new Middleware\Permissions(Acl::GLOBAL_SETTINGS));
->add(new Middleware\Permissions(GlobalPermissions::Settings));
$group->get('/custom_fields', Controller\Admin\CustomFieldsAction::class)
->setName('admin:custom_fields:index')
->add(new Middleware\Permissions(Acl::GLOBAL_CUSTOM_FIELDS));
->add(new Middleware\Permissions(GlobalPermissions::CustomFields));
$group->group(
'/logs',
@ -98,35 +98,35 @@ return static function (RouteCollectorProxy $app) {
->setName('admin:logs:view')
->add(Middleware\GetStation::class);
}
)->add(new Middleware\Permissions(Acl::GLOBAL_LOGS));
)->add(new Middleware\Permissions(GlobalPermissions::Logs));
$group->get('/permissions', Controller\Admin\PermissionsAction::class)
->setName('admin:permissions:index')
->add(new Middleware\Permissions(Acl::GLOBAL_ALL));
->add(new Middleware\Permissions(GlobalPermissions::All));
$group->get('/relays', Controller\Admin\RelaysAction::class)
->setName('admin:relays:index')
->add(new Middleware\Permissions(Acl::GLOBAL_STATIONS));
->add(new Middleware\Permissions(GlobalPermissions::Stations));
$group->map(['GET', 'POST'], '/settings', Controller\Admin\SettingsAction::class)
->setName('admin:settings:index')
->add(new Middleware\Permissions(Acl::GLOBAL_SETTINGS));
->add(new Middleware\Permissions(GlobalPermissions::Settings));
$group->get('/stations', Controller\Admin\StationsAction::class)
->setName('admin:stations:index')
->add(new Middleware\Permissions(Acl::GLOBAL_STATIONS));
->add(new Middleware\Permissions(GlobalPermissions::Stations));
$group->get('/storage_locations', Controller\Admin\StorageLocationsAction::class)
->setName('admin:storage_locations:index')
->add(new Middleware\Permissions(Acl::GLOBAL_STORAGE_LOCATIONS));
->add(new Middleware\Permissions(GlobalPermissions::StorageLocations));
$group->get('/users', Controller\Admin\UsersAction::class)
->setName('admin:users:index')
->add(new Middleware\Permissions(Acl::GLOBAL_ALL));
->add(new Middleware\Permissions(GlobalPermissions::All));
}
)
->add(Middleware\Module\Admin::class)
->add(Middleware\EnableView::class)
->add(new Middleware\Permissions(Acl::GLOBAL_VIEW))
->add(new Middleware\Permissions(GlobalPermissions::View))
->add(Middleware\RequireLogin::class);
};

View File

@ -1,7 +1,7 @@
<?php
use App\Acl;
use App\Controller;
use App\Enums\GlobalPermissions;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Middleware;
@ -29,11 +29,11 @@ return static function (RouteCollectorProxy $group) {
Controller\Api\Admin\ApiKeysController::class . ':deleteAction'
);
}
)->add(new Middleware\Permissions(Acl::GLOBAL_API_KEYS));
)->add(new Middleware\Permissions(GlobalPermissions::ApiKeys));
$group->get('/auditlog', Controller\Api\Admin\AuditLogAction::class)
->setName('api:admin:auditlog')
->add(new Middleware\Permissions(Acl::GLOBAL_LOGS));
->add(new Middleware\Permissions(GlobalPermissions::Logs));
$group->group(
'/backups',
@ -53,10 +53,10 @@ return static function (RouteCollectorProxy $group) {
$group->delete('/delete/{path}', Controller\Api\Admin\Backups\DeleteAction::class)
->setName('api:admin:backups:delete');
}
)->add(new Middleware\Permissions(Acl::GLOBAL_BACKUPS));
)->add(new Middleware\Permissions(GlobalPermissions::Backups));
$group->get('/permissions', Controller\Api\Admin\PermissionsController::class)
->add(new Middleware\Permissions(Acl::GLOBAL_ALL));
->add(new Middleware\Permissions(GlobalPermissions::All));
$group->map(
['GET', 'POST'],
@ -115,23 +115,23 @@ return static function (RouteCollectorProxy $group) {
Controller\Api\Admin\Shoutcast\PostAction::class
);
}
)->add(new Middleware\Permissions(Acl::GLOBAL_SETTINGS));
)->add(new Middleware\Permissions(GlobalPermissions::Settings));
$admin_api_endpoints = [
[
'custom_field',
'custom_fields',
Controller\Api\Admin\CustomFieldsController::class,
Acl::GLOBAL_CUSTOM_FIELDS,
GlobalPermissions::CustomFields,
],
['role', 'roles', Controller\Api\Admin\RolesController::class, Acl::GLOBAL_ALL],
['station', 'stations', Controller\Api\Admin\StationsController::class, Acl::GLOBAL_STATIONS],
['user', 'users', Controller\Api\Admin\UsersController::class, Acl::GLOBAL_ALL],
['role', 'roles', Controller\Api\Admin\RolesController::class, GlobalPermissions::All],
['station', 'stations', Controller\Api\Admin\StationsController::class, GlobalPermissions::Stations],
['user', 'users', Controller\Api\Admin\UsersController::class, GlobalPermissions::All],
[
'storage_location',
'storage_locations',
Controller\Api\Admin\StorageLocationsController::class,
Acl::GLOBAL_STORAGE_LOCATIONS,
GlobalPermissions::StorageLocations,
],
];
@ -153,13 +153,13 @@ return static function (RouteCollectorProxy $group) {
$group->post('/station/{id}/clone', Controller\Api\Admin\Stations\CloneAction::class)
->setName('api:admin:station:clone')
->add(new Middleware\Permissions(Acl::GLOBAL_STATIONS));
->add(new Middleware\Permissions(GlobalPermissions::Stations));
$group->get(
'/stations/storage-locations',
Controller\Api\Admin\Stations\StorageLocationsAction::class
)->setName('api:admin:stations:storage-locations')
->add(new Middleware\Permissions(Acl::GLOBAL_STATIONS));
->add(new Middleware\Permissions(GlobalPermissions::Stations));
}
);
};

View File

@ -1,7 +1,7 @@
<?php
use App\Acl;
use App\Controller;
use App\Enums\StationPermissions;
use App\Middleware;
use Slim\Routing\RouteCollectorProxy;
@ -31,7 +31,7 @@ return static function (RouteCollectorProxy $group) {
Controller\Api\Stations\Automation\RunAction::class
)->setName('api:stations:automation:run');
}
)->add(new Middleware\Permissions(Acl::STATION_AUTOMATION, true));
)->add(new Middleware\Permissions(StationPermissions::Automation, true));
$group->get('/nowplaying', Controller\Api\NowPlayingAction::class . ':indexAction');
@ -40,33 +40,33 @@ return static function (RouteCollectorProxy $group) {
'/nowplaying/update',
Controller\Api\Stations\UpdateMetadataAction::class
)
->add(new Middleware\Permissions(Acl::STATION_BROADCASTING, true));
->add(new Middleware\Permissions(StationPermissions::Broadcasting, true));
$group->get('/profile', Controller\Api\Stations\ProfileAction::class)
->setName('api:stations:profile')
->add(new Middleware\Permissions(Acl::STATION_VIEW, true));
->add(new Middleware\Permissions(StationPermissions::View, true));
$group->get(
'/profile/edit',
Controller\Api\Stations\ProfileEditController::class . ':getProfileAction'
)->setName('api:stations:profile:edit')
->add(new Middleware\Permissions(Acl::STATION_PROFILE, true));
->add(new Middleware\Permissions(StationPermissions::Profile, true));
$group->put(
'/profile/edit',
Controller\Api\Stations\ProfileEditController::class . ':putProfileAction'
)->add(new Middleware\Permissions(Acl::STATION_PROFILE, true));
)->add(new Middleware\Permissions(StationPermissions::Profile, true));
$group->get('/quota[/{type}]', Controller\Api\Stations\GetQuotaAction::class)
->setName('api:stations:quota')
->add(new Middleware\Permissions(Acl::STATION_VIEW, true));
->add(new Middleware\Permissions(StationPermissions::View, true));
$group->get('/schedule', Controller\Api\Stations\ScheduleAction::class)
->setName('api:stations:schedule');
$group->get('/history', Controller\Api\Stations\HistoryController::class)
->setName('api:stations:history')
->add(new Middleware\Permissions(Acl::STATION_REPORTS, true));
->add(new Middleware\Permissions(StationPermissions::Reports, true));
$group->group(
'/queue',
@ -80,7 +80,7 @@ return static function (RouteCollectorProxy $group) {
$group->delete('/{id}', Controller\Api\Stations\QueueController::class . ':deleteAction')
->setName('api:stations:queue:record');
}
)->add(new Middleware\Permissions(Acl::STATION_BROADCASTING, true));
)->add(new Middleware\Permissions(StationPermissions::Broadcasting, true));
$group->get('/requests', Controller\Api\Stations\RequestsController::class . ':listAction')
->setName('api:requests:list');
@ -102,7 +102,7 @@ return static function (RouteCollectorProxy $group) {
$group->get('/listeners', Controller\Api\Stations\ListenersAction::class)
->setName('api:listeners:index')
->add(new Middleware\Permissions(Acl::STATION_REPORTS, true));
->add(new Middleware\Permissions(StationPermissions::Reports, true));
$group->get(
'/waveform/{media_id:[a-zA-Z0-9\-]+}.json',
@ -117,10 +117,10 @@ return static function (RouteCollectorProxy $group) {
->setName('api:stations:media:art-internal');
$group->post('/art/{media_id:[a-zA-Z0-9]+}', Controller\Api\Stations\Art\PostArtAction::class)
->add(new Middleware\Permissions(Acl::STATION_MEDIA, true));
->add(new Middleware\Permissions(StationPermissions::Media, true));
$group->delete('/art/{media_id:[a-zA-Z0-9]+}', Controller\Api\Stations\Art\DeleteArtAction::class)
->add(new Middleware\Permissions(Acl::STATION_MEDIA, true));
->add(new Middleware\Permissions(StationPermissions::Media, true));
$group->group(
'/liquidsoap-config',
@ -135,7 +135,7 @@ return static function (RouteCollectorProxy $group) {
Controller\Api\Stations\LiquidsoapConfig\PutAction::class
);
}
)->add(new Middleware\Permissions(Acl::STATION_BROADCASTING, true));
)->add(new Middleware\Permissions(StationPermissions::Broadcasting, true));
// Public and private podcast pages
$group->group(
@ -188,7 +188,7 @@ return static function (RouteCollectorProxy $group) {
$group->post('/art', Controller\Api\Stations\Podcasts\Art\PostArtAction::class)
->setName('api:stations:podcasts:new-art');
}
)->add(new Middleware\Permissions(Acl::STATION_PODCASTS, true));
)->add(new Middleware\Permissions(StationPermissions::Podcasts, true));
$group->group(
'/podcast/{podcast_id}',
@ -257,35 +257,50 @@ return static function (RouteCollectorProxy $group) {
}
);
}
)->add(new Middleware\Permissions(Acl::STATION_PODCASTS, true));
)->add(new Middleware\Permissions(StationPermissions::Podcasts, true));
$station_api_endpoints = [
['file', 'files', Controller\Api\Stations\FilesController::class, Acl::STATION_MEDIA],
['mount', 'mounts', Controller\Api\Stations\MountsController::class, Acl::STATION_MOUNTS],
[
'file',
'files',
Controller\Api\Stations\FilesController::class,
StationPermissions::Media,
],
[
'mount',
'mounts',
Controller\Api\Stations\MountsController::class,
StationPermissions::MountPoints,
],
[
'playlist',
'playlists',
Controller\Api\Stations\PlaylistsController::class,
Acl::STATION_MEDIA,
StationPermissions::Media,
],
[
'remote',
'remotes',
Controller\Api\Stations\RemotesController::class,
StationPermissions::RemoteRelays,
],
['remote', 'remotes', Controller\Api\Stations\RemotesController::class, Acl::STATION_REMOTES],
[
'sftp-user',
'sftp-users',
Controller\Api\Stations\SftpUsersController::class,
Acl::STATION_MEDIA,
StationPermissions::Media,
],
[
'streamer',
'streamers',
Controller\Api\Stations\StreamersController::class,
Acl::STATION_STREAMERS,
StationPermissions::Streamers,
],
[
'webhook',
'webhooks',
Controller\Api\Stations\WebhooksController::class,
Acl::STATION_WEB_HOOKS,
StationPermissions::WebHooks,
],
];
@ -337,13 +352,13 @@ return static function (RouteCollectorProxy $group) {
}
)
->add(Middleware\Module\StationFiles::class)
->add(new Middleware\Permissions(Acl::STATION_MEDIA, true));
->add(new Middleware\Permissions(StationPermissions::Media, true));
$group->post(
'/mounts/intro',
Controller\Api\Stations\Mounts\Intro\PostIntroAction::class
)->setName('api:stations:mounts:new-intro')
->add(new Middleware\Permissions(Acl::STATION_MOUNTS, true));
->add(new Middleware\Permissions(StationPermissions::MountPoints, true));
$group->group(
'/mount/{id}',
@ -363,14 +378,14 @@ return static function (RouteCollectorProxy $group) {
Controller\Api\Stations\Mounts\Intro\DeleteIntroAction::class
);
}
)->add(new Middleware\Permissions(Acl::STATION_MOUNTS, true));
)->add(new Middleware\Permissions(StationPermissions::MountPoints, true));
$group->get(
'/playlists/schedule',
Controller\Api\Stations\PlaylistsController::class . ':scheduleAction'
)
->setName('api:stations:playlists:schedule')
->add(new Middleware\Permissions(Acl::STATION_MEDIA, true));
->add(new Middleware\Permissions(StationPermissions::Media, true));
$group->group(
'/playlist/{id}',
@ -420,7 +435,7 @@ return static function (RouteCollectorProxy $group) {
Controller\Api\Stations\Playlists\ExportAction::class
)->setName('api:stations:playlist:export');
}
)->add(new Middleware\Permissions(Acl::STATION_MEDIA, true));
)->add(new Middleware\Permissions(StationPermissions::Media, true));
$group->group(
'/reports',
@ -443,7 +458,7 @@ return static function (RouteCollectorProxy $group) {
Controller\Api\Stations\Reports\RequestsController::class . ':deleteAction'
)->setName('api:stations:reports:requests:delete');
}
)->add(new Middleware\Permissions(Acl::STATION_BROADCASTING, true));
)->add(new Middleware\Permissions(StationPermissions::Broadcasting, true));
$group->get(
'/performance',
@ -470,21 +485,21 @@ return static function (RouteCollectorProxy $group) {
Controller\Api\Stations\Reports\SoundExchangeAction::class
)->setName('api:stations:reports:soundexchange');
}
)->add(new Middleware\Permissions(Acl::STATION_REPORTS, true));
)->add(new Middleware\Permissions(StationPermissions::Reports, true));
$group->get(
'/streamers/schedule',
Controller\Api\Stations\StreamersController::class . ':scheduleAction'
)
->setName('api:stations:streamers:schedule')
->add(new Middleware\Permissions(Acl::STATION_STREAMERS, true));
->add(new Middleware\Permissions(StationPermissions::Streamers, true));
$group->get(
'/streamers/broadcasts',
Controller\Api\Stations\Streamers\BroadcastsController::class . ':listAction'
)
->setName('api:stations:streamers:broadcasts')
->add(new Middleware\Permissions(Acl::STATION_STREAMERS, true));
->add(new Middleware\Permissions(StationPermissions::Streamers, true));
$group->group(
'/streamer/{id}',
@ -507,30 +522,30 @@ return static function (RouteCollectorProxy $group) {
)
->setName('api:stations:streamer:broadcast:delete');
}
)->add(new Middleware\Permissions(Acl::STATION_STREAMERS, true));
)->add(new Middleware\Permissions(StationPermissions::Streamers, true));
$group->get('/restart-status', Controller\Api\Stations\GetRestartStatusAction::class)
->setName('api:stations:restart-status')
->add(new Middleware\Permissions(Acl::STATION_VIEW, true));
->add(new Middleware\Permissions(StationPermissions::View, true));
$group->get('/status', Controller\Api\Stations\ServicesController::class . ':statusAction')
->setName('api:stations:status')
->add(new Middleware\Permissions(Acl::STATION_VIEW, true));
->add(new Middleware\Permissions(StationPermissions::View, true));
$group->post('/backend/{do}', Controller\Api\Stations\ServicesController::class . ':backendAction')
->setName('api:stations:backend')
->add(new Middleware\Permissions(Acl::STATION_BROADCASTING, true));
->add(new Middleware\Permissions(StationPermissions::Broadcasting, true));
$group->post(
'/frontend/{do}',
Controller\Api\Stations\ServicesController::class . ':frontendAction'
)
->setName('api:stations:frontend')
->add(new Middleware\Permissions(Acl::STATION_BROADCASTING, true));
->add(new Middleware\Permissions(StationPermissions::Broadcasting, true));
$group->post('/restart', Controller\Api\Stations\ServicesController::class . ':restartAction')
->setName('api:stations:restart')
->add(new Middleware\Permissions(Acl::STATION_BROADCASTING, true));
->add(new Middleware\Permissions(StationPermissions::Broadcasting, true));
$group->group(
'/webhook/{id}',
@ -550,7 +565,7 @@ return static function (RouteCollectorProxy $group) {
Controller\Api\Stations\Webhooks\TestLogAction::class
)->setName('api:stations:webhook:test-log');
}
)->add(new Middleware\Permissions(Acl::STATION_WEB_HOOKS, true));
)->add(new Middleware\Permissions(StationPermissions::WebHooks, true));
}
)->add(Middleware\RequireStation::class)
->add(Middleware\GetStation::class);

View File

@ -1,6 +1,7 @@
<?php
use App\Controller;
use App\Enums\GlobalPermissions;
use App\Middleware;
use Slim\Routing\RouteCollectorProxy;
@ -19,7 +20,7 @@ return static function (RouteCollectorProxy $app) {
$group->get('/login-as/{id}/{csrf}', Controller\Frontend\Account\MasqueradeAction::class)
->setName('account:masquerade')
->add(new Middleware\Permissions(App\Acl::GLOBAL_ALL));
->add(new Middleware\Permissions(GlobalPermissions::All));
$group->get('/endsession', Controller\Frontend\Account\EndMasqueradeAction::class)
->setName('account:endmasquerade');

View File

@ -1,7 +1,7 @@
<?php
use App\Acl;
use App\Controller;
use App\Enums\StationPermissions;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Middleware;
@ -24,15 +24,15 @@ return static function (RouteCollectorProxy $app) {
'/automation',
Controller\Stations\AutomationAction::class
)->setName('stations:automation:index')
->add(new Middleware\Permissions(Acl::STATION_AUTOMATION, true));
->add(new Middleware\Permissions(StationPermissions::Automation, true));
$group->get('/files', Controller\Stations\FilesAction::class)
->setName('stations:files:index')
->add(new Middleware\Permissions(Acl::STATION_MEDIA, true));
->add(new Middleware\Permissions(StationPermissions::Media, true));
$group->get('/ls_config', Controller\Stations\EditLiquidsoapConfigAction::class)
->setName('stations:util:ls_config')
->add(new Middleware\Permissions(Acl::STATION_BROADCASTING, true));
->add(new Middleware\Permissions(StationPermissions::Broadcasting, true));
$group->group(
'/logs',
@ -43,19 +43,19 @@ return static function (RouteCollectorProxy $app) {
$group->get('/view/{log}', Controller\Stations\LogsController::class . ':viewAction')
->setName('stations:logs:view');
}
)->add(new Middleware\Permissions(Acl::STATION_LOGS, true));
)->add(new Middleware\Permissions(StationPermissions::Logs, true));
$group->get('/playlists', Controller\Stations\PlaylistsAction::class)
->setName('stations:playlists:index')
->add(new Middleware\Permissions(Acl::STATION_MEDIA, true));
->add(new Middleware\Permissions(StationPermissions::Media, true));
$group->get('/podcasts', Controller\Stations\PodcastsAction::class)
->setName('stations:podcasts:index')
->add(new Middleware\Permissions(Acl::STATION_PODCASTS, true));
->add(new Middleware\Permissions(StationPermissions::Podcasts, true));
$group->get('/mounts', Controller\Stations\MountsAction::class)
->setName('stations:mounts:index')
->add(new Middleware\Permissions(Acl::STATION_MOUNTS, true));
->add(new Middleware\Permissions(StationPermissions::MountPoints, true));
$group->get('/profile', Controller\Stations\ProfileController::class)
->setName('stations:profile:index');
@ -65,19 +65,19 @@ return static function (RouteCollectorProxy $app) {
Controller\Stations\ProfileController::class . ':toggleAction'
)
->setName('stations:profile:toggle')
->add(new Middleware\Permissions(Acl::STATION_PROFILE, true));
->add(new Middleware\Permissions(StationPermissions::Profile, true));
$group->get('/profile/edit', Controller\Stations\ProfileController::class . ':editAction')
->setName('stations:profile:edit')
->add(new Middleware\Permissions(Acl::STATION_PROFILE, true));
->add(new Middleware\Permissions(StationPermissions::Profile, true));
$group->get('/queue', Controller\Stations\QueueAction::class)
->setName('stations:queue:index')
->add(new Middleware\Permissions(Acl::STATION_BROADCASTING, true));
->add(new Middleware\Permissions(StationPermissions::Broadcasting, true));
$group->get('/remotes', Controller\Stations\RemotesAction::class)
->setName('stations:remotes:index')
->add(new Middleware\Permissions(Acl::STATION_REMOTES, true));
->add(new Middleware\Permissions(StationPermissions::RemoteRelays, true));
$group->group(
'/reports',
@ -106,23 +106,23 @@ return static function (RouteCollectorProxy $app) {
$group->get('/requests', Controller\Stations\Reports\RequestsAction::class)
->setName('stations:reports:requests');
}
)->add(new Middleware\Permissions(Acl::STATION_REPORTS, true));
)->add(new Middleware\Permissions(StationPermissions::Reports, true));
$group->get('/sftp_users', Controller\Stations\SftpUsersAction::class)
->setName('stations:sftp_users:index')
->add(new Middleware\Permissions(Acl::STATION_MEDIA, true));
->add(new Middleware\Permissions(StationPermissions::Media, true));
$group->get('/streamers', Controller\Stations\StreamersAction::class)
->setName('stations:streamers:index')
->add(new Middleware\Permissions(Acl::STATION_STREAMERS, true));
->add(new Middleware\Permissions(StationPermissions::Streamers, true));
$group->get('/webhooks', Controller\Stations\WebhooksAction::class)
->setName('stations:webhooks:index')
->add(new Middleware\Permissions(Acl::STATION_WEB_HOOKS, true));
->add(new Middleware\Permissions(StationPermissions::WebHooks, true));
}
)
->add(Middleware\Module\Stations::class)
->add(new Middleware\Permissions(Acl::STATION_VIEW, true))
->add(new Middleware\Permissions(StationPermissions::View, true))
->add(Middleware\RequireStation::class)
->add(Middleware\GetStation::class)
->add(Middleware\EnableView::class)

View File

@ -5,8 +5,12 @@ declare(strict_types=1);
namespace App;
use App\Entity;
use App\Enums\GlobalPermissions;
use App\Enums\PermissionInterface;
use App\Enums\StationPermissions;
use App\Http\ServerRequest;
use App\Traits\RequestAwareTrait;
use Doctrine\ORM\EntityManagerInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Message\ServerRequestInterface;
@ -17,36 +21,12 @@ class Acl
{
use RequestAwareTrait;
public const GLOBAL_ALL = 'administer all';
public const GLOBAL_VIEW = 'view administration';
public const GLOBAL_LOGS = 'view system logs';
public const GLOBAL_SETTINGS = 'administer settings';
public const GLOBAL_API_KEYS = 'administer api keys';
public const GLOBAL_STATIONS = 'administer stations';
public const GLOBAL_CUSTOM_FIELDS = 'administer custom fields';
public const GLOBAL_BACKUPS = 'administer backups';
public const GLOBAL_STORAGE_LOCATIONS = 'administer storage locations';
public const STATION_ALL = 'administer all';
public const STATION_VIEW = 'view station management';
public const STATION_REPORTS = 'view station reports';
public const STATION_LOGS = 'view station logs';
public const STATION_PROFILE = 'manage station profile';
public const STATION_BROADCASTING = 'manage station broadcasting';
public const STATION_STREAMERS = 'manage station streamers';
public const STATION_MOUNTS = 'manage station mounts';
public const STATION_REMOTES = 'manage station remotes';
public const STATION_MEDIA = 'manage station media';
public const STATION_AUTOMATION = 'manage station automation';
public const STATION_WEB_HOOKS = 'manage station web hooks';
public const STATION_PODCASTS = 'manage station podcasts';
protected array $permissions;
protected ?array $actions;
public function __construct(
protected Entity\Repository\RolePermissionRepository $permissionRepo,
protected EntityManagerInterface $em,
protected EventDispatcherInterface $dispatcher
) {
$this->reload();
@ -57,7 +37,20 @@ class Acl
*/
public function reload(): void
{
$this->actions = $this->permissionRepo->getActionsForAllRoles();
$sql = $this->em->createQuery(
<<<'DQL'
SELECT rp FROM App\Entity\RolePermission rp
DQL
);
$this->actions = [];
foreach ($sql->getArrayResult() as $row) {
if ($row['station_id']) {
$this->actions[$row['role_id']]['stations'][$row['station_id']][] = $row['action_name'];
} else {
$this->actions[$row['role_id']]['global'][] = $row['action_name'];
}
}
}
/**
@ -81,34 +74,17 @@ class Acl
if (!isset($this->permissions)) {
/** @var array<string,array> $permissions */
$permissions = [
'global' => [
self::GLOBAL_ALL => __('All Permissions'),
self::GLOBAL_VIEW => __('View Administration Page'),
self::GLOBAL_LOGS => __('View System Logs'),
self::GLOBAL_SETTINGS => __('Administer Settings'),
self::GLOBAL_API_KEYS => __('Administer API Keys'),
self::GLOBAL_STATIONS => __('Administer Stations'),
self::GLOBAL_CUSTOM_FIELDS => __('Administer Custom Fields'),
self::GLOBAL_BACKUPS => __('Administer Backups'),
self::GLOBAL_STORAGE_LOCATIONS => __('Administer Storage Locations'),
],
'station' => [
self::STATION_ALL => __('All Permissions'),
self::STATION_VIEW => __('View Station Page'),
self::STATION_REPORTS => __('View Station Reports'),
self::STATION_LOGS => __('View Station Logs'),
self::STATION_PROFILE => __('Manage Station Profile'),
self::STATION_BROADCASTING => __('Manage Station Broadcasting'),
self::STATION_STREAMERS => __('Manage Station Streamers'),
self::STATION_MOUNTS => __('Manage Station Mount Points'),
self::STATION_REMOTES => __('Manage Station Remote Relays'),
self::STATION_MEDIA => __('Manage Station Media'),
self::STATION_AUTOMATION => __('Manage Station Automation'),
self::STATION_WEB_HOOKS => __('Manage Station Web Hooks'),
self::STATION_PODCASTS => __('Manage Station Podcasts'),
],
'global' => [],
'station' => [],
];
foreach (GlobalPermissions::cases() as $globalPermission) {
$permissions['global'][$globalPermission->value] = $globalPermission->getName();
}
foreach (StationPermissions::cases() as $stationPermission) {
$permissions['station'][$stationPermission->value] = $stationPermission->getName();
}
$buildPermissionsEvent = new Event\BuildPermissions($permissions);
$this->dispatcher->dispatch($buildPermissionsEvent);
@ -121,11 +97,13 @@ class Acl
/**
* Check if the current user associated with the request has the specified permission.
*
* @param array|string $action
* @param array<string|PermissionInterface>|string|PermissionInterface $action
* @param int|Entity\Station|null $stationId
*/
public function isAllowed(array|string $action, Entity\Station|int $stationId = null): bool
{
public function isAllowed(
array|string|PermissionInterface $action,
Entity\Station|int $stationId = null
): bool {
if ($this->request instanceof ServerRequestInterface) {
$user = $this->request->getAttribute(ServerRequest::ATTR_USER);
return $this->userAllowed($user, $action, $stationId);
@ -138,12 +116,12 @@ class Acl
* Check if a specified User entity is allowed to perform an action (or array of actions).
*
* @param Entity\User|null $user
* @param array|string $action
* @param array<string|PermissionInterface>|string|PermissionInterface $action
* @param int|Entity\Station|null $stationId
*/
public function userAllowed(
?Entity\User $user = null,
array|string $action = null,
array|string|PermissionInterface $action = null,
Entity\Station|int $stationId = null
): bool {
if (null === $user || null === $action) {
@ -154,18 +132,16 @@ class Acl
$stationId = $stationId->getId();
}
$num_roles = $user->getRoles()->count();
if ($num_roles > 0) {
if ($num_roles === 1) {
$numRoles = $user->getRoles()->count();
if ($numRoles > 0) {
if ($numRoles === 1) {
$role = $user->getRoles()->first();
return $this->roleAllowed($role->getId(), $action, $stationId);
}
$roles = [];
if ($user->getRoles()->count() > 0) {
foreach ($user->getRoles() as $role) {
$roles[] = $role->getId();
}
foreach ($user->getRoles() as $role) {
$roles[] = $role->getId();
}
return $this->roleAllowed($roles, $action, $stationId);
@ -178,15 +154,22 @@ class Acl
* Check if a role (or array of roles) is allowed to perform an action (or array of actions).
*
* @param array|int $role_id
* @param array|string $action
* @param array<string|PermissionInterface>|string|PermissionInterface $action
* @param int|Entity\Station|null $station_id
*/
public function roleAllowed(array|int $role_id, array|string $action, Entity\Station|int $station_id = null): bool
{
public function roleAllowed(
array|int $role_id,
array|string|PermissionInterface $action,
Entity\Station|int $station_id = null
): bool {
if ($station_id instanceof Entity\Station) {
$station_id = $station_id->getId();
}
if ($action instanceof PermissionInterface) {
$action = $action->getValue();
}
// Iterate through an array of roles and return with the first "true" response, or "false" otherwise.
if (is_array($role_id)) {
foreach ($role_id as $r) {
@ -212,17 +195,35 @@ class Acl
if (!empty($this->actions[$role_id])) {
$role_actions = (array)$this->actions[$role_id];
if (in_array(self::GLOBAL_ALL, (array)$role_actions['global'], true)) {
if (
in_array(
GlobalPermissions::All->value,
(array)$role_actions['global'],
true
)
) {
return true;
}
if ($station_id !== null) {
if (in_array(self::GLOBAL_STATIONS, (array)$role_actions['global'], true)) {
if (
in_array(
GlobalPermissions::Stations->value,
(array)$role_actions['global'],
true
)
) {
return true;
}
if (!empty($role_actions['stations'][$station_id])) {
if (in_array(self::STATION_ALL, $role_actions['stations'][$station_id], true)) {
if (
in_array(
StationPermissions::All->value,
$role_actions['stations'][$station_id],
true
)
) {
return true;
}

View File

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App;
use App\Console\Application;
use App\Enums\SupportedLocales;
use App\Http\Factory\ResponseFactory;
use App\Http\Factory\ServerRequestFactory;
use Composer\Autoload\ClassLoader;
@ -60,8 +61,8 @@ class AppFactory
self::buildAppFromContainer($di);
$env = $di->get(Environment::class);
$locale = Locale::createForCli($env);
$locale->register();
SupportedLocales::createForCli($env);
return $di->get(Application::class);
}

View File

@ -68,7 +68,7 @@ class BackupCommand extends CommandAbstract
}
$storageLocation = $this->storageLocationRepo->findByType(
Entity\StorageLocation::TYPE_BACKUP,
Entity\Enums\StorageLocationTypes::Backup,
$storageLocationId
);
if (!($storageLocation instanceof Entity\StorageLocation)) {

View File

@ -5,8 +5,8 @@ declare(strict_types=1);
namespace App\Console\Command\Locale;
use App\Console\Command\CommandAbstract;
use App\Enums\SupportedLocales;
use App\Environment;
use App\Locale;
use Gettext\Translation;
use Gettext\Translations;
use Symfony\Component\Console\Attribute\AsCommand;
@ -32,32 +32,34 @@ class ImportCommand extends CommandAbstract
$io = new SymfonyStyle($input, $output);
$io->title('Import Locales');
$locales = Locale::SUPPORTED_LOCALES;
$locale_base = $this->environment->getBaseDirectory() . '/resources/locale';
$localeBase = $this->environment->getBaseDirectory() . '/resources/locale';
$jsTranslations = [];
foreach ($locales as $locale_key => $locale_name) {
if ($locale_key === Locale::DEFAULT_LOCALE) {
$supportedLocales = SupportedLocales::cases();
$defaultLocale = SupportedLocales::default();
foreach ($supportedLocales as $supportedLocale) {
if ($supportedLocale === $defaultLocale) {
continue;
}
$locale_source = $locale_base . '/' . $locale_key . '/LC_MESSAGES/default.po';
$locale_source = $localeBase . '/' . $supportedLocale->value . '/LC_MESSAGES/default.po';
if (is_file($locale_source)) {
$translations = Translations::fromPoFile($locale_source);
// Temporary inclusion of frontend translations
$frontendTranslations = $locale_base . '/' . $locale_key . '/LC_MESSAGES/frontend.po';
$frontendTranslations = $localeBase . '/' . $supportedLocale->value . '/LC_MESSAGES/frontend.po';
if (is_file($frontendTranslations)) {
$frontendTranslations = Translations::fromPoFile($frontendTranslations);
$translations->mergeWith($frontendTranslations);
}
$locale_dest = $locale_base . '/compiled/' . $locale_key . '.php';
$locale_dest = $localeBase . '/compiled/' . $supportedLocale->value . '.php';
$translations->toPhpArrayFile($locale_dest);
$localeJsKey = str_replace('.UTF-8', '', $locale_key);
$localeJsKey = str_replace('.UTF-8', '', $supportedLocale->value);
/** @var Translation $translation */
foreach ($translations as $translation) {
@ -83,7 +85,9 @@ class ImportCommand extends CommandAbstract
ksort($jsTranslations[$localeJsKey]);
$io->writeln(__('Imported locale: %s', $locale_key . ' (' . $locale_name . ')'));
$io->writeln(
__('Imported locale: %s', $supportedLocale->value . ' (' . $supportedLocale->getLocalName() . ')')
);
}
}
@ -91,7 +95,7 @@ class ImportCommand extends CommandAbstract
$jsTranslations,
JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE
);
$jsTranslationsPath = $locale_base . '/translations.json';
$jsTranslationsPath = $localeBase . '/translations.json';
file_put_contents($jsTranslationsPath, $jsTranslations);

View File

@ -6,7 +6,8 @@ namespace App\Controller;
use App\Controller\Api\Traits\HasLogViewer;
use App\Entity;
use App\Radio\Adapters;
use App\Radio\Enums\BackendAdapters;
use App\Radio\Enums\FrontendAdapters;
abstract class AbstractLogViewerController
{
@ -21,8 +22,8 @@ abstract class AbstractLogViewerController
$stationConfigDir = $station->getRadioConfigDir();
switch ($station->getBackendType()) {
case Adapters::BACKEND_LIQUIDSOAP:
switch ($station->getBackendTypeEnum()) {
case BackendAdapters::Liquidsoap:
$log_paths['liquidsoap_log'] = [
'name' => __('Liquidsoap Log'),
'path' => $stationConfigDir . '/liquidsoap.log',
@ -36,8 +37,8 @@ abstract class AbstractLogViewerController
break;
}
switch ($station->getFrontendType()) {
case Adapters::FRONTEND_ICECAST:
switch ($station->getFrontendTypeEnum()) {
case FrontendAdapters::Icecast:
$log_paths['icecast_access_log'] = [
'name' => __('Icecast Access Log'),
'path' => $stationConfigDir . '/icecast_access.log',
@ -55,7 +56,7 @@ abstract class AbstractLogViewerController
];
break;
case Adapters::FRONTEND_SHOUTCAST:
case FrontendAdapters::SHOUTcast:
$log_paths['shoutcast_log'] = [
'name' => __('SHOUTcast Log'),
'path' => $stationConfigDir . '/shoutcast.log',

View File

@ -32,7 +32,9 @@ class BackupsAction
'group' => Entity\Settings::GROUP_BACKUP,
]),
'isDocker' => $environment->isDocker(),
'storageLocations' => $storageLocationRepo->fetchSelectByType(Entity\StorageLocation::TYPE_BACKUP),
'storageLocations' => $storageLocationRepo->fetchSelectByType(
Entity\Enums\StorageLocationTypes::Backup
),
],
);
}

View File

@ -21,7 +21,7 @@ abstract class AbstractFileAction
[$storageLocationId, $path] = explode('|', $pathStr);
$storageLocation = $this->storageLocationRepo->findByType(
Entity\StorageLocation::TYPE_BACKUP,
Entity\Enums\StorageLocationTypes::Backup,
(int)$storageLocationId
);

View File

@ -22,7 +22,7 @@ class GetAction
$router = $request->getRouter();
$backups = [];
$storageLocations = $storageLocationRepo->findAllByType(Entity\StorageLocation::TYPE_BACKUP);
$storageLocations = $storageLocationRepo->findAllByType(Entity\Enums\StorageLocationTypes::Backup);
foreach ($storageLocations as $storageLocation) {
/** @var StorageAttributes $file */

View File

@ -4,11 +4,13 @@ declare(strict_types=1);
namespace App\Controller\Api\Admin;
use App\Acl;
use App\Entity;
use App\Enums\StationPermissions;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Radio\Adapters;
use App\Radio\Enums\FrontendAdapters;
use App\Radio\Enums\RemoteAdapters;
use Doctrine\ORM\EntityManagerInterface;
use OpenApi\Attributes as OA;
use Psr\Http\Message\ResponseInterface;
@ -39,6 +41,7 @@ class RelaysController
protected Adapters $adapters
) {
}
public function __invoke(ServerRequest $request, Response $response): ResponseInterface
{
$stations = $this->getManageableStations($request);
@ -96,7 +99,7 @@ class RelaysController
WHERE s.is_enabled = 1
AND s.frontend_type != :remote_frontend
DQL
)->setParameter('remote_frontend', Adapters::FRONTEND_REMOTE)
)->setParameter('remote_frontend', FrontendAdapters::Remote->value)
->execute();
$acl = $request->getAcl();
@ -104,7 +107,7 @@ class RelaysController
return array_filter(
$all_stations,
static function (Entity\Station $station) use ($acl) {
return $acl->isAllowed(Acl::STATION_BROADCASTING, $station->getId());
return $acl->isAllowed(StationPermissions::Broadcasting, $station->getId());
}
);
}
@ -160,7 +163,7 @@ class RelaysController
}
$remote->setRelay($relay);
$remote->setType(Adapters::REMOTE_AZURARELAY);
$remote->setType(RemoteAdapters::AzuraRelay->value);
$remote->setDisplayName($mount->getDisplayName() . ' (' . $relay->getName() . ')');
$remote->setIsVisibleOnPublicPages($relay->getIsVisibleOnPublicPages());
$remote->setAutodjBitrate($mount->getAutodjBitrate());

View File

@ -21,7 +21,7 @@ class StorageLocationsAction extends StationsController
$newStorageLocationMessage = __('Create a new storage location based on the base directory.');
$storageLocations = [];
foreach (Station::getStorageLocationTypes() as $locationType => $locationKey) {
foreach (Station::getStorageLocationTypes() as $locationKey => $locationType) {
$storageLocations[$locationKey] = $storageLocationRepo->fetchSelectByType(
$locationType,
true,

View File

@ -168,6 +168,7 @@ class StationsController extends AbstractAdminApiCrudController
) {
parent::__construct($reloadableEm, $serializer, $validator);
}
/**
* @param ServerRequest $request
* @param Response $response
@ -195,6 +196,7 @@ class StationsController extends AbstractAdminApiCrudController
return $this->listPaginatedFromQuery($request, $response, $qb->getQuery());
}
protected function viewRecord(object $record, ServerRequest $request): mixed
{
if (!($record instanceof $this->entityClass)) {
@ -226,6 +228,7 @@ class StationsController extends AbstractAdminApiCrudController
return $return;
}
/**
* @param Entity\Station $record
* @param array<string, mixed> $context
@ -243,7 +246,7 @@ class StationsController extends AbstractAdminApiCrudController
'has_started',
];
foreach (Entity\Station::getStorageLocationTypes() as $storageLocationType => $locationKey) {
foreach (Entity\Station::getStorageLocationTypes() as $locationKey => $storageLocationType) {
$context[AbstractNormalizer::CALLBACKS][$locationKey] = fn(
array $value
) => $value['id'];
@ -251,9 +254,10 @@ class StationsController extends AbstractAdminApiCrudController
return parent::toArray($record, $context);
}
protected function fromArray(array $data, ?object $record = null, array $context = []): object
{
foreach (Entity\Station::getStorageLocationTypes() as $locationKey) {
foreach (Entity\Station::getStorageLocationTypes() as $locationKey => $storageLocationType) {
$idKey = $locationKey . '_id';
if (!empty($data[$idKey])) {
$data[$locationKey] = $data[$idKey];
@ -263,6 +267,7 @@ class StationsController extends AbstractAdminApiCrudController
return parent::fromArray($data, $record, $context);
}
/**
* @param array<mixed>|null $data
* @param Entity\Station|null $record
@ -289,6 +294,7 @@ class StationsController extends AbstractAdminApiCrudController
? $this->handleCreate($record)
: $this->handleEdit($record);
}
/**
* @param Entity\Station $record
*/
@ -296,6 +302,7 @@ class StationsController extends AbstractAdminApiCrudController
{
$this->handleDelete($record);
}
protected function handleEdit(Entity\Station $station): Entity\Station
{
$original_record = $this->em->getUnitOfWork()->getOriginalEntityData($station);
@ -333,6 +340,7 @@ class StationsController extends AbstractAdminApiCrudController
return $station;
}
protected function handleCreate(Entity\Station $station): Entity\Station
{
$station->generateAdapterApiKey();
@ -348,6 +356,7 @@ class StationsController extends AbstractAdminApiCrudController
return $station;
}
protected function handleDelete(Entity\Station $station): void
{
$this->configuration->removeConfiguration($station);

View File

@ -4,8 +4,9 @@ declare(strict_types=1);
namespace App\Controller\Api\Frontend\Dashboard;
use App\Acl;
use App\Entity;
use App\Enums\GlobalPermissions;
use App\Enums\StationPermissions;
use App\Http\Response;
use App\Http\ServerRequest;
use Carbon\CarbonImmutable;
@ -31,7 +32,7 @@ class ChartsAction
$acl = $request->getAcl();
// Don't show stations the user can't manage.
$showAdmin = $acl->isAllowed(Acl::GLOBAL_VIEW);
$showAdmin = $acl->isAllowed(GlobalPermissions::View);
/** @var Entity\Station[] $stations */
$stations = array_filter(
@ -39,7 +40,7 @@ class ChartsAction
static function ($station) use ($acl) {
/** @var Entity\Station $station */
return $station->getIsEnabled() &&
$acl->isAllowed(Acl::STATION_VIEW, $station->getId());
$acl->isAllowed(StationPermissions::View, $station->getId());
}
);

View File

@ -4,8 +4,8 @@ declare(strict_types=1);
namespace App\Controller\Api\Frontend\Dashboard;
use App\Acl;
use App\Entity;
use App\Enums\StationPermissions;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Paginator;
@ -29,7 +29,7 @@ class StationsAction
static function ($station) use ($acl) {
/** @var Entity\Station $station */
return $station->getIsEnabled() &&
$acl->isAllowed(Acl::STATION_VIEW, $station->getId());
$acl->isAllowed(StationPermissions::View, $station->getId());
}
);

View File

@ -4,11 +4,11 @@ declare(strict_types=1);
namespace App\Controller\Api;
use App\Acl;
use App\Enums\StationPermissions;
use App\Enums\SupportedLocales;
use App\Exception\PermissionDeniedException;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Locale;
use App\Radio\AutoDJ;
use App\Radio\Backend\Liquidsoap;
use App\Service\IpGeolocation;
@ -66,7 +66,7 @@ class InternalController
$station = $request->getStation();
$acl = $request->getAcl();
if ($acl->isAllowed(Acl::GLOBAL_VIEW, $station->getId())) {
if ($acl->isAllowed(StationPermissions::View, $station->getId())) {
return;
}
@ -182,7 +182,7 @@ class InternalController
}
$listenerIp = $request->getParam('ip') ?? '';
$listenerLocation = $this->ipGeolocation->getLocationInfo($listenerIp, Locale::DEFAULT_LOCALE);
$listenerLocation = $this->ipGeolocation->getLocationInfo($listenerIp, SupportedLocales::default());
$allowedIps = $frontendConfig->getAllowedIps();
if (!empty($allowedIps)) {
@ -209,7 +209,7 @@ class InternalController
if ('success' === $listenerLocation['status']) {
$listenerCountry = $listenerLocation['country'];
$countries = Countries::getNames(Locale::DEFAULT_LOCALE);
$countries = Countries::getNames(SupportedLocales::default()->name);
$listenerCountryCode = '';
foreach ($countries as $countryCode => $countryName) {

View File

@ -20,14 +20,17 @@ class GetQuotaAction
public function __invoke(
ServerRequest $request,
Response $response,
string $type = Entity\StorageLocation::TYPE_STATION_MEDIA
string $type = null
): ResponseInterface {
$station = $request->getStation();
$storageLocation = $station->getStorageLocation($type);
$typeEnum = Entity\Enums\StorageLocationTypes::tryFrom($type ?? '')
?? Entity\Enums\StorageLocationTypes::StationMedia;
$numFiles = match ($type) {
Entity\StorageLocation::TYPE_STATION_MEDIA => $this->getNumStationMedia($station),
Entity\StorageLocation::TYPE_STATION_PODCASTS => $this->getNumStationPodcastMedia($station),
$station = $request->getStation();
$storageLocation = $station->getStorageLocation($typeEnum);
$numFiles = match ($typeEnum) {
Entity\Enums\StorageLocationTypes::StationMedia => $this->getNumStationMedia($station),
Entity\Enums\StorageLocationTypes::StationPodcasts => $this->getNumStationPodcastMedia($station),
default => null,
};

View File

@ -4,7 +4,6 @@ declare(strict_types=1);
namespace App\Controller\Api\Stations;
use App\Entity;
use App\Http\Response;
use App\Http\ServerRequest;
use Psr\Http\Message\ResponseInterface;
@ -13,8 +12,7 @@ class GetRestartStatusAction
{
public function __invoke(
ServerRequest $request,
Response $response,
string $type = Entity\StorageLocation::TYPE_STATION_MEDIA
Response $response
): ResponseInterface {
$station = $request->getStation();
return $response->withJson([

View File

@ -5,10 +5,10 @@ declare(strict_types=1);
namespace App\Controller\Api\Stations;
use App\Entity;
use App\Enums\SupportedLocales;
use App\Environment;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Locale;
use App\OpenApi;
use App\Service\DeviceDetector;
use App\Service\IpGeolocation;
@ -111,8 +111,8 @@ class ListenersAction
->setParameter('time_end', $endTimestamp);
}
/** @var Locale $locale */
$locale = $request->getAttribute(ServerRequest::ATTR_LOCALE);
$locale = $request->getAttribute(ServerRequest::ATTR_LOCALE)
?? SupportedLocales::default();
$mountNames = $mountRepo->getDisplayNames($station);
$remoteNames = $remoteRepo->getDisplayNames($station);
@ -190,7 +190,7 @@ class ListenersAction
$api->mount_name = $remoteNames[$remoteId];
}
$api->location = $geoLite->getLocationInfo($api->ip, $locale->getLocale());
$api->location = $geoLite->getLocationInfo($api->ip, $locale);
if ($groupByUnique) {
$listenersByHash[$hash] = [

View File

@ -21,8 +21,8 @@ class GetOrderAction extends AbstractPlaylistsAction
$record = $this->requireRecord($station, $id);
if (
$record->getSource() !== Entity\StationPlaylist::SOURCE_SONGS
|| $record->getOrder() !== Entity\StationPlaylist::ORDER_SEQUENTIAL
Entity\Enums\PlaylistSources::Songs !== $record->getSourceEnum()
|| Entity\Enums\PlaylistOrders::Sequential !== $record->getOrderEnum()
) {
throw new Exception(__('This playlist is not a sequential playlist.'));
}

View File

@ -21,11 +21,11 @@ class GetQueueAction extends AbstractPlaylistsAction
): ResponseInterface {
$record = $this->requireRecord($request->getStation(), $id);
if (Entity\StationPlaylist::SOURCE_SONGS !== $record->getSource()) {
if (Entity\Enums\PlaylistSources::Songs !== $record->getSourceEnum()) {
throw new InvalidArgumentException('This playlist does not have songs as its primary source.');
}
if (Entity\StationPlaylist::ORDER_RANDOM === $record->getOrder()) {
if (Entity\Enums\PlaylistOrders::Random === $record->getOrderEnum()) {
throw new InvalidArgumentException('This playlist is always shuffled and has no visible queue.');
}

View File

@ -21,8 +21,8 @@ class PutOrderAction extends AbstractPlaylistsAction
$record = $this->requireRecord($request->getStation(), $id);
if (
$record->getSource() !== Entity\StationPlaylist::SOURCE_SONGS
|| $record->getOrder() !== Entity\StationPlaylist::ORDER_SEQUENTIAL
Entity\Enums\PlaylistSources::Songs !== $record->getSourceEnum()
|| Entity\Enums\PlaylistOrders::Sequential !== $record->getOrderEnum()
) {
throw new Exception(__('This playlist is not a sequential playlist.'));
}

View File

@ -4,10 +4,10 @@ declare(strict_types=1);
namespace App\Controller\Api\Stations;
use App\Acl;
use App\Controller\Api\AbstractApiCrudController;
use App\Doctrine\ReloadableEntityManagerInterface;
use App\Entity;
use App\Enums\StationPermissions;
use App\Flysystem\StationFilesystems;
use App\Http\Response;
use App\Http\ServerRequest;
@ -409,7 +409,7 @@ class PodcastEpisodesController extends AbstractApiCrudController
$acl = $request->getAcl();
$station = $request->getStation();
if ($acl->isAllowed(Acl::STATION_PODCASTS, $station)) {
if ($acl->isAllowed(StationPermissions::Podcasts, $station)) {
$return->links['art'] = (string)$router->fromHere(
route_name: 'api:stations:podcast:episode:art-internal',
route_params: ['episode_id' => $record->getId()],

View File

@ -4,10 +4,10 @@ declare(strict_types=1);
namespace App\Controller\Api\Stations;
use App\Acl;
use App\Controller\Api\AbstractApiCrudController;
use App\Doctrine\ReloadableEntityManagerInterface;
use App\Entity;
use App\Enums\StationPermissions;
use App\Flysystem\StationFilesystems;
use App\Http\Response;
use App\Http\ServerRequest;
@ -354,7 +354,7 @@ class PodcastsController extends AbstractApiCrudController
$acl = $request->getAcl();
if ($acl->isAllowed(Acl::STATION_PODCASTS, $station)) {
if ($acl->isAllowed(StationPermissions::Podcasts, $station)) {
$return->links['art'] = (string)$router->fromHere(
route_name: 'api:stations:podcast:art-internal',
route_params: ['podcast_id' => $record->getId()],

View File

@ -4,10 +4,10 @@ declare(strict_types=1);
namespace App\Controller\Api\Stations;
use App\Acl;
use App\Controller\Api\Admin\StationsController;
use App\Entity;
use App\Entity\Interfaces\EntityGroupsInterface;
use App\Enums\GlobalPermissions;
use App\Http\Response;
use App\Http\ServerRequest;
use Psr\Http\Message\ResponseInterface;
@ -50,7 +50,7 @@ class ProfileEditController extends StationsController
],
];
if ($request->getAcl()->isAllowed(Acl::GLOBAL_STATIONS)) {
if ($request->getAcl()->isAllowed(GlobalPermissions::Stations)) {
$context[AbstractNormalizer::GROUPS][] = EntityGroupsInterface::GROUP_ALL;
}

View File

@ -4,8 +4,8 @@ declare(strict_types=1);
namespace App\Controller\Frontend;
use App\Acl;
use App\Entity;
use App\Enums\GlobalPermissions;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Service\Avatar;
@ -40,7 +40,7 @@ class DashboardAction
'userUrl' => (string)$router->named('api:frontend:account:me'),
'profileUrl' => (string)$router->named('profile:index'),
'adminUrl' => (string)$router->named('admin:index:index'),
'showAdmin' => $acl->isAllowed(Acl::GLOBAL_VIEW),
'showAdmin' => $acl->isAllowed(GlobalPermissions::View),
'notificationsUrl' => (string)$router->named('api:frontend:dashboard:notifications'),
'showCharts' => $showCharts,
'chartsUrl' => (string)$router->named('api:frontend:dashboard:charts'),

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Controller\Frontend\PWA;
use App\Enums\SupportedThemes;
use App\Exception\StationNotFoundException;
use App\Http\Response;
use App\Http\ServerRequest;
@ -24,51 +25,51 @@ class AppManifestAction
$customization = $request->getCustomization();
$manifest = [
'name' => $station->getName() . ' - AzuraCast',
'short_name' => $station->getName(),
'name' => $station->getName() . ' - AzuraCast',
'short_name' => $station->getName(),
'description' => $station->getDescription(),
'scope' => '/public/',
'start_url' => '.',
'display' => 'standalone',
'scope' => '/public/',
'start_url' => '.',
'display' => 'standalone',
'theme_color' => '#2196F3',
'categories' => [
'categories' => [
'music',
],
'icons' => [
'icons' => [
[
'src' => $customization->getBrowserIconUrl(36),
'sizes' => '36x36',
'type' => 'image/png',
'src' => $customization->getBrowserIconUrl(36),
'sizes' => '36x36',
'type' => 'image/png',
'density' => '0.75',
],
[
'src' => $customization->getBrowserIconUrl(48),
'sizes' => '48x48',
'type' => 'image/png',
'src' => $customization->getBrowserIconUrl(48),
'sizes' => '48x48',
'type' => 'image/png',
'density' => '1.0',
],
[
'src' => $customization->getBrowserIconUrl(72),
'sizes' => '72x72',
'type' => 'image/png',
'src' => $customization->getBrowserIconUrl(72),
'sizes' => '72x72',
'type' => 'image/png',
'density' => '1.5',
],
[
'src' => $customization->getBrowserIconUrl(96),
'sizes' => '96x96',
'type' => 'image/png',
'src' => $customization->getBrowserIconUrl(96),
'sizes' => '96x96',
'type' => 'image/png',
'density' => '2.0',
],
[
'src' => $customization->getBrowserIconUrl(144),
'sizes' => '144x144',
'type' => 'image/png',
'src' => $customization->getBrowserIconUrl(144),
'sizes' => '144x144',
'type' => 'image/png',
'density' => '3.0',
],
[
'src' => $customization->getBrowserIconUrl(192),
'sizes' => '192x192',
'type' => 'image/png',
'src' => $customization->getBrowserIconUrl(192),
'sizes' => '192x192',
'type' => 'image/png',
'density' => '4.0',
],
],
@ -77,9 +78,9 @@ class AppManifestAction
$customization = $request->getCustomization();
$publicTheme = $customization->getPublicTheme();
if ($customization::THEME_BROWSER !== $publicTheme) {
if (SupportedThemes::Browser !== $publicTheme) {
$manifest['background_color'] = match ($publicTheme) {
$customization::THEME_DARK => '#222222',
SupportedThemes::Dark => '#222222',
default => '#EEEEEE'
};
}

View File

@ -4,9 +4,9 @@ declare(strict_types=1);
namespace App\Controller\Frontend\Profile;
use App\Enums\SupportedLocales;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Locale;
use Psr\Http\Message\ResponseInterface;
class IndexAction
@ -17,6 +17,11 @@ class IndexAction
): ResponseInterface {
$router = $request->getRouter();
$supportedLocales = [];
foreach (SupportedLocales::cases() as $supportedLocale) {
$supportedLocales[$supportedLocale->value] = $supportedLocale->getLocalName();
}
return $request->getView()->renderVuePage(
response: $response,
component: 'Vue_Account',
@ -27,7 +32,7 @@ class IndexAction
'changePasswordUrl' => (string)$router->named('api:frontend:account:password'),
'twoFactorUrl' => (string)$router->named('api:frontend:account:two-factor'),
'apiKeysApiUrl' => (string)$router->named('api:frontend:api-keys'),
'supportedLocales' => Locale::SUPPORTED_LOCALES,
'supportedLocales' => $supportedLocales,
]
);
}

View File

@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Controller\Frontend\Profile;
use App\Customization;
use App\Enums\SupportedThemes;
use App\Http\Response;
use App\Http\ServerRequest;
use Doctrine\ORM\EntityManagerInterface;
@ -19,16 +19,12 @@ class ThemeAction
): ResponseInterface {
$user = $request->getUser();
$currentTheme = $user->getTheme();
if (empty($currentTheme)) {
$currentTheme = Customization::DEFAULT_THEME;
}
$user->setTheme(
(Customization::THEME_LIGHT === $currentTheme)
? Customization::THEME_DARK
: Customization::THEME_LIGHT
);
$currentTheme = $user->getThemeEnum();
$newTheme = match ($currentTheme) {
SupportedThemes::Dark => SupportedThemes::Light,
default => SupportedThemes::Dark
};
$user->setTheme($newTheme->value);
$em->persist($user);
$em->flush();

View File

@ -30,7 +30,7 @@ class FilesAction
ORDER BY sp.name ASC
DQL
)->setParameter('station_id', $station->getId())
->setParameter('source', Entity\StationPlaylist::SOURCE_SONGS)
->setParameter('source', Entity\Enums\PlaylistSources::Songs->value)
->getArrayResult();
$router = $request->getRouter();
@ -48,7 +48,7 @@ class FilesAction
'mkdirUrl' => (string)$router->fromHere('api:stations:files:mkdir'),
'renameUrl' => (string)$router->fromHere('api:stations:files:rename'),
'quotaUrl' => (string)$router->fromHere('api:stations:quota', [
'type' => Entity\StorageLocation::TYPE_STATION_MEDIA,
'type' => Entity\Enums\StorageLocationTypes::StationMedia->value,
]),
'initialPlaylists' => $playlists,
'customFields' => $customFieldRepo->fetchArray(),

View File

@ -15,10 +15,10 @@ class PodcastsAction
public function __invoke(ServerRequest $request, Response $response): ResponseInterface
{
$router = $request->getRouter();
$customization = $request->getCustomization();
$station = $request->getStation();
$userLocale = (string)$request->getCustomization()->getLocale();
$locale = $request->getCustomization()->getLocale();
$userLocale = $locale->value;
$languageOptions = Languages::getNames($userLocale);
$categoriesOptions = Entity\PodcastCategory::getAvailableCategories();
@ -33,9 +33,9 @@ class PodcastsAction
'newArtUrl' => (string)$router->fromHere('api:stations:podcasts:new-art'),
'stationUrl' => (string)$router->fromHere('stations:index:index'),
'quotaUrl' => (string)$router->fromHere('api:stations:quota', [
'type' => Entity\StorageLocation::TYPE_STATION_PODCASTS,
'type' => Entity\Enums\StorageLocationTypes::StationPodcasts->value,
]),
'locale' => substr((string)$customization->getLocale(), 0, 2),
'locale' => substr($locale->value, 0, 2),
'stationTimeZone' => $station->getTimezone(),
'languageOptions' => $languageOptions,
'categoriesOptions' => $categoriesOptions,

View File

@ -4,8 +4,8 @@ declare(strict_types=1);
namespace App\Controller\Stations;
use App\Acl;
use App\Entity;
use App\Enums\StationPermissions;
use App\Http\Response;
use App\Http\ServerRequest;
use App\VueComponent\StationFormComponent;
@ -73,39 +73,39 @@ class ProfileController
title: __('Profile'),
props: [
// Common
'backendType' => $station->getBackendType(),
'frontendType' => $station->getFrontendType(),
'stationTimeZone' => $station->getTimezone(),
'stationSupportsRequests' => $backend->supportsRequests(),
'stationSupportsStreamers' => $backend->supportsStreamers(),
'enableRequests' => $station->getEnableRequests(),
'enableStreamers' => $station->getEnableStreamers(),
'enablePublicPage' => $station->getEnablePublicPage(),
'enableOnDemand' => $station->getEnableOnDemand(),
'profileApiUri' => (string)$router->fromHere('api:stations:profile'),
'backendType' => $station->getBackendType(),
'frontendType' => $station->getFrontendType(),
'stationTimeZone' => $station->getTimezone(),
'stationSupportsRequests' => $backend->supportsRequests(),
'stationSupportsStreamers' => $backend->supportsStreamers(),
'enableRequests' => $station->getEnableRequests(),
'enableStreamers' => $station->getEnableStreamers(),
'enablePublicPage' => $station->getEnablePublicPage(),
'enableOnDemand' => $station->getEnableOnDemand(),
'profileApiUri' => (string)$router->fromHere('api:stations:profile'),
// ACL
'userCanManageMedia' => $acl->isAllowed(Acl::STATION_MEDIA, $station->getId()),
'userCanManageBroadcasting' => $acl->isAllowed(Acl::STATION_BROADCASTING, $station->getId()),
'userCanManageProfile' => $acl->isAllowed(Acl::STATION_PROFILE, $station->getId()),
'userCanManageReports' => $acl->isAllowed(Acl::STATION_REPORTS, $station->getId()),
'userCanManageStreamers' => $acl->isAllowed(Acl::STATION_STREAMERS, $station->getId()),
'userCanManageMedia' => $acl->isAllowed(StationPermissions::Media, $station->getId()),
'userCanManageBroadcasting' => $acl->isAllowed(StationPermissions::Broadcasting, $station->getId()),
'userCanManageProfile' => $acl->isAllowed(StationPermissions::Profile, $station->getId()),
'userCanManageReports' => $acl->isAllowed(StationPermissions::Reports, $station->getId()),
'userCanManageStreamers' => $acl->isAllowed(StationPermissions::Streamers, $station->getId()),
// Header
'stationName' => $station->getName(),
'stationDescription' => $station->getDescription(),
'manageProfileUri' => (string)$router->fromHere('stations:profile:edit'),
'stationName' => $station->getName(),
'stationDescription' => $station->getDescription(),
'manageProfileUri' => (string)$router->fromHere('stations:profile:edit'),
// Now Playing
'backendSkipSongUri' => (string)$router->fromHere('api:stations:backend', ['do' => 'skip']),
'backendSkipSongUri' => (string)$router->fromHere('api:stations:backend', ['do' => 'skip']),
'backendDisconnectStreamerUri' => (string)$router->fromHere(
'api:stations:backend',
['do' => 'disconnect']
),
// Requests
'requestsViewUri' => (string)$router->fromHere('stations:reports:requests'),
'requestsToggleUri' => (string)$router->fromHere(
'requestsViewUri' => (string)$router->fromHere('stations:reports:requests'),
'requestsToggleUri' => (string)$router->fromHere(
'stations:profile:toggle',
['feature' => 'requests', 'csrf' => $csrf]
),

View File

@ -6,28 +6,23 @@ namespace App;
use App\Assets\AssetFactory;
use App\Entity;
use App\Enums\SupportedLocales;
use App\Enums\SupportedThemes;
use App\Http\ServerRequest;
use App\Service\NChan;
use Psr\Http\Message\ServerRequestInterface;
class Customization
{
public const DEFAULT_THEME = 'browser';
public const THEME_BROWSER = 'browser';
public const THEME_LIGHT = 'light';
public const THEME_DARK = 'dark';
public const THEMES = [self::THEME_BROWSER, self::THEME_LIGHT, self::THEME_DARK];
protected ?Entity\User $user = null;
protected Entity\Settings $settings;
protected Locale $locale;
protected SupportedLocales $locale;
protected string $theme = self::DEFAULT_THEME;
protected SupportedThemes $theme;
protected string $publicTheme = self::DEFAULT_THEME;
protected SupportedThemes $publicTheme;
protected string $instanceName = '';
@ -44,24 +39,41 @@ class Customization
$this->user = $request->getAttribute(ServerRequest::ATTR_USER);
// Register current theme
$this->theme = $this->determineTheme($request, false);
$this->publicTheme = $this->determineTheme($request, true);
// Register locale
$this->locale = SupportedLocales::createFromRequest($this->environment, $request);
}
protected function determineTheme(
ServerRequestInterface $request,
bool $isPublicTheme = false
): SupportedThemes {
$queryParams = $request->getQueryParams();
if (!empty($queryParams['theme']) && in_array($queryParams['theme'], self::THEMES, true)) {
$this->publicTheme = $this->theme = $queryParams['theme'];
} else {
$this->publicTheme = $this->settings->getPublicTheme();
if (null !== $this->user && !empty($this->user->getTheme())) {
$this->theme = (string)$this->user->getTheme();
if (!empty($queryParams['theme'])) {
$theme = SupportedThemes::tryFrom($queryParams['theme']);
if (null !== $theme) {
return $theme;
}
}
// Register locale
$this->locale = Locale::createFromRequest($this->environment, $request);
$this->locale->register();
if (null !== $this->user) {
$themeName = $this->user->getTheme();
if (!empty($themeName)) {
$theme = SupportedThemes::tryFrom($themeName);
if (null !== $theme) {
return $theme;
}
}
}
return ($isPublicTheme)
? $this->settings->getPublicThemeEnum()
: SupportedThemes::default();
}
public function getLocale(): Locale
public function getLocale(): SupportedLocales
{
return $this->locale;
}
@ -69,7 +81,7 @@ class Customization
/**
* Returns the user-customized or system default theme.
*/
public function getTheme(): string
public function getTheme(): SupportedThemes
{
return $this->theme;
}
@ -85,7 +97,7 @@ class Customization
/**
* Get the theme name to be used in public (non-logged-in) pages.
*/
public function getPublicTheme(): string
public function getPublicTheme(): SupportedThemes
{
return $this->publicTheme;
}
@ -178,4 +190,8 @@ class Customization
return $this->settings->getEnableWebsockets();
}
public function registerLocale(SupportedLocales $locale): void
{
}
}

View File

@ -0,0 +1,14 @@
<?php
// phpcs:ignoreFile
declare(strict_types=1);
namespace App\Entity\Enums;
enum PlaylistOrders: string
{
case Random = 'random';
case Shuffle = 'shuffle';
case Sequential = 'sequential';
}

View File

@ -0,0 +1,13 @@
<?php
// phpcs:ignoreFile
declare(strict_types=1);
namespace App\Entity\Enums;
enum PlaylistRemoteTypes: string
{
case Stream = 'stream';
case Playlist = 'playlist';
}

View File

@ -0,0 +1,13 @@
<?php
// phpcs:ignoreFile
declare(strict_types=1);
namespace App\Entity\Enums;
enum PlaylistSources: string
{
case Songs = 'songs';
case RemoteUrl = 'remote_url';
}

View File

@ -0,0 +1,21 @@
<?php
// phpcs:ignoreFile
declare(strict_types=1);
namespace App\Entity\Enums;
enum PlaylistTypes: string
{
case Standard = 'default';
case OncePerXSongs = 'once_per_x_songs';
case OncePerXMinutes = 'once_per_x_minutes';
case OncePerHour = 'once_per_hour';
case Advanced = 'custom';
public static function default(): self
{
return self::Standard;
}
}

View File

@ -0,0 +1,28 @@
<?php
// phpcs:ignoreFile
declare(strict_types=1);
namespace App\Entity\Enums;
enum StorageLocationAdapters: string
{
case Local = 'local';
case S3 = 's3';
case Dropbox = 'dropbox';
public function isLocal(): bool
{
return self::Local === $this;
}
public function getName(): string
{
return match ($this) {
self::Local => 'Local',
self::S3 => 'S3',
self::Dropbox => 'Dropbox',
};
}
}

View File

@ -0,0 +1,15 @@
<?php
// phpcs:ignoreFile
declare(strict_types=1);
namespace App\Entity\Enums;
enum StorageLocationTypes: string
{
case Backup = 'backup';
case StationMedia = 'station_media';
case StationRecordings = 'station_recordings';
case StationPodcasts = 'station_podcasts';
}

View File

@ -4,8 +4,9 @@ declare(strict_types=1);
namespace App\Entity\Fixture;
use App\Acl;
use App\Entity;
use App\Enums\GlobalPermissions;
use App\Enums\StationPermissions;
use Doctrine\Common\DataFixtures\AbstractFixture;
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
use Doctrine\Persistence\ObjectManager;
@ -19,18 +20,18 @@ class RolePermission extends AbstractFixture implements DependentFixtureInterfac
$permissions = [
'admin_role' => [
[Acl::GLOBAL_ALL, null],
[GlobalPermissions::All, null],
],
'demo_role' => [
[Acl::STATION_VIEW, $station],
[Acl::STATION_REPORTS, $station],
[Acl::STATION_PROFILE, $station],
[Acl::STATION_STREAMERS, $station],
[Acl::STATION_MOUNTS, $station],
[Acl::STATION_REMOTES, $station],
[Acl::STATION_MEDIA, $station],
[Acl::STATION_AUTOMATION, $station],
[Acl::STATION_WEB_HOOKS, $station],
[StationPermissions::View, $station],
[StationPermissions::Reports, $station],
[StationPermissions::Profile, $station],
[StationPermissions::Streamers, $station],
[StationPermissions::MountPoints, $station],
[StationPermissions::RemoteRelays, $station],
[StationPermissions::Media, $station],
[StationPermissions::Automation, $station],
[StationPermissions::WebHooks, $station],
],
];

View File

@ -5,7 +5,8 @@ declare(strict_types=1);
namespace App\Entity\Fixture;
use App\Entity;
use App\Radio\Adapters;
use App\Radio\Enums\BackendAdapters;
use App\Radio\Enums\FrontendAdapters;
use Doctrine\Common\DataFixtures\AbstractFixture;
use Doctrine\Persistence\ObjectManager;
@ -17,8 +18,8 @@ class Station extends AbstractFixture
$station->setName('AzuraTest Radio');
$station->setDescription('A test radio station.');
$station->setEnableRequests(true);
$station->setFrontendType(Adapters::FRONTEND_ICECAST);
$station->setBackendType(Adapters::BACKEND_LIQUIDSOAP);
$station->setFrontendType(FrontendAdapters::Icecast->value);
$station->setBackendType(BackendAdapters::Liquidsoap->value);
$station->setRadioBaseDir('/var/azuracast/stations/azuratest_radio');
$station->ensureDirectoriesExist();

View File

@ -4,18 +4,12 @@ declare(strict_types=1);
namespace App\Entity\Interfaces;
use App\Radio\Enums\AdapterTypeInterface;
use App\Radio\Enums\StreamFormats;
use App\Radio\Enums\StreamProtocols;
interface StationMountInterface
{
public const FORMAT_MP3 = 'mp3';
public const FORMAT_OGG = 'ogg';
public const FORMAT_AAC = 'aac';
public const FORMAT_OPUS = 'opus';
public const FORMAT_FLAC = 'flac';
public const PROTOCOL_ICY = 'icy';
public const PROTOCOL_HTTP = 'http';
public const PROTOCOL_HTTPS = 'https';
public function getEnableAutodj(): bool;
public function getAutodjUsername(): ?string;
@ -24,17 +18,17 @@ interface StationMountInterface
public function getAutodjBitrate(): ?int;
public function getAutodjFormat(): ?string;
public function getAutodjFormatEnum(): ?StreamFormats;
public function getAutodjHost(): ?string;
public function getAutodjPort(): ?int;
public function getAutodjProtocol(): ?string;
public function getAutodjProtocolEnum(): ?StreamProtocols;
public function getAutodjMount(): ?string;
public function getAutodjAdapterType(): string;
public function getAutodjAdapterTypeEnum(): AdapterTypeInterface;
public function getIsPublic(): bool;
}

View File

@ -4,34 +4,15 @@ declare(strict_types=1);
namespace App\Entity\Repository;
use App\Acl;
use App\Doctrine\Repository;
use App\Entity;
use App\Enums\GlobalPermissions;
/**
* @extends Repository<Entity\RolePermission>
*/
class RolePermissionRepository extends Repository
{
/**
* @return mixed[]
*/
public function getActionsForAllRoles(): array
{
$all_permissions = $this->fetchArray();
$roles = [];
foreach ($all_permissions as $row) {
if ($row['station_id']) {
$roles[$row['role_id']]['stations'][$row['station_id']][] = $row['action_name'];
} else {
$roles[$row['role_id']]['global'][] = $row['action_name'];
}
}
return $roles;
}
/**
* @param Entity\Role $role
*
@ -68,7 +49,7 @@ class RolePermissionRepository extends Repository
App\Entity\Role r LEFT JOIN r.permissions rp
WHERE rp.station IS NULL AND rp.action_name = :action
DQL
)->setParameter('action', Acl::GLOBAL_ALL)
)->setParameter('action', GlobalPermissions::All->value)
->setMaxResults(1)
->getOneOrNullResult();
@ -80,7 +61,7 @@ class RolePermissionRepository extends Repository
$newRole->setName('Super Administrator');
$this->em->persist($newRole);
$newPerm = new Entity\RolePermission($newRole, null, Acl::GLOBAL_ALL);
$newPerm = new Entity\RolePermission($newRole, null, GlobalPermissions::All);
$this->em->persist($newPerm);
$this->em->flush();

View File

@ -38,8 +38,8 @@ class StationPlaylistFolderRepository extends Repository
foreach ($playlists as $playlistId => $playlistRecord) {
/** @var Entity\StationPlaylist $playlistRecord */
if (
Entity\StationPlaylist::ORDER_SEQUENTIAL !== $playlistRecord->getOrder()
&& Entity\StationPlaylist::SOURCE_SONGS === $playlistRecord->getSource()
Entity\Enums\PlaylistOrders::Sequential !== $playlistRecord->getOrderEnum()
&& Entity\Enums\PlaylistSources::Songs === $playlistRecord->getSourceEnum()
) {
/** @var Entity\StationPlaylist $playlist */
$playlist = $this->em->getReference(Entity\StationPlaylist::class, $playlistId);

View File

@ -51,12 +51,12 @@ class StationPlaylistMediaRepository extends Repository
Entity\StationPlaylist $playlist,
int $weight = 0
): int {
if ($playlist->getSource() !== Entity\StationPlaylist::SOURCE_SONGS) {
if (Entity\Enums\PlaylistSources::Songs !== $playlist->getSourceEnum()) {
throw new RuntimeException('This playlist is not meant to contain songs!');
}
// Only update existing record for random-order playlists.
if ($playlist->getOrder() !== Entity\StationPlaylist::ORDER_SEQUENTIAL) {
if (Entity\Enums\PlaylistOrders::Sequential !== $playlist->getOrderEnum()) {
$record = $this->repository->findOneBy(
[
'media_id' => $media->getId(),
@ -175,11 +175,11 @@ class StationPlaylistMediaRepository extends Repository
*/
public function resetQueue(Entity\StationPlaylist $playlist, CarbonInterface $now = null): array
{
if ($playlist::SOURCE_SONGS !== $playlist->getSource()) {
if (Entity\Enums\PlaylistSources::Songs !== $playlist->getSourceEnum()) {
throw new InvalidArgumentException('Playlist must contain songs.');
}
if ($playlist::ORDER_SEQUENTIAL === $playlist->getOrder()) {
if (Entity\Enums\PlaylistOrders::Sequential === $playlist->getOrderEnum()) {
$this->em->createQuery(
<<<'DQL'
UPDATE App\Entity\StationPlaylistMedia spm
@ -188,7 +188,7 @@ class StationPlaylistMediaRepository extends Repository
DQL
)->setParameter('playlist', $playlist)
->execute();
} elseif ($playlist::ORDER_SHUFFLE === $playlist->getOrder()) {
} elseif (Entity\Enums\PlaylistOrders::Shuffle === $playlist->getOrderEnum()) {
$this->em->transactional(
function () use ($playlist): void {
$allSpmRecordsQuery = $this->em->createQuery(
@ -236,7 +236,7 @@ class StationPlaylistMediaRepository extends Repository
*/
public function getQueue(Entity\StationPlaylist $playlist): array
{
if ($playlist::SOURCE_SONGS !== $playlist->getSource()) {
if (Entity\Enums\PlaylistSources::Songs !== $playlist->getSourceEnum()) {
throw new InvalidArgumentException('Playlist must contain songs.');
}
@ -247,7 +247,7 @@ class StationPlaylistMediaRepository extends Repository
->where('spm.playlist = :playlist')
->setParameter('playlist', $playlist);
if ($playlist::ORDER_RANDOM === $playlist->getOrder()) {
if (Entity\Enums\PlaylistOrders::Random === $playlist->getOrderEnum()) {
$queuedMediaQuery = $queuedMediaQuery->orderBy('RAND()');
} else {
$queuedMediaQuery = $queuedMediaQuery->andWhere('spm.is_queued = 1')
@ -273,11 +273,11 @@ class StationPlaylistMediaRepository extends Repository
public function isQueueCompletelyFilled(Entity\StationPlaylist $playlist): bool
{
if ($playlist::SOURCE_SONGS !== $playlist->getSource()) {
if (Entity\Enums\PlaylistSources::Songs !== $playlist->getSourceEnum()) {
return true;
}
if ($playlist::ORDER_RANDOM === $playlist->getOrder()) {
if (Entity\Enums\PlaylistOrders::Random === $playlist->getOrderEnum()) {
return true;
}
@ -291,11 +291,11 @@ class StationPlaylistMediaRepository extends Repository
public function isQueueEmpty(Entity\StationPlaylist $playlist): bool
{
if ($playlist::SOURCE_SONGS !== $playlist->getSource()) {
if (Entity\Enums\PlaylistSources::Songs !== $playlist->getSourceEnum()) {
return false;
}
if ($playlist::ORDER_RANDOM === $playlist->getOrder()) {
if (Entity\Enums\PlaylistOrders::Random === $playlist->getOrderEnum()) {
return false;
}

View File

@ -9,6 +9,7 @@ use App\Doctrine\Repository;
use App\Entity;
use App\Environment;
use App\Radio\AutoDJ\Scheduler;
use App\Radio\Enums\StreamFormats;
use Psr\Log\LoggerInterface;
use Symfony\Component\Serializer\Serializer;
@ -86,8 +87,7 @@ class StationStreamerRepository extends Repository
$recordStreams = $backendConfig->recordStreams();
if ($recordStreams) {
$format = $backendConfig->getRecordStreamsFormat()
?? Entity\Interfaces\StationMountInterface::FORMAT_MP3;
$format = $backendConfig->getRecordStreamsFormatEnum() ?? StreamFormats::Mp3;
$recordingPath = $record->generateRecordingPath($format);
$this->em->persist($record);

View File

@ -13,8 +13,14 @@ use Brick\Math\BigInteger;
*/
class StorageLocationRepository extends Repository
{
public function findByType(string $type, int $id): ?Entity\StorageLocation
{
public function findByType(
string|Entity\Enums\StorageLocationTypes $type,
int $id
): ?Entity\StorageLocation {
if ($type instanceof Entity\Enums\StorageLocationTypes) {
$type = $type->value;
}
return $this->repository->findOneBy(
[
'type' => $type,
@ -24,12 +30,16 @@ class StorageLocationRepository extends Repository
}
/**
* @param string $type
* @param string|Entity\Enums\StorageLocationTypes $type
*
* @return Entity\StorageLocation[]
*/
public function findAllByType(string $type): array
public function findAllByType(string|Entity\Enums\StorageLocationTypes $type): array
{
if ($type instanceof Entity\Enums\StorageLocationTypes) {
$type = $type->value;
}
return $this->repository->findBy(
[
'type' => $type,
@ -38,14 +48,14 @@ class StorageLocationRepository extends Repository
}
/**
* @param string $type
* @param string|Entity\Enums\StorageLocationTypes $type
* @param bool $addBlank
* @param string|null $emptyString
*
* @return string[]
*/
public function fetchSelectByType(
string $type,
string|Entity\Enums\StorageLocationTypes $type,
bool $addBlank = false,
?string $emptyString = null
): array {
@ -65,12 +75,12 @@ class StorageLocationRepository extends Repository
public function createDefaultStorageLocations(): void
{
$backupLocations = $this->findAllByType(Entity\StorageLocation::TYPE_BACKUP);
$backupLocations = $this->findAllByType(Entity\Enums\StorageLocationTypes::Backup);
if (0 === count($backupLocations)) {
$record = new Entity\StorageLocation(
Entity\StorageLocation::TYPE_BACKUP,
Entity\StorageLocation::ADAPTER_LOCAL
Entity\Enums\StorageLocationTypes::Backup,
Entity\Enums\StorageLocationAdapters::Local
);
$record->setPath(Entity\StorageLocation::DEFAULT_BACKUPS_PATH);
$this->em->persist($record);
@ -90,24 +100,23 @@ class StorageLocationRepository extends Repository
->select('s')
->from(Entity\Station::class, 's');
switch ($storageLocation->getType()) {
case Entity\StorageLocation::TYPE_STATION_MEDIA:
switch ($storageLocation->getTypeEnum()) {
case Entity\Enums\StorageLocationTypes::StationMedia:
$qb->where('s.media_storage_location = :storageLocation')
->setParameter('storageLocation', $storageLocation);
break;
case Entity\StorageLocation::TYPE_STATION_RECORDINGS:
case Entity\Enums\StorageLocationTypes::StationRecordings:
$qb->where('s.recordings_storage_location = :storageLocation')
->setParameter('storageLocation', $storageLocation);
break;
case Entity\StorageLocation::TYPE_STATION_PODCASTS:
case Entity\Enums\StorageLocationTypes::StationPodcasts:
$qb->where('s.podcasts_storage_location = :storageLocation')
->setParameter('storageLocation', $storageLocation);
break;
case Entity\StorageLocation::TYPE_BACKUP:
default:
case Entity\Enums\StorageLocationTypes::Backup:
return [];
}

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Entity;
use App\Enums\PermissionInterface;
use Doctrine\ORM\Mapping as ORM;
use JsonSerializable;
@ -36,8 +37,11 @@ class RolePermission implements
#[ORM\JoinColumn(name: 'station_id', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
protected ?Station $station = null;
public function __construct(Role $role, Station $station = null, ?string $action_name = null)
{
public function __construct(
Role $role,
Station $station = null,
string|PermissionInterface|null $action_name = null
) {
$this->role = $role;
$this->station = $station;
@ -71,8 +75,12 @@ class RolePermission implements
return $this->action_name;
}
public function setActionName(string $action_name): void
public function setActionName(string|PermissionInterface $action_name): void
{
if ($action_name instanceof PermissionInterface) {
$action_name = $action_name->getValue();
}
$this->action_name = $action_name;
}
@ -82,7 +90,7 @@ class RolePermission implements
public function jsonSerialize(): array
{
return [
'action' => $this->action_name,
'action' => $this->action_name,
'station_id' => $this->station_id,
];
}

View File

@ -4,9 +4,9 @@ declare(strict_types=1);
namespace App\Entity;
use App\Customization;
use App\Doctrine\Generator\UuidV6Generator;
use App\Entity;
use App\Enums\SupportedThemes;
use App\OpenApi;
use App\Service\Avatar;
use App\Utilities\Urls;
@ -306,18 +306,27 @@ class Settings implements Stringable
#[
OA\Property(description: "Base Theme for Public Pages", example: "light"),
ORM\Column(length: 50, nullable: true),
Assert\Choice([Customization::THEME_BROWSER, Customization::THEME_LIGHT, Customization::THEME_DARK]),
Groups(self::GROUP_BRANDING)
]
protected ?string $public_theme = Customization::DEFAULT_THEME;
protected ?string $public_theme = null;
public function getPublicTheme(): string
{
return $this->public_theme ?? Customization::DEFAULT_THEME;
return $this->getPublicThemeEnum()->value;
}
public function getPublicThemeEnum(): SupportedThemes
{
return SupportedThemes::tryFrom($this->public_theme ?? '')
?? SupportedThemes::default();
}
public function setPublicTheme(?string $publicTheme): void
{
if (null !== $publicTheme && null === SupportedThemes::tryFrom($publicTheme)) {
throw new \InvalidArgumentException('Unsupported theme specified.');
}
$this->public_theme = $publicTheme;
}

View File

@ -4,10 +4,13 @@ declare(strict_types=1);
namespace App\Entity;
use App\Entity\Enums\StorageLocationAdapters;
use App\Entity\Enums\StorageLocationTypes;
use App\Entity\Interfaces\EntityGroupsInterface;
use App\Entity\Interfaces\IdentifiableEntityInterface;
use App\Environment;
use App\Radio\Adapters;
use App\Radio\Enums\BackendAdapters;
use App\Radio\Enums\FrontendAdapters;
use App\Utilities\File;
use App\Utilities\Urls;
use App\Validator\Constraints as AppAssert;
@ -16,7 +19,6 @@ use DateTimeZone;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use InvalidArgumentException;
use League\Flysystem\UnixVisibility\PortableVisibilityConverter;
use League\Flysystem\Visibility;
use OpenApi\Attributes as OA;
@ -79,10 +81,9 @@ class Station implements Stringable, IdentifiableEntityInterface
example: "icecast"
),
ORM\Column(length: 100, nullable: true),
Assert\Choice(choices: [Adapters::FRONTEND_ICECAST, Adapters::FRONTEND_REMOTE, Adapters::FRONTEND_SHOUTCAST]),
Serializer\Groups([EntityGroupsInterface::GROUP_GENERAL, EntityGroupsInterface::GROUP_ALL])
]
protected ?string $frontend_type = Adapters::FRONTEND_ICECAST;
protected ?string $frontend_type = null;
#[
OA\Property(
@ -101,10 +102,9 @@ class Station implements Stringable, IdentifiableEntityInterface
example: "liquidsoap"
),
ORM\Column(length: 100, nullable: true),
Assert\Choice(choices: [Adapters::BACKEND_LIQUIDSOAP, Adapters::BACKEND_NONE]),
Serializer\Groups([EntityGroupsInterface::GROUP_GENERAL, EntityGroupsInterface::GROUP_ALL])
]
protected ?string $backend_type = Adapters::BACKEND_LIQUIDSOAP;
protected ?string $backend_type = null;
#[
OA\Property(
@ -388,6 +388,9 @@ class Station implements Stringable, IdentifiableEntityInterface
public function __construct()
{
$this->frontend_type = FrontendAdapters::Icecast->value;
$this->backend_type = BackendAdapters::Liquidsoap->value;
$this->history = new ArrayCollection();
$this->playlists = new ArrayCollection();
$this->mounts = new ArrayCollection();
@ -444,8 +447,19 @@ class Station implements Stringable, IdentifiableEntityInterface
return $this->frontend_type;
}
public function getFrontendTypeEnum(): FrontendAdapters
{
return (null !== $this->frontend_type)
? FrontendAdapters::from($this->frontend_type)
: FrontendAdapters::default();
}
public function setFrontendType(?string $frontend_type = null): void
{
if (null !== $frontend_type && null === FrontendAdapters::tryFrom($frontend_type)) {
throw new \InvalidArgumentException('Invalid frontend type specified.');
}
$this->frontend_type = $frontend_type;
}
@ -488,8 +502,19 @@ class Station implements Stringable, IdentifiableEntityInterface
return $this->backend_type;
}
public function getBackendTypeEnum(): BackendAdapters
{
return (null !== $this->backend_type)
? BackendAdapters::from($this->backend_type)
: BackendAdapters::default();
}
public function setBackendType(string $backend_type = null): void
{
if (null !== $backend_type && null === BackendAdapters::tryFrom($backend_type)) {
throw new \InvalidArgumentException('Invalid frontend type specified.');
}
$this->backend_type = $backend_type;
}
@ -619,8 +644,8 @@ class Station implements Stringable, IdentifiableEntityInterface
if (null === $this->media_storage_location) {
$storageLocation = new StorageLocation(
StorageLocation::TYPE_STATION_MEDIA,
StorageLocation::ADAPTER_LOCAL
StorageLocationTypes::StationMedia,
StorageLocationAdapters::Local
);
$mediaPath = $this->getRadioBaseDir() . '/media';
@ -632,8 +657,8 @@ class Station implements Stringable, IdentifiableEntityInterface
if (null === $this->recordings_storage_location) {
$storageLocation = new StorageLocation(
StorageLocation::TYPE_STATION_RECORDINGS,
StorageLocation::ADAPTER_LOCAL
StorageLocationTypes::StationRecordings,
StorageLocationAdapters::Local
);
$recordingsPath = $this->getRadioBaseDir() . '/recordings';
@ -645,8 +670,8 @@ class Station implements Stringable, IdentifiableEntityInterface
if (null === $this->podcasts_storage_location) {
$storageLocation = new StorageLocation(
StorageLocation::TYPE_STATION_PODCASTS,
StorageLocation::ADAPTER_LOCAL
StorageLocationTypes::StationPodcasts,
StorageLocationAdapters::Local
);
$podcastsPath = $this->getRadioBaseDir() . '/podcasts';
@ -934,65 +959,63 @@ class Station implements Stringable, IdentifiableEntityInterface
public function getMediaStorageLocation(): StorageLocation
{
return $this->getStorageLocation(StorageLocation::TYPE_STATION_MEDIA);
if (null === $this->media_storage_location) {
throw new \RuntimeException('Media storage location not initialized.');
}
return $this->media_storage_location;
}
public function setMediaStorageLocation(?StorageLocation $storageLocation = null): void
{
$this->setStorageLocation(StorageLocation::TYPE_STATION_MEDIA, $storageLocation);
if (null !== $storageLocation && StorageLocationTypes::StationMedia !== $storageLocation->getTypeEnum()) {
throw new \RuntimeException('Invalid storage location.');
}
$this->media_storage_location = $storageLocation;
}
public function getRecordingsStorageLocation(): StorageLocation
{
return $this->getStorageLocation(StorageLocation::TYPE_STATION_RECORDINGS);
if (null === $this->recordings_storage_location) {
throw new \RuntimeException('Recordings storage location not initialized.');
}
return $this->recordings_storage_location;
}
public function setRecordingsStorageLocation(?StorageLocation $storageLocation = null): void
{
$this->setStorageLocation(StorageLocation::TYPE_STATION_RECORDINGS, $storageLocation);
if (null !== $storageLocation && StorageLocationTypes::StationRecordings !== $storageLocation->getTypeEnum()) {
throw new \RuntimeException('Invalid storage location.');
}
$this->recordings_storage_location = $storageLocation;
}
public function getPodcastsStorageLocation(): StorageLocation
{
return $this->getStorageLocation(StorageLocation::TYPE_STATION_PODCASTS);
if (null === $this->podcasts_storage_location) {
throw new \RuntimeException('Podcasts storage location not initialized.');
}
return $this->podcasts_storage_location;
}
public function setPodcastsStorageLocation(?StorageLocation $storageLocation = null): void
{
$this->setStorageLocation(StorageLocation::TYPE_STATION_PODCASTS, $storageLocation);
if (null !== $storageLocation && StorageLocationTypes::StationPodcasts !== $storageLocation->getTypeEnum()) {
throw new \RuntimeException('Invalid storage location.');
}
$this->podcasts_storage_location = $storageLocation;
}
public function getStorageLocation(string $type): StorageLocation
public function getStorageLocation(StorageLocationTypes $type): StorageLocation
{
$supportedTypes = self::getStorageLocationTypes();
if (!isset($supportedTypes[$type])) {
throw new InvalidArgumentException(sprintf('Invalid type: %s', $type));
}
$record = $this->{$supportedTypes[$type]};
if (null === $record) {
throw new RuntimeException(sprintf('Storage location for type %s has not been configured yet.', $type));
}
return $record;
}
public function setStorageLocation(string $type, ?StorageLocation $newStorageLocation): void
{
// Ignore if being set to null.
if (null === $newStorageLocation) {
return;
}
if ($type !== $newStorageLocation->getType()) {
throw new InvalidArgumentException(sprintf('Specified location is not of type %s.', $type));
}
$supportedTypes = self::getStorageLocationTypes();
if (!isset($supportedTypes[$type])) {
throw new InvalidArgumentException(sprintf('Invalid type: %s', $type));
}
$this->{$supportedTypes[$type]} = $newStorageLocation;
return match ($type) {
StorageLocationTypes::StationMedia => $this->getMediaStorageLocation(),
StorageLocationTypes::StationRecordings => $this->getRecordingsStorageLocation(),
StorageLocationTypes::StationPodcasts => $this->getPodcastsStorageLocation(),
default => throw new \InvalidArgumentException('Invalid storage location.')
};
}
/** @return StorageLocation[] */
@ -1005,12 +1028,15 @@ class Station implements Stringable, IdentifiableEntityInterface
];
}
/**
* @return array<string, StorageLocationTypes>
*/
public static function getStorageLocationTypes(): array
{
return [
StorageLocation::TYPE_STATION_MEDIA => 'media_storage_location',
StorageLocation::TYPE_STATION_RECORDINGS => 'recordings_storage_location',
StorageLocation::TYPE_STATION_PODCASTS => 'podcasts_storage_location',
'media_storage_location' => StorageLocationTypes::StationMedia,
'recordings_storage_location' => StorageLocationTypes::StationRecordings,
'podcasts_storage_location' => StorageLocationTypes::StationPodcasts,
];
}

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Entity;
use App\Radio\Enums\StreamFormats;
use Doctrine\Common\Collections\ArrayCollection;
class StationBackendConfiguration extends ArrayCollection
@ -65,8 +66,24 @@ class StationBackendConfiguration extends ArrayCollection
return $this->get(self::RECORD_STREAMS_FORMAT);
}
public function getRecordStreamsFormatEnum(): ?StreamFormats
{
$recordStreamsFormat = $this->getRecordStreamsFormat();
return (null !== $recordStreamsFormat)
? StreamFormats::from(strtolower($recordStreamsFormat))
: null;
}
public function setRecordStreamsFormat(?string $format): void
{
if (null !== $format) {
$format = strtolower($format);
}
if (null !== $format && null === StreamFormats::tryFrom($format)) {
throw new \InvalidArgumentException('Invalid recording type specified.');
}
$this->set(self::RECORD_STREAMS_FORMAT, $format);
}

View File

@ -4,7 +4,10 @@ declare(strict_types=1);
namespace App\Entity;
use App\Radio\Adapters;
use App\Radio\Enums\AdapterTypeInterface;
use App\Radio\Enums\FrontendAdapters;
use App\Radio\Enums\StreamFormats;
use App\Radio\Enums\StreamProtocols;
use App\Radio\Frontend\AbstractFrontend;
use App\Utilities\Urls;
use Doctrine\ORM\Mapping as ORM;
@ -280,6 +283,13 @@ class StationMount implements
return $this->autodj_format;
}
public function getAutodjFormatEnum(): ?StreamFormats
{
return (null !== $this->autodj_format)
? StreamFormats::from(strtolower($this->autodj_format))
: null;
}
public function setAutodjFormat(?string $autodj_format = null): void
{
$this->autodj_format = $this->truncateNullableString($autodj_format, 10);
@ -360,11 +370,12 @@ class StationMount implements
return $this->getStation()->getFrontendConfig()->getPort();
}
public function getAutodjProtocol(): ?string
public function getAutodjProtocolEnum(): ?StreamProtocols
{
return Adapters::FRONTEND_SHOUTCAST === $this->getAutodjAdapterType()
? self::PROTOCOL_ICY
: null;
return match ($this->getAutodjAdapterTypeEnum()) {
FrontendAdapters::SHOUTcast => StreamProtocols::Icy,
default => null
};
}
public function getAutodjUsername(): ?string
@ -382,15 +393,9 @@ class StationMount implements
return $this->getName();
}
public function getAutodjAdapterType(): string
public function getAutodjAdapterTypeEnum(): AdapterTypeInterface
{
$adapterLookup = [
Adapters::FRONTEND_ICECAST => Adapters::REMOTE_ICECAST,
Adapters::FRONTEND_SHOUTCAST => Adapters::REMOTE_SHOUTCAST2,
];
$frontendType = $this->getStation()->getFrontendType();
return $adapterLookup[$frontendType];
return $this->getStation()->getFrontendTypeEnum();
}
/**

View File

@ -4,6 +4,10 @@ declare(strict_types=1);
namespace App\Entity;
use App\Entity\Enums\PlaylistOrders;
use App\Entity\Enums\PlaylistRemoteTypes;
use App\Entity\Enums\PlaylistSources;
use App\Entity\Enums\PlaylistTypes;
use Azura\Normalizer\Attributes\DeepNormalize;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
@ -31,22 +35,6 @@ class StationPlaylist implements
public const DEFAULT_WEIGHT = 3;
public const DEFAULT_REMOTE_BUFFER = 20;
public const TYPE_DEFAULT = 'default';
public const TYPE_ONCE_PER_X_SONGS = 'once_per_x_songs';
public const TYPE_ONCE_PER_X_MINUTES = 'once_per_x_minutes';
public const TYPE_ONCE_PER_HOUR = 'once_per_hour';
public const TYPE_ADVANCED = 'custom';
public const SOURCE_SONGS = 'songs';
public const SOURCE_REMOTE_URL = 'remote_url';
public const REMOTE_TYPE_STREAM = 'stream';
public const REMOTE_TYPE_PLAYLIST = 'playlist';
public const ORDER_RANDOM = 'random';
public const ORDER_SHUFFLE = 'shuffle';
public const ORDER_SEQUENTIAL = 'sequential';
public const OPTION_INTERRUPT_OTHER_SONGS = 'interrupt';
public const OPTION_LOOP_PLAYLIST_ONCE = 'loop_once';
public const OPTION_PLAY_SINGLE_TRACK = 'single_track';
@ -70,30 +58,21 @@ class StationPlaylist implements
#[
OA\Property(example: "default"),
ORM\Column(length: 50),
Assert\Choice(choices: [
self::TYPE_DEFAULT,
self::TYPE_ONCE_PER_X_SONGS,
self::TYPE_ONCE_PER_X_MINUTES,
self::TYPE_ONCE_PER_HOUR,
self::TYPE_ADVANCED,
])
ORM\Column(length: 50)
]
protected string $type = self::TYPE_DEFAULT;
protected string $type;
#[
OA\Property(example: "songs"),
ORM\Column(length: 50),
Assert\Choice(choices: [self::SOURCE_SONGS, self::SOURCE_REMOTE_URL])
ORM\Column(length: 50)
]
protected string $source = self::SOURCE_SONGS;
protected string $source;
#[
OA\Property(example: "shuffle"),
ORM\Column(name: 'playback_order', length: 50),
Assert\Choice(choices: [self::ORDER_RANDOM, self::ORDER_SHUFFLE, self::ORDER_SEQUENTIAL])
ORM\Column(name: 'playback_order', length: 50)
]
protected string $order = self::ORDER_SHUFFLE;
protected string $order;
#[
OA\Property(example: "https://remote-url.example.com/stream.mp3"),
@ -103,10 +82,9 @@ class StationPlaylist implements
#[
OA\Property(example: "stream"),
ORM\Column(length: 25, nullable: true),
Assert\Choice(choices: [self::REMOTE_TYPE_STREAM, self::REMOTE_TYPE_PLAYLIST])
ORM\Column(length: 25, nullable: true)
]
protected ?string $remote_type = self::REMOTE_TYPE_STREAM;
protected ?string $remote_type;
#[
OA\Property(
@ -224,6 +202,11 @@ class StationPlaylist implements
{
$this->station = $station;
$this->type = PlaylistTypes::default()->value;
$this->source = PlaylistSources::Songs->value;
$this->order = PlaylistOrders::Shuffle->value;
$this->remote_type = PlaylistRemoteTypes::Stream->value;
$this->media_items = new ArrayCollection();
$this->folders = new ArrayCollection();
$this->schedule_items = new ArrayCollection();
@ -259,8 +242,17 @@ class StationPlaylist implements
return $this->type;
}
public function getTypeEnum(): PlaylistTypes
{
return PlaylistTypes::from($this->type);
}
public function setType(string $type): void
{
if (null === PlaylistTypes::tryFrom($type)) {
throw new \InvalidArgumentException('Invalid playlist type.');
}
$this->type = $type;
}
@ -269,8 +261,17 @@ class StationPlaylist implements
return $this->source;
}
public function getSourceEnum(): PlaylistSources
{
return PlaylistSources::from($this->source);
}
public function setSource(string $source): void
{
if (null === PlaylistSources::tryFrom($source)) {
throw new \InvalidArgumentException('Invalid playlist source.');
}
$this->source = $source;
}
@ -279,8 +280,17 @@ class StationPlaylist implements
return $this->order;
}
public function getOrderEnum(): PlaylistOrders
{
return PlaylistOrders::from($this->order);
}
public function setOrder(string $order): void
{
if (null === PlaylistOrders::tryFrom($order)) {
throw new \InvalidArgumentException('Invalid playlist order.');
}
$this->order = $order;
}
@ -299,8 +309,17 @@ class StationPlaylist implements
return $this->remote_type;
}
public function getRemoteTypeEnum(): ?PlaylistRemoteTypes
{
return PlaylistRemoteTypes::tryFrom($this->remote_type ?? '');
}
public function setRemoteType(?string $remote_type): void
{
if (null !== $remote_type && null === PlaylistRemoteTypes::tryFrom($remote_type)) {
throw new \InvalidArgumentException('Invalid playlist remote type.');
}
$this->remote_type = $remote_type;
}
@ -456,12 +475,12 @@ class StationPlaylist implements
return false;
}
if (self::SOURCE_SONGS === $this->source) {
if (PlaylistSources::Songs === $this->getSourceEnum()) {
return $this->media_items->count() > 0;
}
// Remote stream playlists aren't supported by the AzuraCast AutoDJ.
return self::REMOTE_TYPE_PLAYLIST === $this->remote_type;
return PlaylistRemoteTypes::Playlist === $this->getRemoteTypeEnum();
}
/**

View File

@ -4,14 +4,16 @@ declare(strict_types=1);
namespace App\Entity;
use App\Radio\Adapters;
use App\Radio\Enums\AdapterTypeInterface;
use App\Radio\Enums\RemoteAdapters;
use App\Radio\Enums\StreamFormats;
use App\Radio\Enums\StreamProtocols;
use App\Radio\Remote\AbstractRemote;
use App\Utilities;
use Doctrine\ORM\Mapping as ORM;
use GuzzleHttp\Psr7\Uri;
use Psr\Http\Message\UriInterface;
use Stringable;
use Symfony\Component\Validator\Constraints as Assert;
#[
ORM\Entity,
@ -48,7 +50,6 @@ class StationRemote implements
protected bool $is_visible_on_public_pages = true;
#[ORM\Column(length: 50)]
#[Assert\Choice(choices: [Adapters::REMOTE_ICECAST, Adapters::REMOTE_SHOUTCAST1, Adapters::REMOTE_SHOUTCAST2])]
protected string $type;
#[ORM\Column]
@ -145,6 +146,13 @@ class StationRemote implements
return $this->autodj_format;
}
public function getAutodjFormatEnum(): ?StreamFormats
{
return (null !== $this->autodj_format)
? StreamFormats::from(strtolower($this->autodj_format))
: null;
}
public function setAutodjFormat(string $autodj_format = null): void
{
$this->autodj_format = $autodj_format;
@ -189,7 +197,7 @@ class StationRemote implements
{
$password = $this->getSourcePassword();
if (Adapters::REMOTE_SHOUTCAST2 === $this->getType()) {
if (RemoteAdapters::SHOUTcast2 === $this->getTypeEnum()) {
$mount = $this->getSourceMount();
if (empty($mount)) {
$mount = $this->getMount();
@ -218,8 +226,17 @@ class StationRemote implements
return $this->type;
}
public function getTypeEnum(): RemoteAdapters
{
return RemoteAdapters::from($this->type);
}
public function setType(string $type): void
{
if (null === RemoteAdapters::tryFrom($type)) {
throw new \InvalidArgumentException('Invalid type specified.');
}
$this->type = $type;
}
@ -256,7 +273,7 @@ class StationRemote implements
/** @inheritdoc */
public function getAutodjMount(): ?string
{
if (Adapters::REMOTE_ICECAST !== $this->getType()) {
if (RemoteAdapters::Icecast !== $this->getTypeEnum()) {
return null;
}
@ -317,19 +334,19 @@ class StationRemote implements
$this->source_port = $source_port;
}
public function getAutodjProtocol(): ?string
public function getAutodjProtocolEnum(): ?StreamProtocols
{
$urlScheme = $this->getUrlAsUri()->getScheme();
return match ($this->getAutodjAdapterType()) {
Adapters::REMOTE_SHOUTCAST1, Adapters::REMOTE_SHOUTCAST2 => self::PROTOCOL_ICY,
default => ('https' === $urlScheme) ? self::PROTOCOL_HTTPS : self::PROTOCOL_HTTP
return match ($this->getAutodjAdapterTypeEnum()) {
RemoteAdapters::SHOUTcast1, RemoteAdapters::SHOUTcast2 => StreamProtocols::Icy,
default => ('https' === $urlScheme) ? StreamProtocols::Https : StreamProtocols::Http
};
}
public function getAutodjAdapterType(): string
public function getAutodjAdapterTypeEnum(): AdapterTypeInterface
{
return $this->getType();
return $this->getTypeEnum();
}
public function getIsPublic(): bool
@ -367,7 +384,7 @@ class StationRemote implements
*/
public function isEditable(): bool
{
return (Adapters::REMOTE_AZURARELAY !== $this->type);
return (RemoteAdapters::AzuraRelay !== $this->getTypeEnum());
}
/**
@ -389,7 +406,7 @@ class StationRemote implements
unique: $this->listeners_unique
);
if ($this->enable_autodj || (Adapters::REMOTE_AZURARELAY === $this->type)) {
if ($this->enable_autodj || (RemoteAdapters::AzuraRelay === $this->getTypeEnum())) {
$response->bitrate = (int)$this->autodj_bitrate;
$response->format = (string)$this->autodj_format;
}

View File

@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Entity;
use App\Entity\Interfaces\IdentifiableEntityInterface;
use App\Entity\Interfaces\StationMountInterface;
use App\Radio\Enums\StreamFormats;
use Carbon\CarbonImmutable;
use Doctrine\ORM\Mapping as ORM;
use OpenApi\Attributes as OA;
@ -84,22 +84,15 @@ class StationStreamerBroadcast implements IdentifiableEntityInterface
return $this->recordingPath;
}
public function generateRecordingPath(string $format = StationMountInterface::FORMAT_MP3): string
public function generateRecordingPath(StreamFormats $format): string
{
$ext = match (strtolower($format)) {
StationMountInterface::FORMAT_AAC => 'mp4',
StationMountInterface::FORMAT_OGG => 'ogg',
StationMountInterface::FORMAT_OPUS => 'opus',
default => 'mp3',
};
$now = CarbonImmutable::createFromTimestamp(
$this->timestampStart,
$this->station->getTimezoneObject()
);
$this->recordingPath = $this->streamer->getStreamerUsername()
. '/' . self::PATH_PREFIX . '_' . $now->format('Ymd-His') . '.' . $ext;
. '/' . self::PATH_PREFIX . '_' . $now->format('Ymd-His') . '.' . $format->getExtension();
return $this->recordingPath;
}

View File

@ -4,6 +4,8 @@ declare(strict_types=1);
namespace App\Entity;
use App\Entity\Enums\StorageLocationAdapters;
use App\Entity\Enums\StorageLocationTypes;
use App\Entity\Interfaces\IdentifiableEntityInterface;
use App\Exception\StorageLocationFullException;
use App\Radio\Quota;
@ -27,7 +29,6 @@ use League\Flysystem\Visibility;
use RuntimeException;
use Spatie\Dropbox\Client;
use Stringable;
use Symfony\Component\Validator\Constraints as Assert;
#[
ORM\Entity,
@ -40,33 +41,13 @@ class StorageLocation implements Stringable, IdentifiableEntityInterface
use Traits\HasAutoIncrementId;
use Traits\TruncateStrings;
public const TYPE_BACKUP = 'backup';
public const TYPE_STATION_MEDIA = 'station_media';
public const TYPE_STATION_RECORDINGS = 'station_recordings';
public const TYPE_STATION_PODCASTS = 'station_podcasts';
public const ADAPTER_LOCAL = 'local';
public const ADAPTER_S3 = 's3';
public const ADAPTER_DROPBOX = 'dropbox';
public const DEFAULT_BACKUPS_PATH = '/var/azuracast/backups';
#[ORM\Column(length: 50)]
#[Assert\Choice(choices: [
StorageLocation::TYPE_BACKUP,
StorageLocation::TYPE_STATION_MEDIA,
StorageLocation::TYPE_STATION_RECORDINGS,
StorageLocation::TYPE_STATION_PODCASTS,
])]
protected string $type;
#[ORM\Column(length: 50)]
#[Assert\Choice(choices: [
StorageLocation::ADAPTER_LOCAL,
StorageLocation::ADAPTER_S3,
StorageLocation::ADAPTER_DROPBOX,
])]
protected string $adapter = self::ADAPTER_LOCAL;
protected string $adapter;
#[ORM\Column(length: 255, nullable: false)]
protected string $path = '';
@ -102,10 +83,19 @@ class StorageLocation implements Stringable, IdentifiableEntityInterface
#[ORM\OneToMany(mappedBy: 'storage_location', targetEntity: StationMedia::class)]
protected Collection $media;
public function __construct(string $type, string $adapter)
{
$this->type = $type;
$this->adapter = $adapter;
public function __construct(
string|StorageLocationTypes $type,
string|StorageLocationAdapters $adapter
) {
if (!($type instanceof StorageLocationTypes)) {
$type = StorageLocationTypes::from($type);
}
if (!($adapter instanceof StorageLocationAdapters)) {
$adapter = StorageLocationAdapters::from($adapter);
}
$this->type = $type->value;
$this->adapter = $adapter->value;
$this->media = new ArrayCollection();
}
@ -115,11 +105,21 @@ class StorageLocation implements Stringable, IdentifiableEntityInterface
return $this->type;
}
public function getTypeEnum(): StorageLocationTypes
{
return StorageLocationTypes::from($this->type);
}
public function getAdapter(): string
{
return $this->adapter;
}
public function getAdapterEnum(): StorageLocationAdapters
{
return StorageLocationAdapters::from($this->adapter);
}
public function getPath(): string
{
return $this->path;
@ -127,8 +127,8 @@ class StorageLocation implements Stringable, IdentifiableEntityInterface
public function getFilteredPath(): string
{
return match ($this->adapter) {
self::ADAPTER_S3, self::ADAPTER_DROPBOX => trim($this->path, '/'),
return match ($this->getAdapterEnum()) {
StorageLocationAdapters::S3, StorageLocationAdapters::Dropbox => trim($this->path, '/'),
default => rtrim($this->path, '/')
};
}
@ -219,7 +219,7 @@ class StorageLocation implements Stringable, IdentifiableEntityInterface
public function isLocal(): bool
{
return self::ADAPTER_LOCAL === $this->adapter;
return $this->getAdapterEnum()->isLocal();
}
public function getStorageQuota(): ?string
@ -393,9 +393,9 @@ class StorageLocation implements Stringable, IdentifiableEntityInterface
{
$path = $this->applyPath($suffix);
return match ($this->adapter) {
self::ADAPTER_S3 => $this->getS3ObjectUri($suffix),
self::ADAPTER_DROPBOX => 'dropbox://' . $this->dropboxAuthToken . ltrim($path, '/'),
return match ($this->getAdapterEnum()) {
StorageLocationAdapters::S3 => $this->getS3ObjectUri($suffix),
StorageLocationAdapters::Dropbox => 'dropbox://' . $this->dropboxAuthToken . ltrim($path, '/'),
default => $path,
};
}
@ -424,11 +424,11 @@ class StorageLocation implements Stringable, IdentifiableEntityInterface
public function validate(): void
{
if (self::ADAPTER_S3 === $this->adapter) {
if (StorageLocationAdapters::S3 === $this->getAdapterEnum()) {
$client = $this->getS3Client();
$client->listObjectsV2(
[
'Bucket' => $this->s3Bucket,
'Bucket' => $this->s3Bucket,
'max-keys' => 1,
]
);
@ -442,15 +442,15 @@ class StorageLocation implements Stringable, IdentifiableEntityInterface
{
$filteredPath = $this->getFilteredPath();
switch ($this->adapter) {
case self::ADAPTER_S3:
switch ($this->getAdapterEnum()) {
case StorageLocationAdapters::S3:
$bucket = $this->s3Bucket;
if (null === $bucket) {
throw new RuntimeException('Amazon S3 bucket is empty.');
}
return new AwsS3Adapter($this->getS3Client(), $bucket, $filteredPath);
case self::ADAPTER_DROPBOX:
case StorageLocationAdapters::Dropbox:
return new DropboxAdapter($this->getDropboxClient(), $filteredPath);
default:
@ -460,19 +460,19 @@ class StorageLocation implements Stringable, IdentifiableEntityInterface
protected function getS3Client(): S3Client
{
if (self::ADAPTER_S3 !== $this->adapter) {
if (StorageLocationAdapters::S3 !== $this->getAdapterEnum()) {
throw new InvalidArgumentException('This storage location is not using the S3 adapter.');
}
$s3Options = array_filter(
[
'credentials' => [
'key' => $this->s3CredentialKey,
'key' => $this->s3CredentialKey,
'secret' => $this->s3CredentialSecret,
],
'region' => $this->s3Region,
'version' => $this->s3Version,
'endpoint' => $this->s3Endpoint,
'region' => $this->s3Region,
'version' => $this->s3Version,
'endpoint' => $this->s3Endpoint,
]
);
return new S3Client($s3Options);
@ -480,7 +480,7 @@ class StorageLocation implements Stringable, IdentifiableEntityInterface
protected function getDropboxClient(): Client
{
if (self::ADAPTER_DROPBOX !== $this->adapter) {
if (StorageLocationAdapters::Dropbox !== $this->getAdapterEnum()) {
throw new InvalidArgumentException('This storage location is not using the Dropbox adapter.');
}
@ -501,11 +501,6 @@ class StorageLocation implements Stringable, IdentifiableEntityInterface
public function __toString(): string
{
$adapterNames = [
self::ADAPTER_LOCAL => 'Local',
self::ADAPTER_S3 => 'S3',
self::ADAPTER_DROPBOX => 'Dropbox',
];
return $adapterNames[$this->adapter] . ': ' . $this->getUri();
return $this->getAdapterEnum()->getName() . ': ' . $this->getUri();
}
}

View File

@ -7,6 +7,7 @@ namespace App\Entity;
use App\Auth;
use App\Entity\Interfaces\EntityGroupsInterface;
use App\Entity\Interfaces\IdentifiableEntityInterface;
use App\Enums\SupportedThemes;
use App\OpenApi;
use App\Utilities\Strings;
use App\Validator\Constraints\UniqueEntity;
@ -214,6 +215,18 @@ class User implements Stringable, IdentifiableEntityInterface
return $this->theme;
}
public function getThemeEnum(): SupportedThemes
{
if (null !== $this->theme) {
$theme = SupportedThemes::tryFrom($this->theme);
if (null !== $theme) {
return $theme;
}
}
return SupportedThemes::default();
}
public function setTheme(?string $theme = null): void
{
$this->theme = $theme;

View File

@ -0,0 +1,45 @@
<?php
// phpcs:ignoreFile
declare(strict_types=1);
namespace App\Enums;
enum GlobalPermissions: string implements PermissionInterface
{
case All = 'administer all';
case View = 'view administration';
case Logs = 'view system logs';
case Settings = 'administer settings';
case ApiKeys = 'administer api keys';
case Stations = 'administer stations';
case CustomFields = 'administer custom fields';
case Backups = 'administer backups';
case StorageLocations = 'administer storage locations';
public function getName(): string
{
return match ($this) {
self::All => __('All Permissions'),
self::View => __('View Administration Page'),
self::Logs => __('View System Logs'),
self::Settings => __('Administer Settings'),
self::ApiKeys => __('Administer API Keys'),
self::Stations => __('Administer Stations'),
self::CustomFields => __('Administer Custom Fields'),
self::Backups => __('Administer Backups'),
self::StorageLocations => __('Administer Storage Locations'),
};
}
public function getValue(): string
{
return $this->value;
}
public function needsStation(): bool
{
return false;
}
}

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace App\Enums;
interface PermissionInterface
{
public function getValue(): string;
public function needsStation(): bool;
}

View File

@ -0,0 +1,53 @@
<?php
// phpcs:ignoreFile
declare(strict_types=1);
namespace App\Enums;
enum StationPermissions: string implements PermissionInterface
{
case All = 'administer all';
case View = 'view station management';
case Reports = 'view station reports';
case Logs = 'view station logs';
case Profile = 'manage station profile';
case Broadcasting = 'manage station broadcasting';
case Streamers = 'manage station streamers';
case MountPoints = 'manage station mounts';
case RemoteRelays = 'manage station remotes';
case Media = 'manage station media';
case Automation = 'manage station automation';
case WebHooks = 'manage station web hooks';
case Podcasts = 'manage station podcasts';
public function getName(): string
{
return match ($this) {
self::All => __('All Permissions'),
self::View => __('View Station Page'),
self::Reports => __('View Station Reports'),
self::Logs => __('View Station Logs'),
self::Profile => __('Manage Station Profile'),
self::Broadcasting => __('Manage Station Broadcasting'),
self::Streamers => __('Manage Station Streamers'),
self::MountPoints => __('Manage Station Mount Points'),
self::RemoteRelays => __('Manage Station Remote Relays'),
self::Media => __('Manage Station Media'),
self::Automation => __('Manage Station Automation'),
self::WebHooks => __('Manage Station Web Hooks'),
self::Podcasts => __('Manage Station Podcasts'),
};
}
public function getValue(): string
{
return $this->value;
}
public function needsStation(): bool
{
return true;
}
}

View File

@ -0,0 +1,178 @@
<?php
// phpcs:ignoreFile
declare(strict_types=1);
namespace App\Enums;
use App\Environment;
use App\Http\ServerRequest;
use Gettext\Translator;
use Psr\Http\Message\ServerRequestInterface;
enum SupportedLocales: string
{
case English = 'en_US.UTF-8';
case Czech = 'cs_CZ.UTF-8';
case German = 'de_DE.UTF-8';
case Spanish = 'es_ES.UTF-8';
case French = 'fr_FR.UTF-8';
case Greek = 'el_GR.UTF-8';
case Italian = 'it_IT.UTF-8';
case Hungarian = 'hu_HU.UTF-8';
case Dutch = 'nl_NL.UTF-8';
case Polish = 'pl_PL.UTF-8';
case Portuguese = 'pt_PT.UTF-8';
case BrazilianPortuguese = 'pt_BR.UTF-8';
case Russian = 'ru_RU.UTF-8';
case Swedish = 'sv_SE.UTF-8';
case Turkish = 'tr_TR.UTF-8';
case SimplifiedChinese = 'zh_CN.UTF-8';
case Korean = 'ko_KR.UTF-8';
public function getLocalName(): string
{
return match ($this) {
self::English => 'English (Default)',
self::Czech => 'čeština',
self::German => 'Deutsch',
self::Spanish => 'Español',
self::French => 'Français',
self::Greek => 'ελληνικά',
self::Italian => 'Italiano',
self::Hungarian => 'magyar',
self::Dutch => 'Nederlands',
self::Polish => 'Polski',
self::Portuguese => 'Português',
self::BrazilianPortuguese => 'Português do Brasil',
self::Russian => 'Русский язык',
self::Swedish => 'Svenska',
self::Turkish => 'Türkçe',
self::SimplifiedChinese => '簡化字',
self::Korean => '한국어',
};
}
/**
* @return string A shortened locale (minus .UTF-8).
*/
public function getLocaleWithoutEncoding(): string
{
return self::stripLocaleEncoding($this);
}
public function createTranslator(Environment $environment): Translator
{
$translator = new Translator();
$localeBase = $environment->getBaseDirectory() . '/resources/locale/compiled';
$localePath = $localeBase . '/' . $this->value . '.php';
if (file_exists($localePath)) {
$translator->loadTranslations($localePath);
}
return $translator;
}
public function register(Environment $environment): void
{
$translator = $this->createTranslator($environment);
$translator->register();
// Register translation superglobal functions
setlocale(LC_ALL, $this->value);
}
public static function default(): self
{
return self::English;
}
public static function getValidLocale(array|string|null $possibleLocales): self
{
if (null !== $possibleLocales) {
if (is_string($possibleLocales)) {
$possibleLocales = [$possibleLocales];
}
foreach ($possibleLocales as $locale) {
$locale = self::ensureLocaleEncoding($locale);
// Prefer exact match.
$exactLocale = self::tryFrom($locale);
if (null !== $exactLocale) {
return $exactLocale;
}
// Use approximate match if available.
foreach (self::cases() as $supportedLocale) {
if (str_starts_with($locale, substr($supportedLocale->value, 0, 2))) {
return $supportedLocale;
}
}
}
}
return self::default();
}
public static function createFromRequest(
Environment $environment,
ServerRequestInterface $request
): self {
$possibleLocales = [];
// Prefer user-based profile locale.
$user = $request->getAttribute(ServerRequest::ATTR_USER);
if (null !== $user && !empty($user->getLocale()) && 'default' !== $user->getLocale()) {
$possibleLocales[] = $user->getLocale();
}
$server_params = $request->getServerParams();
$browser_locale = \Locale::acceptFromHttp($server_params['HTTP_ACCEPT_LANGUAGE'] ?? '');
if (!empty($browser_locale)) {
if (2 === strlen($browser_locale)) {
$browser_locale = strtolower($browser_locale) . '_' . strtoupper($browser_locale);
}
$possibleLocales[] = substr($browser_locale, 0, 5) . '.UTF-8';
}
// Attempt to load from environment variable.
$envLang = $environment->getLang();
if (null !== $envLang) {
$possibleLocales[] = $envLang;
}
$locale = self::getValidLocale($possibleLocales);
$locale->register($environment);
return $locale;
}
public static function createForCli(
Environment $environment
): self {
$locale = self::getValidLocale($environment->getLang());
$locale->register($environment);
return $locale;
}
public static function stripLocaleEncoding(string|self $locale): string
{
if ($locale instanceof self) {
$locale = $locale->value;
}
if (str_contains($locale, '.')) {
return explode('.', $locale, 2)[0];
}
return $locale;
}
public static function ensureLocaleEncoding(string|self $locale): string
{
return self::stripLocaleEncoding($locale) . '.UTF-8';
}
}

View File

@ -0,0 +1,19 @@
<?php
// phpcs:ignoreFile
declare(strict_types=1);
namespace App\Enums;
enum SupportedThemes: string
{
case Browser = 'browser';
case Light = 'light';
case Dark = 'dark';
public static function default(): self
{
return self::Browser;
}
}

View File

@ -69,11 +69,11 @@ class Environment
// Default settings
protected array $defaults = [
self::APP_NAME => 'AzuraCast',
self::APP_ENV => self::ENV_PRODUCTION,
self::APP_ENV => self::ENV_PRODUCTION,
self::LOG_LEVEL => LogLevel::NOTICE,
self::IS_DOCKER => true,
self::IS_CLI => ('cli' === PHP_SAPI),
self::IS_CLI => ('cli' === PHP_SAPI),
self::ASSET_URL => '/static',
@ -83,13 +83,11 @@ class Environment
self::ENABLE_REDIS => true,
self::SYNC_SHORT_EXECUTION_TIME => 600,
self::SYNC_LONG_EXECUTION_TIME => 1800,
self::SYNC_LONG_EXECUTION_TIME => 1800,
self::PROFILING_EXTENSION_ENABLED => 0,
self::PROFILING_EXTENSION_ENABLED => 0,
self::PROFILING_EXTENSION_ALWAYS_ON => 0,
self::PROFILING_EXTENSION_HTTP_KEY => 'dev',
self::LANG => Locale::DEFAULT_LOCALE,
self::PROFILING_EXTENSION_HTTP_KEY => 'dev',
];
public function __construct(array $elements = [])
@ -285,10 +283,10 @@ class Environment
public function getDatabaseSettings(): array
{
return [
'host' => $this->data[self::DB_HOST] ?? ($this->isDocker() ? 'mariadb' : 'localhost'),
'port' => (int)($this->data[self::DB_PORT] ?? 3306),
'dbname' => $this->data[self::DB_NAME] ?? 'azuracast',
'user' => $this->data[self::DB_USER] ?? 'azuracast',
'host' => $this->data[self::DB_HOST] ?? ($this->isDocker() ? 'mariadb' : 'localhost'),
'port' => (int)($this->data[self::DB_PORT] ?? 3306),
'dbname' => $this->data[self::DB_NAME] ?? 'azuracast',
'user' => $this->data[self::DB_USER] ?? 'azuracast',
'password' => $this->data[self::DB_PASSWORD] ?? 'azur4c457',
];
}
@ -306,7 +304,7 @@ class Environment
return [
'host' => $this->data[self::REDIS_HOST] ?? ($this->isDocker() ? 'redis' : 'localhost'),
'port' => (int)($this->data[self::REDIS_PORT] ?? 6379),
'db' => (int)($this->data[self::REDIS_DB] ?? 1),
'db' => (int)($this->data[self::REDIS_DB] ?? 1),
];
}

View File

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Event;
use App\Entity\Settings;
use App\Enums\PermissionInterface;
use App\Http\ServerRequest;
use Symfony\Contracts\EventDispatcher\Event;
@ -85,7 +86,7 @@ abstract class AbstractBuildMenu extends Event
return true;
}
public function checkPermission(string $permission_name): bool
public function checkPermission(string|PermissionInterface $permission_name): bool
{
return $this->request->getAcl()->isAllowed($permission_name);
}

View File

@ -6,6 +6,7 @@ namespace App\Event;
use App\Entity\Settings;
use App\Entity\Station;
use App\Enums\PermissionInterface;
use App\Http\ServerRequest;
class BuildStationMenu extends AbstractBuildMenu
@ -23,7 +24,7 @@ class BuildStationMenu extends AbstractBuildMenu
return $this->station;
}
public function checkPermission(string $permission_name): bool
public function checkPermission(string|PermissionInterface $permission_name): bool
{
return $this->request->getAcl()->isAllowed($permission_name, $this->station->getId());
}

View File

@ -8,8 +8,8 @@ use App\Acl;
use App\Auth;
use App\Customization;
use App\Entity;
use App\Enums\SupportedLocales;
use App\Exception;
use App\Locale;
use App\Radio;
use App\RateLimit;
use App\Session;
@ -65,9 +65,9 @@ final class ServerRequest extends \Slim\Http\ServerRequest
return $this->getAttributeOfClass(self::ATTR_RATE_LIMIT, RateLimit::class);
}
public function getLocale(): Locale
public function getLocale(): SupportedLocales
{
return $this->getAttributeOfClass(self::ATTR_LOCALE, Locale::class);
return $this->getAttributeOfClass(self::ATTR_LOCALE, SupportedLocales::class);
}
public function getCustomization(): Customization

View File

@ -4,11 +4,11 @@ declare(strict_types=1);
namespace App\Installer\Command;
use App\Enums\SupportedLocales;
use App\Environment;
use App\Installer\EnvFiles\AbstractEnvFile;
use App\Installer\EnvFiles\AzuraCastEnvFile;
use App\Installer\EnvFiles\EnvFile;
use App\Locale;
use App\Radio\Configuration;
use App\Utilities\Strings;
use InvalidArgumentException;
@ -88,19 +88,19 @@ class InstallCommand extends Command
// Initialize locale for translated installer/updater.
if (!$defaults && ($isNewInstall || empty($azuracastEnv[Environment::LANG]))) {
$langOptions = [];
foreach (Locale::SUPPORTED_LOCALES as $langKey => $langName) {
$langOptions[Locale::stripLocaleEncoding($langKey)] = $langName;
foreach (SupportedLocales::cases() as $supportedLocale) {
$langOptions[$supportedLocale->getLocaleWithoutEncoding()] = $supportedLocale->getLocalName();
}
$azuracastEnv[Environment::LANG] = $io->choice(
'Select Language',
$langOptions,
Locale::stripLocaleEncoding(Locale::DEFAULT_LOCALE)
SupportedLocales::default()->getLocaleWithoutEncoding()
);
}
$locale = new Locale($this->environment, $azuracastEnv[Environment::LANG] ?? Locale::DEFAULT_LOCALE);
$locale->register();
$locale = SupportedLocales::getValidLocale($azuracastEnv[Environment::LANG] ?? null);
$locale->register($this->environment);
$envConfig = EnvFile::getConfiguration();
$env->setFromDefaults();

View File

@ -4,8 +4,8 @@ declare(strict_types=1);
namespace App\Installer\EnvFiles;
use App\Enums\SupportedLocales;
use App\Environment;
use App\Locale;
use Psr\Log\LogLevel;
use function __;
@ -22,41 +22,41 @@ class AzuraCastEnvFile extends AbstractEnvFile
$defaults = $emptyEnv->toArray();
$langOptions = [];
foreach (Locale::SUPPORTED_LOCALES as $locale => $localeName) {
$langOptions[] = Locale::stripLocaleEncoding($locale);
foreach (SupportedLocales::cases() as $supportedLocale) {
$langOptions[] = $supportedLocale->getLocaleWithoutEncoding();
}
$dbSettings = $emptyEnv->getDatabaseSettings();
$redisSettings = $emptyEnv->getRedisSettings();
$config = [
Environment::LANG => [
'name' => __(
Environment::LANG => [
'name' => __(
'The locale to use for CLI commands.',
),
'options' => $langOptions,
'default' => Locale::stripLocaleEncoding(Locale::DEFAULT_LOCALE),
'options' => $langOptions,
'default' => SupportedLocales::default()->getLocaleWithoutEncoding(),
'required' => true,
],
Environment::APP_ENV => [
'name' => __(
Environment::APP_ENV => [
'name' => __(
'The application environment.',
),
'options' => [
'options' => [
Environment::ENV_PRODUCTION,
Environment::ENV_DEVELOPMENT,
Environment::ENV_TESTING,
],
'required' => true,
],
Environment::LOG_LEVEL => [
'name' => __(
Environment::LOG_LEVEL => [
'name' => __(
'Manually modify the logging level.',
),
'description' => __(
'This allows you to log debug-level errors temporarily (for problem-solving) or reduce the volume of logs that are produced by your installation, without needing to modify whether your installation is a production or development instance.'
),
'options' => [
'options' => [
LogLevel::DEBUG,
LogLevel::INFO,
LogLevel::NOTICE,
@ -67,168 +67,168 @@ class AzuraCastEnvFile extends AbstractEnvFile
LogLevel::EMERGENCY,
],
],
'COMPOSER_PLUGIN_MODE' => [
'name' => __('Composer Plugin Mode'),
'COMPOSER_PLUGIN_MODE' => [
'name' => __('Composer Plugin Mode'),
'description' => __(
'Enable the composer "merge" functionality to combine the main application\'s composer.json file with any plugin composer files. This can have performance implications, so you should only use it if you use one or more plugins with their own Composer dependencies.',
),
'options' => [true, false],
'default' => false,
'options' => [true, false],
'default' => false,
],
Environment::AUTO_ASSIGN_PORT_MIN => [
'name' => __(
Environment::AUTO_ASSIGN_PORT_MIN => [
'name' => __(
'Minimum Port for Station Port Assignment'
),
'description' => __(
'Modify this if your stations are listening on nonstandard ports.',
),
],
Environment::AUTO_ASSIGN_PORT_MAX => [
'name' => __(
Environment::AUTO_ASSIGN_PORT_MAX => [
'name' => __(
'Maximum Port for Station Port Assignment'
),
'description' => __(
'Modify this if your stations are listening on nonstandard ports.',
),
],
Environment::DB_HOST => [
'name' => __('MariaDB Host'),
Environment::DB_HOST => [
'name' => __('MariaDB Host'),
'description' => __(
'Do not modify this after installation.',
),
'default' => $dbSettings['host'],
'required' => true,
'default' => $dbSettings['host'],
'required' => true,
],
Environment::DB_PORT => [
'name' => __('MariaDB Port'),
Environment::DB_PORT => [
'name' => __('MariaDB Port'),
'description' => __(
'Do not modify this after installation.',
),
'default' => $dbSettings['port'],
'required' => true,
'default' => $dbSettings['port'],
'required' => true,
],
Environment::DB_USER => [
'name' => __('MariaDB Username'),
Environment::DB_USER => [
'name' => __('MariaDB Username'),
'description' => __(
'Do not modify this after installation.',
),
'default' => $dbSettings['user'],
'required' => true,
'default' => $dbSettings['user'],
'required' => true,
],
Environment::DB_PASSWORD => [
'name' => __('MariaDB Password'),
Environment::DB_PASSWORD => [
'name' => __('MariaDB Password'),
'description' => __(
'Do not modify this after installation.',
),
'default' => $dbSettings['password'],
'required' => true,
'default' => $dbSettings['password'],
'required' => true,
],
Environment::DB_NAME => [
'name' => __('MariaDB Database Name'),
Environment::DB_NAME => [
'name' => __('MariaDB Database Name'),
'description' => __(
'Do not modify this after installation.',
),
'default' => $dbSettings['dbname'],
'required' => true,
'default' => $dbSettings['dbname'],
'required' => true,
],
'MYSQL_RANDOM_ROOT_PASSWORD' => [
'name' => __('Auto-generate Random MariaDB Root Password'),
'MYSQL_RANDOM_ROOT_PASSWORD' => [
'name' => __('Auto-generate Random MariaDB Root Password'),
'description' => __(
'Do not modify this after installation.',
),
],
'MYSQL_ROOT_PASSWORD' => [
'name' => __('MariaDB Root Password'),
'MYSQL_ROOT_PASSWORD' => [
'name' => __('MariaDB Root Password'),
'description' => __(
'Do not modify this after installation.',
),
],
'MYSQL_SLOW_QUERY_LOG' => [
'name' => __('Enable MariaDB Slow Query Log'),
'MYSQL_SLOW_QUERY_LOG' => [
'name' => __('Enable MariaDB Slow Query Log'),
'description' => __(
'Log slower queries to diagnose possible database issues. Only turn this on if needed.',
),
'default' => 0,
'default' => 0,
],
'MYSQL_MAX_CONNECTIONS' => [
'name' => __('MariaDB Maximum Connections'),
'MYSQL_MAX_CONNECTIONS' => [
'name' => __('MariaDB Maximum Connections'),
'description' => __(
'Set the amount of allowed connections to the database. This value should be increased if you are seeing the "Too many connections" error in the logs.',
),
'default' => 100,
'default' => 100,
],
Environment::ENABLE_REDIS => [
'name' => __('Enable Redis'),
Environment::ENABLE_REDIS => [
'name' => __('Enable Redis'),
'description' => __(
'Disable to use a flatfile cache instead of Redis.',
),
],
Environment::REDIS_HOST => [
'name' => __('Redis Host'),
'default' => $redisSettings['host'],
Environment::REDIS_HOST => [
'name' => __('Redis Host'),
'default' => $redisSettings['host'],
'required' => true,
],
Environment::REDIS_PORT => [
'name' => __('Redis Port'),
'default' => $redisSettings['port'],
Environment::REDIS_PORT => [
'name' => __('Redis Port'),
'default' => $redisSettings['port'],
'required' => true,
],
Environment::REDIS_DB => [
'name' => __('Redis Database Index'),
'options' => range(0, 15),
'default' => $redisSettings['db'],
Environment::REDIS_DB => [
'name' => __('Redis Database Index'),
'options' => range(0, 15),
'default' => $redisSettings['db'],
'required' => true,
],
'PHP_MAX_FILE_SIZE' => [
'name' => __('PHP Maximum POST File Size'),
'PHP_MAX_FILE_SIZE' => [
'name' => __('PHP Maximum POST File Size'),
'default' => '25M',
],
'PHP_MEMORY_LIMIT' => [
'name' => __('PHP Memory Limit'),
'PHP_MEMORY_LIMIT' => [
'name' => __('PHP Memory Limit'),
'default' => '128M',
],
'PHP_MAX_EXECUTION_TIME' => [
'name' => __('PHP Script Maximum Execution Time'),
'PHP_MAX_EXECUTION_TIME' => [
'name' => __('PHP Script Maximum Execution Time'),
'description' => __('(in seconds)'),
'default' => 30,
'default' => 30,
],
Environment::SYNC_SHORT_EXECUTION_TIME => [
'name' => __('Short Sync Task Execution Time'),
Environment::SYNC_SHORT_EXECUTION_TIME => [
'name' => __('Short Sync Task Execution Time'),
'description' => __(
'The maximum execution time (and lock timeout) for the 15-second, 1-minute and 5-minute synchronization tasks.'
),
],
Environment::SYNC_LONG_EXECUTION_TIME => [
'name' => __('Long Sync Task Execution Time'),
Environment::SYNC_LONG_EXECUTION_TIME => [
'name' => __('Long Sync Task Execution Time'),
'description' => __(
'The maximum execution time (and lock timeout) for the 1-hour synchronization task.',
),
],
'PHP_FPM_MAX_CHILDREN' => [
'name' => __('Maximum PHP-FPM Worker Processes'),
'PHP_FPM_MAX_CHILDREN' => [
'name' => __('Maximum PHP-FPM Worker Processes'),
'default' => 5,
],
Environment::PROFILING_EXTENSION_ENABLED => [
'name' => __('Enable Performance Profiling Extension'),
Environment::PROFILING_EXTENSION_ENABLED => [
'name' => __('Enable Performance Profiling Extension'),
'description' => __(
'Profiling data can be viewed by visiting %s.',
'http://your-azuracast-site/?SPX_KEY=dev&SPX_UI_URI=/',
),
],
Environment::PROFILING_EXTENSION_ALWAYS_ON => [
'name' => __('Profile Performance on All Requests'),
'name' => __('Profile Performance on All Requests'),
'description' => __(
'This will have a significant performance impact on your installation.',
),
],
Environment::PROFILING_EXTENSION_HTTP_KEY => [
'name' => __('Profiling Extension HTTP Key'),
Environment::PROFILING_EXTENSION_HTTP_KEY => [
'name' => __('Profiling Extension HTTP Key'),
'description' => __(
'The value for the "SPX_KEY" parameter for viewing profiling pages.',
),
],
'PROFILING_EXTENSION_HTTP_IP_WHITELIST' => [
'name' => __('Profiling Extension IP Allow List'),
'PROFILING_EXTENSION_HTTP_IP_WHITELIST' => [
'name' => __('Profiling Extension IP Allow List'),
'options' => ['127.0.0.1', '*'],
'default' => '*',
],

View File

@ -1,170 +0,0 @@
<?php
declare(strict_types=1);
namespace App;
use App\Http\ServerRequest;
use Gettext\Translator;
use Psr\Http\Message\ServerRequestInterface;
class Locale
{
public const DEFAULT_LOCALE = 'en_US.UTF-8';
public const SUPPORTED_LOCALES = [
'en_US.UTF-8' => 'English (Default)',
'cs_CZ.UTF-8' => 'čeština', // Czech
'de_DE.UTF-8' => 'Deutsch', // German
'es_ES.UTF-8' => 'Español', // Spanish
'fr_FR.UTF-8' => 'Français', // French
'el_GR.UTF-8' => 'ελληνικά', // Greek
'it_IT.UTF-8' => 'Italiano', // Italian
'hu_HU.UTF-8' => 'magyar', // Hungarian
'nl_NL.UTF-8' => 'Nederlands', // Dutch
'pl_PL.UTF-8' => 'Polski', // Polish
'pt_PT.UTF-8' => 'Português', // Portuguese
'pt_BR.UTF-8' => 'Português do Brasil', // Brazilian Portuguese
'ru_RU.UTF-8' => 'Русский язык', // Russian
'sv_SE.UTF-8' => 'Svenska', // Swedish
'tr_TR.UTF-8' => 'Türkçe', // Turkish
'zh_CN.UTF-8' => '簡化字', // Simplified Chinese
'ko_KR.UTF-8' => '한국어', // Korean (South Korean)
];
protected string $locale = self::DEFAULT_LOCALE;
public function __construct(
protected Environment $environment,
string|array $possibleLocales
) {
if (is_string($possibleLocales)) {
$possibleLocales = [$possibleLocales];
}
$this->locale = $this->getValidLocale($possibleLocales);
}
protected function getValidLocale(array $possibleLocales): string
{
$supportedLocales = self::SUPPORTED_LOCALES;
foreach ($possibleLocales as $locale) {
$locale = self::ensureLocaleEncoding($locale);
// Prefer exact match.
if (isset($supportedLocales[$locale])) {
return $locale;
}
// Use approximate match if available.
foreach ($supportedLocales as $langCode => $langName) {
if (str_starts_with($locale, substr($langCode, 0, 2))) {
return $langCode;
}
}
}
return self::DEFAULT_LOCALE;
}
public function getLocale(): string
{
return $this->locale;
}
/**
* @return string A shortened locale (minus .UTF-8).
*/
public function getLocaleWithoutEncoding(): string
{
return self::stripLocaleEncoding($this->locale);
}
public function setLocale(string $newLocale = self::DEFAULT_LOCALE): void
{
$this->locale = $newLocale;
}
public function createTranslator(): Translator
{
$translator = new Translator();
$localeBase = $this->environment->getBaseDirectory() . '/resources/locale/compiled';
$localePath = $localeBase . '/' . $this->locale . '.php';
if (file_exists($localePath)) {
$translator->loadTranslations($localePath);
}
return $translator;
}
public function register(): void
{
$translator = $this->createTranslator();
$translator->register();
// Register translation superglobal functions
setlocale(LC_ALL, $this->locale);
}
public function __toString(): string
{
return $this->locale;
}
public static function createFromRequest(
Environment $environment,
ServerRequestInterface $request
): self {
$possibleLocales = [];
// Prefer user-based profile locale.
$user = $request->getAttribute(ServerRequest::ATTR_USER);
if (null !== $user && !empty($user->getLocale()) && 'default' !== $user->getLocale()) {
$possibleLocales[] = $user->getLocale();
}
$server_params = $request->getServerParams();
$browser_locale = \Locale::acceptFromHttp($server_params['HTTP_ACCEPT_LANGUAGE'] ?? '');
if (!empty($browser_locale)) {
if (2 === strlen($browser_locale)) {
$browser_locale = strtolower($browser_locale) . '_' . strtoupper($browser_locale);
}
$possibleLocales[] = substr($browser_locale, 0, 5) . '.UTF-8';
}
// Attempt to load from environment variable.
$envLang = $environment->getLang();
if (null !== $envLang) {
$possibleLocales[] = $envLang;
}
return new self($environment, $possibleLocales);
}
public static function createForCli(
Environment $environment
): self {
return new self(
$environment,
$environment->getLang() ?? self::DEFAULT_LOCALE
);
}
public static function stripLocaleEncoding(string $locale): string
{
if (str_contains($locale, '.')) {
return explode('.', $locale, 2)[0];
}
return $locale;
}
public static function ensureLocaleEncoding(string $locale): string
{
return self::stripLocaleEncoding($locale) . '.UTF-8';
}
}

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Middleware;
use App\Enums\PermissionInterface;
use App\Exception\PermissionDeniedException;
use App\Http\ServerRequest;
use Exception;
@ -16,7 +17,7 @@ use Psr\Http\Server\RequestHandlerInterface;
class Permissions
{
public function __construct(
protected string $action,
protected string|PermissionInterface $action,
protected bool $use_station = false
) {
}

View File

@ -9,6 +9,7 @@ use App\Entity\PodcastEpisode;
use App\Entity\Repository\PodcastRepository;
use App\Entity\Station;
use App\Entity\User;
use App\Enums\StationPermissions;
use App\Exception\PodcastNotFoundException;
use App\Http\Response;
use App\Http\ServerRequest;
@ -66,7 +67,7 @@ class RequirePublishedPodcastEpisodeMiddleware
protected function canUserManageStationPodcasts(User $user, Station $station, Acl $acl): bool
{
return $acl->userAllowed($user, Acl::STATION_PODCASTS, $station->getId());
return $acl->userAllowed($user, StationPermissions::Podcasts, $station->getId());
}
protected function getPodcastIdFromRequest(ServerRequest $request): ?string

View File

@ -4,8 +4,8 @@ declare(strict_types=1);
namespace App\Notification\Check;
use App\Acl;
use App\Entity\Api\Notification;
use App\Enums\GlobalPermissions;
use App\Environment;
use App\Event\GetNotifications;
use App\Session\Flash;
@ -22,7 +22,7 @@ class ComposeVersionCheck
{
// This notification is for full administrators only.
$acl = $event->getRequest()->getAcl();
if (!$acl->isAllowed(Acl::GLOBAL_ALL)) {
if (!$acl->isAllowed(GlobalPermissions::All)) {
return;
}

View File

@ -4,8 +4,8 @@ declare(strict_types=1);
namespace App\Notification\Check;
use App\Acl;
use App\Entity\Api\Notification;
use App\Enums\GlobalPermissions;
use App\Environment;
use App\Event\GetNotifications;
use App\Session\Flash;
@ -21,7 +21,7 @@ class ProfilerAdvisorCheck
{
// This notification is for full administrators only.
$acl = $event->getRequest()->getAcl();
if (!$acl->isAllowed(Acl::GLOBAL_ALL)) {
if (!$acl->isAllowed(GlobalPermissions::All)) {
return;
}

View File

@ -4,8 +4,8 @@ declare(strict_types=1);
namespace App\Notification\Check;
use App\Acl;
use App\Entity;
use App\Enums\GlobalPermissions;
use App\Environment;
use App\Event\GetNotifications;
use App\Session\Flash;
@ -24,7 +24,7 @@ class RecentBackupCheck
// This notification is for backup administrators only.
$request = $event->getRequest();
$acl = $request->getAcl();
if (!$acl->isAllowed(Acl::GLOBAL_BACKUPS)) {
if (!$acl->isAllowed(GlobalPermissions::Backups)) {
return;
}

View File

@ -4,8 +4,8 @@ declare(strict_types=1);
namespace App\Notification\Check;
use App\Acl;
use App\Entity;
use App\Enums\GlobalPermissions;
use App\Event\GetNotifications;
use App\Session\Flash;
@ -21,7 +21,7 @@ class SyncTaskCheck
// This notification is for full administrators only.
$request = $event->getRequest();
$acl = $request->getAcl();
if (!$acl->isAllowed(Acl::GLOBAL_ALL)) {
if (!$acl->isAllowed(GlobalPermissions::All)) {
return;
}

View File

@ -4,8 +4,8 @@ declare(strict_types=1);
namespace App\Notification\Check;
use App\Acl;
use App\Entity;
use App\Enums\GlobalPermissions;
use App\Event\GetNotifications;
use App\Session\Flash;
use App\Version;
@ -22,7 +22,7 @@ class UpdateCheck
{
// This notification is for full administrators only.
$acl = $event->getRequest()->getAcl();
if (!$acl->isAllowed(Acl::GLOBAL_ALL)) {
if (!$acl->isAllowed(GlobalPermissions::All)) {
return;
}

View File

@ -6,6 +6,10 @@ namespace App\Radio;
use App\Entity;
use App\Exception\NotFoundException;
use App\Radio\Enums\AdapterTypeInterface;
use App\Radio\Enums\BackendAdapters;
use App\Radio\Enums\FrontendAdapters;
use App\Radio\Enums\RemoteAdapters;
use Psr\Container\ContainerInterface;
/**
@ -13,18 +17,6 @@ use Psr\Container\ContainerInterface;
*/
class Adapters
{
public const FRONTEND_ICECAST = 'icecast';
public const FRONTEND_SHOUTCAST = 'shoutcast2';
public const FRONTEND_REMOTE = 'remote';
public const BACKEND_LIQUIDSOAP = 'liquidsoap';
public const BACKEND_NONE = 'none';
public const REMOTE_SHOUTCAST1 = 'shoutcast1';
public const REMOTE_SHOUTCAST2 = 'shoutcast2';
public const REMOTE_ICECAST = 'icecast';
public const REMOTE_AZURARELAY = 'azurarelay';
public function __construct(
protected ContainerInterface $adapters
) {
@ -37,16 +29,7 @@ class Adapters
*/
public function getFrontendAdapter(Entity\Station $station): Frontend\AbstractFrontend
{
$adapters = $this->listFrontendAdapters();
$frontend_type = $station->getFrontendType();
if (!isset($adapters[$frontend_type])) {
throw new NotFoundException('Adapter not found: ' . $frontend_type);
}
$class_name = $adapters[$frontend_type]['class'];
$class_name = $station->getFrontendTypeEnum()->getClass();
if ($this->adapters->has($class_name)) {
return $this->adapters->get($class_name);
}
@ -55,39 +38,12 @@ class Adapters
}
/**
* @param bool $check_installed
*
* @param bool $checkInstalled
* @return mixed[]
*/
public function listFrontendAdapters(bool $check_installed = false): array
public function listFrontendAdapters(bool $checkInstalled = false): array
{
$adapters = [
self::FRONTEND_ICECAST => [
'name' => 'Icecast 2.4',
'class' => Frontend\Icecast::class,
],
self::FRONTEND_SHOUTCAST => [
'name' => 'SHOUTcast DNAS 2',
'class' => Frontend\SHOUTcast::class,
],
self::FRONTEND_REMOTE => [
'name' => 'Remote',
'class' => Frontend\Remote::class,
],
];
if ($check_installed) {
return array_filter(
$adapters,
function ($adapter_info) {
/** @var AbstractAdapter $adapter */
$adapter = $this->adapters->get($adapter_info['class']);
return $adapter->isInstalled();
}
);
}
return $adapters;
return $this->listAdaptersFromEnum(FrontendAdapters::cases(), $checkInstalled);
}
/**
@ -97,16 +53,7 @@ class Adapters
*/
public function getBackendAdapter(Entity\Station $station): Backend\AbstractBackend
{
$adapters = $this->listBackendAdapters();
$backend_type = $station->getBackendType();
if (!isset($adapters[$backend_type])) {
throw new NotFoundException('Adapter not found: ' . $backend_type);
}
$class_name = $adapters[$backend_type]['class'];
$class_name = $station->getBackendTypeEnum()->getClass();
if ($this->adapters->has($class_name)) {
return $this->adapters->get($class_name);
}
@ -115,35 +62,12 @@ class Adapters
}
/**
* @param bool $check_installed
*
* @param bool $checkInstalled
* @return mixed[]
*/
public function listBackendAdapters(bool $check_installed = false): array
public function listBackendAdapters(bool $checkInstalled = false): array
{
$adapters = [
self::BACKEND_LIQUIDSOAP => [
'name' => 'Liquidsoap',
'class' => Backend\Liquidsoap::class,
],
self::BACKEND_NONE => [
'name' => 'Disabled',
'class' => Backend\None::class,
],
];
if ($check_installed) {
return array_filter(
$adapters,
function ($adapter_info) {
/** @var AbstractAdapter $adapter */
$adapter = $this->adapters->get($adapter_info['class']);
return $adapter->isInstalled();
}
);
}
return $adapters;
return $this->listAdaptersFromEnum(BackendAdapters::cases(), $checkInstalled);
}
/**
@ -155,34 +79,15 @@ class Adapters
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;
}
/**
* Assemble an array of ready-to-operate
*
* @param Entity\Station $station
* @param Entity\StationRemote $remote
*
* @throws NotFoundException
*/
public function getRemoteAdapter(Entity\Station $station, Entity\StationRemote $remote): Remote\AbstractRemote
{
$adapters = $this->listRemoteAdapters();
$remote_type = $remote->getType();
if (!isset($adapters[$remote_type])) {
throw new NotFoundException('Adapter not found: ' . $remote_type);
}
$class_name = $adapters[$remote_type]['class'];
$class_name = $remote->getTypeEnum()->getClass();
if ($this->adapters->has($class_name)) {
return $this->adapters->get($class_name);
}
@ -195,23 +100,36 @@ class Adapters
*/
public function listRemoteAdapters(): array
{
return [
self::REMOTE_SHOUTCAST1 => [
'name' => 'SHOUTcast 1',
'class' => Remote\SHOUTcast1::class,
],
self::REMOTE_SHOUTCAST2 => [
'name' => 'SHOUTcast 2',
'class' => Remote\SHOUTcast2::class,
],
self::REMOTE_ICECAST => [
'name' => 'Icecast',
'class' => Remote\Icecast::class,
],
self::REMOTE_AZURARELAY => [
'name' => 'AzuraRelay',
'class' => Remote\AzuraRelay::class,
],
];
return $this->listAdaptersFromEnum(RemoteAdapters::cases());
}
/**
* @param array<AdapterTypeInterface> $cases
* @param bool $checkInstalled
* @return mixed[]
*/
protected function listAdaptersFromEnum(array $cases, bool $checkInstalled = false): array
{
$adapters = [];
foreach ($cases as $adapter) {
$adapters[$adapter->getValue()] = [
'enum' => $adapter,
'name' => $adapter->getName(),
'class' => $adapter->getClass(),
];
}
if ($checkInstalled) {
return array_filter(
$adapters,
function ($adapter_info) {
/** @var AbstractAdapter $adapter */
$adapter = $this->adapters->get($adapter_info['class']);
return $adapter->isInstalled();
}
);
}
return $adapters;
}
}

View File

@ -19,17 +19,6 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class Queue implements EventSubscriberInterface
{
protected const TYPES_TO_PLAY_BY_PRIORITY = [
Entity\StationPlaylist::TYPE_ONCE_PER_HOUR . '_scheduled',
Entity\StationPlaylist::TYPE_ONCE_PER_HOUR . '_unscheduled',
Entity\StationPlaylist::TYPE_ONCE_PER_X_SONGS . '_scheduled',
Entity\StationPlaylist::TYPE_ONCE_PER_X_SONGS . '_unscheduled',
Entity\StationPlaylist::TYPE_ONCE_PER_X_MINUTES . '_scheduled',
Entity\StationPlaylist::TYPE_ONCE_PER_X_MINUTES . '_unscheduled',
Entity\StationPlaylist::TYPE_DEFAULT . '_scheduled',
Entity\StationPlaylist::TYPE_DEFAULT . '_unscheduled',
];
public function __construct(
protected EntityManagerInterface $em,
protected Logger $logger,
@ -245,7 +234,19 @@ class Queue implements EventSubscriberInterface
$recentSongHistoryForDuplicatePrevention
);
foreach (self::TYPES_TO_PLAY_BY_PRIORITY as $currentPlaylistType) {
$typesToPlay = [
Entity\Enums\PlaylistTypes::OncePerHour->value,
Entity\Enums\PlaylistTypes::OncePerXSongs->value,
Entity\Enums\PlaylistTypes::OncePerXMinutes->value,
Entity\Enums\PlaylistTypes::Standard->value,
];
$typesToPlayByPriority = [];
foreach ($typesToPlay as $type) {
$typesToPlayByPriority[] = $type . '_scheduled';
$typesToPlayByPriority[] = $type . '_unscheduled';
}
foreach ($typesToPlayByPriority as $currentPlaylistType) {
if (empty($activePlaylistsByType[$currentPlaylistType])) {
continue;
}
@ -313,7 +314,7 @@ class Queue implements EventSubscriberInterface
if ($playlist->isPlayable()) {
$type = $playlist->getType();
if (Entity\StationPlaylist::TYPE_ONCE_PER_X_SONGS === $type) {
if (Entity\Enums\PlaylistTypes::OncePerXSongs === $playlist->getTypeEnum()) {
$oncePerXSongHistoryCount = max($oncePerXSongHistoryCount, $playlist->getPlayPerSongs());
}
@ -432,23 +433,22 @@ class Queue implements EventSubscriberInterface
CarbonInterface $expectedPlayTime,
bool $allowDuplicates = false
): ?Entity\StationQueue {
if (Entity\StationPlaylist::SOURCE_REMOTE_URL === $playlist->getSource()) {
if (Entity\Enums\PlaylistSources::RemoteUrl === $playlist->getSourceEnum()) {
return $this->getSongFromRemotePlaylist($playlist, $expectedPlayTime);
}
$validTrack = match ($playlist->getOrder()) {
$playlist::ORDER_RANDOM => $this->getRandomMediaIdFromPlaylist(
$validTrack = match ($playlist->getOrderEnum()) {
Entity\Enums\PlaylistOrders::Random => $this->getRandomMediaIdFromPlaylist(
$playlist,
$recentSongHistory,
$allowDuplicates
),
$playlist::ORDER_SEQUENTIAL => $this->getSequentialMediaIdFromPlaylist($playlist),
$playlist::ORDER_SHUFFLE => $this->getShuffledMediaIdFromPlaylist(
Entity\Enums\PlaylistOrders::Sequential => $this->getSequentialMediaIdFromPlaylist($playlist),
Entity\Enums\PlaylistOrders::Shuffle => $this->getShuffledMediaIdFromPlaylist(
$playlist,
$recentSongHistory,
$allowDuplicates
),
default => null
)
};
if (null === $validTrack) {
@ -521,10 +521,10 @@ class Queue implements EventSubscriberInterface
*/
protected function getMediaFromRemoteUrl(Entity\StationPlaylist $playlist): ?array
{
$remoteType = $playlist->getRemoteType() ?? Entity\StationPlaylist::REMOTE_TYPE_STREAM;
$remoteType = $playlist->getRemoteTypeEnum() ?? Entity\Enums\PlaylistRemoteTypes::Stream;
// Handle a raw stream URL of possibly indeterminate length.
if (Entity\StationPlaylist::REMOTE_TYPE_STREAM === $remoteType) {
if (Entity\Enums\PlaylistRemoteTypes::Stream === $remoteType) {
// Annotate a hard-coded "duration" parameter to avoid infinite play for scheduled playlists.
$duration = $this->scheduler->getPlaylistScheduleDuration($playlist);
return [$playlist->getRemoteUrl(), $duration];

View File

@ -51,42 +51,51 @@ class Scheduler
$shouldPlay = true;
switch ($playlist->getType()) {
case Entity\StationPlaylist::TYPE_ONCE_PER_HOUR:
switch ($playlist->getTypeEnum()) {
case Entity\Enums\PlaylistTypes::OncePerHour:
$shouldPlay = $this->shouldPlaylistPlayNowPerHour($playlist, $now);
$this->logger->debug(sprintf(
'Once-per-hour playlist %s been played yet this hour.',
$shouldPlay ? 'HAS NOT' : 'HAS'
));
$this->logger->debug(
sprintf(
'Once-per-hour playlist %s been played yet this hour.',
$shouldPlay ? 'HAS NOT' : 'HAS'
)
);
break;
case Entity\StationPlaylist::TYPE_ONCE_PER_X_SONGS:
case Entity\Enums\PlaylistTypes::OncePerXSongs:
$playPerSongs = $playlist->getPlayPerSongs();
$shouldPlay = !$this->wasPlaylistPlayedRecently($playlist, $recentPlaylistHistory, $playPerSongs);
$this->logger->debug(sprintf(
'Once-per-X-songs playlist %s been played within the last %d song(s).',
$shouldPlay ? 'HAS NOT' : 'HAS',
$playPerSongs
));
$this->logger->debug(
sprintf(
'Once-per-X-songs playlist %s been played within the last %d song(s).',
$shouldPlay ? 'HAS NOT' : 'HAS',
$playPerSongs
)
);
break;
case Entity\StationPlaylist::TYPE_ONCE_PER_X_MINUTES:
case Entity\Enums\PlaylistTypes::OncePerXMinutes:
$playPerMinutes = $playlist->getPlayPerMinutes();
$shouldPlay = !$this->wasPlaylistPlayedInLastXMinutes($playlist, $now, $playPerMinutes);
$this->logger->debug(sprintf(
'Once-per-X-minutes playlist %s been played within the last %d minute(s).',
$shouldPlay ? 'HAS NOT' : 'HAS',
$playPerMinutes
));
$this->logger->debug(
sprintf(
'Once-per-X-minutes playlist %s been played within the last %d minute(s).',
$shouldPlay ? 'HAS NOT' : 'HAS',
$playPerMinutes
)
);
break;
case Entity\StationPlaylist::TYPE_ADVANCED:
case Entity\Enums\PlaylistTypes::Advanced:
$this->logger->debug('Playlist is "Advanced" type and is not managed by the AutoDJ.');
$shouldPlay = false;
break;
case Entity\Enums\PlaylistTypes::Standard:
break;
}
$this->logger->popProcessor();

View File

@ -10,8 +10,10 @@ use App\Event\Radio\WriteLiquidsoapConfiguration;
use App\Exception;
use App\Flysystem\StationFilesystems;
use App\Message;
use App\Radio\Adapters;
use App\Radio\Backend\Liquidsoap;
use App\Radio\Enums\FrontendAdapters;
use App\Radio\Enums\StreamFormats;
use App\Radio\Enums\StreamProtocols;
use Doctrine\ORM\EntityManagerInterface;
use League\Flysystem\StorageAttributes;
use Psr\Log\LoggerInterface;
@ -225,7 +227,7 @@ class ConfigWriter implements EventSubscriberInterface
$playlistConfigLines = [];
if (Entity\StationPlaylist::SOURCE_SONGS === $playlist->getSource()) {
if (Entity\Enums\PlaylistSources::Songs === $playlist->getSourceEnum()) {
$playlistFilePath = $this->writePlaylistFile($playlist, false);
if (!$playlistFilePath) {
continue;
@ -236,13 +238,12 @@ class ConfigWriter implements EventSubscriberInterface
'mime_type="audio/x-mpegurl"',
];
$playlistModes = [
Entity\StationPlaylist::ORDER_SEQUENTIAL => 'normal',
Entity\StationPlaylist::ORDER_SHUFFLE => 'randomize',
Entity\StationPlaylist::ORDER_RANDOM => 'random',
];
$playlistParams[] = 'mode="' . $playlistModes[$playlist->getOrder()] . '"';
$playlistMode = match ($playlist->getOrderEnum()) {
Entity\Enums\PlaylistOrders::Sequential => 'normal',
Entity\Enums\PlaylistOrders::Shuffle => 'randomize',
Entity\Enums\PlaylistOrders::Random => 'random'
};
$playlistParams[] = 'mode="' . $playlistMode . '"';
if ($playlist->backendLoopPlaylistOnce()) {
$playlistParams[] = 'reload_mode="never"';
@ -263,15 +264,15 @@ class ConfigWriter implements EventSubscriberInterface
$playlistConfigLines[] = $playlistVarName . ' = cue_cut(id="cue_'
. self::cleanUpString($playlistVarName) . '", ' . $playlistVarName . ')';
} else {
switch ($playlist->getRemoteType()) {
case Entity\StationPlaylist::REMOTE_TYPE_PLAYLIST:
switch ($playlist->getRemoteTypeEnum()) {
case Entity\Enums\PlaylistRemoteTypes::Playlist:
$playlistFunc = 'playlist("'
. self::cleanUpString($playlist->getRemoteUrl())
. '")';
$playlistConfigLines[] = $playlistVarName . ' = ' . $playlistFunc;
break;
case Entity\StationPlaylist::REMOTE_TYPE_STREAM:
case Entity\Enums\PlaylistRemoteTypes::Stream:
default:
$remote_url = $playlist->getRemoteUrl();
if (null !== $remote_url) {
@ -290,7 +291,7 @@ class ConfigWriter implements EventSubscriberInterface
$playlistConfigLines[] = $playlistVarName . ' = drop_metadata(' . $playlistVarName . ')';
}
if (Entity\StationPlaylist::TYPE_ADVANCED === $playlist->getType()) {
if (Entity\Enums\PlaylistTypes::Advanced === $playlist->getTypeEnum()) {
$playlistConfigLines[] = 'ignore(' . $playlistVarName . ')';
}
@ -302,8 +303,8 @@ class ConfigWriter implements EventSubscriberInterface
$scheduleItems = $playlist->getScheduleItems();
switch ($playlist->getType()) {
case Entity\StationPlaylist::TYPE_DEFAULT:
switch ($playlist->getTypeEnum()) {
case Entity\Enums\PlaylistTypes::Standard:
if ($scheduleItems->count() > 0) {
foreach ($scheduleItems as $scheduleItem) {
$play_time = $this->getScheduledPlaylistPlayTime($scheduleItem);
@ -321,9 +322,9 @@ class ConfigWriter implements EventSubscriberInterface
}
break;
case Entity\StationPlaylist::TYPE_ONCE_PER_X_SONGS:
case Entity\StationPlaylist::TYPE_ONCE_PER_X_MINUTES:
if (Entity\StationPlaylist::TYPE_ONCE_PER_X_SONGS === $playlist->getType()) {
case Entity\Enums\PlaylistTypes::OncePerXSongs:
case Entity\Enums\PlaylistTypes::OncePerXMinutes:
if (Entity\Enums\PlaylistTypes::OncePerXSongs === $playlist->getTypeEnum()) {
$playlistScheduleVar = 'rotate(weights=[1,'
. $playlist->getPlayPerSongs() . '], [' . $playlistVarName . ', radio])';
} else {
@ -349,7 +350,7 @@ class ConfigWriter implements EventSubscriberInterface
}
break;
case Entity\StationPlaylist::TYPE_ONCE_PER_HOUR:
case Entity\Enums\PlaylistTypes::OncePerHour:
$minutePlayTime = $playlist->getPlayPerHourMinute() . 'm';
if ($scheduleItems->count() > 0) {
@ -849,7 +850,7 @@ class ConfigWriter implements EventSubscriberInterface
);
if ($recordLiveStreams) {
$recordLiveStreamsFormat = $settings['record_streams_format'] ?? Entity\Interfaces\StationMountInterface::FORMAT_MP3;
$recordLiveStreamsFormat = $settings->getRecordStreamsFormatEnum() ?? StreamFormats::Mp3;
$recordLiveStreamsBitrate = (int)($settings['record_streams_bitrate'] ?? 128);
$formatString = $this->getOutputFormatString($recordLiveStreamsFormat, $recordLiveStreamsBitrate);
@ -963,7 +964,7 @@ class ConfigWriter implements EventSubscriberInterface
{
$station = $event->getStation();
if (Adapters::FRONTEND_REMOTE === $station->getFrontendType()) {
if (FrontendAdapters::Remote === $station->getFrontendTypeEnum()) {
return;
}
@ -998,8 +999,9 @@ class ConfigWriter implements EventSubscriberInterface
): string {
$charset = $station->getBackendConfig()->getCharset();
$format = $mount->getAutodjFormatEnum() ?? StreamFormats::Mp3;
$output_format = $this->getOutputFormatString(
$mount->getAutodjFormat() ?? $mount::FORMAT_MP3,
$format,
$mount->getAutodjBitrate() ?? 128
);
@ -1015,16 +1017,16 @@ class ConfigWriter implements EventSubscriberInterface
$output_params[] = 'user = "' . self::cleanUpString($username) . '"';
}
$protocol = $mount->getAutodjProtocolEnum();
$password = self::cleanUpString($mount->getAutodjPassword());
if (Adapters::REMOTE_SHOUTCAST2 === $mount->getAutodjAdapterType()) {
if (StreamProtocols::Icy === $protocol) {
$password .= ':#' . $id;
}
$output_params[] = 'password = "' . $password . '"';
$protocol = $mount->getAutodjProtocol();
if (!empty($mount->getAutodjMount())) {
if ($mount::PROTOCOL_ICY === $protocol) {
if (StreamProtocols::Icy === $protocol) {
$output_params[] = 'icy_id = ' . $id;
} else {
$output_params[] = 'mount = "' . self::cleanUpString($mount->getAutodjMount()) . '"';
@ -1043,13 +1045,10 @@ class ConfigWriter implements EventSubscriberInterface
$output_params[] = 'encoding = "' . $charset . '"';
if (null !== $protocol) {
$output_params[] = 'protocol="' . $protocol . '"';
$output_params[] = 'protocol="' . $protocol->value . '"';
}
if (
Entity\Interfaces\StationMountInterface::FORMAT_OPUS === $mount->getAutodjFormat()
|| Entity\Interfaces\StationMountInterface::FORMAT_FLAC === $mount->getAutodjFormat()
) {
if ($format->sendIcyMetadata()) {
$output_params[] = 'icy_metadata="true"';
}
@ -1058,25 +1057,25 @@ class ConfigWriter implements EventSubscriberInterface
return 'output.icecast(' . implode(', ', $output_params) . ')';
}
protected function getOutputFormatString(string $format, int $bitrate = 128): string
protected function getOutputFormatString(StreamFormats $format, int $bitrate = 128): string
{
switch (strtolower($format)) {
case Entity\Interfaces\StationMountInterface::FORMAT_AAC:
switch ($format) {
case StreamFormats::Aac:
$afterburner = ($bitrate >= 160) ? 'true' : 'false';
$aot = ($bitrate >= 96) ? 'mpeg4_aac_lc' : 'mpeg4_he_aac_v2';
return '%fdkaac(channels=2, samplerate=44100, bitrate=' . $bitrate . ', afterburner=' . $afterburner . ', aot="' . $aot . '", sbr_mode=true)';
case Entity\Interfaces\StationMountInterface::FORMAT_OGG:
case StreamFormats::Ogg:
return '%vorbis.cbr(samplerate=44100, channels=2, bitrate=' . $bitrate . ')';
case Entity\Interfaces\StationMountInterface::FORMAT_OPUS:
case StreamFormats::Opus:
return '%opus(samplerate=48000, bitrate=' . $bitrate . ', vbr="constrained", application="audio", channels=2, signal="music", complexity=10, max_bandwidth="full_band")';
case Entity\Interfaces\StationMountInterface::FORMAT_FLAC:
case StreamFormats::Flac:
return '%ogg(%flac(samplerate=48000, channels=2, compression=4, bits_per_sample=24))';
case Entity\Interfaces\StationMountInterface::FORMAT_MP3:
case StreamFormats::Mp3:
default:
return '%mp3(samplerate=44100, stereo=true, bitrate=' . $bitrate . ', id3v2=true)';
}

View File

@ -4,10 +4,13 @@ declare(strict_types=1);
namespace App\Radio;
use App\Entity\Enums\PlaylistTypes;
use App\Entity\Station;
use App\Entity\StationPlaylist;
use App\Environment;
use App\Exception;
use App\Radio\Enums\BackendAdapters;
use App\Radio\Enums\FrontendAdapters;
use Doctrine\ORM\EntityManagerInterface;
use Monolog\Logger;
use Supervisor\Exception\SupervisorException;
@ -48,7 +51,7 @@ class Configuration
// Check for at least one playlist, and create one if it doesn't exist.
$defaultPlaylists = $station->getPlaylists()->filter(
function (StationPlaylist $row) {
return $row->getIsEnabled() && StationPlaylist::TYPE_DEFAULT === $row->getType();
return $row->getIsEnabled() && PlaylistTypes::default() === $row->getTypeEnum();
}
);
@ -266,8 +269,8 @@ class Configuration
public function assignRadioPorts(Station $station, bool $force = false): void
{
if (
$station->getFrontendType() !== Adapters::FRONTEND_REMOTE
|| $station->getBackendType() !== Adapters::BACKEND_NONE
FrontendAdapters::Remote !== $station->getFrontendTypeEnum()
|| BackendAdapters::Liquidsoap !== $station->getBackendTypeEnum()
) {
$frontend_config = $station->getFrontendConfig();
$backend_config = $station->getBackendConfig();
@ -360,7 +363,7 @@ class Configuration
foreach ($station_configs as $row) {
$station_reference = ['id' => $row['id'], 'name' => $row['name']];
if ($row['frontend_type'] !== Adapters::FRONTEND_REMOTE) {
if ($row['frontend_type'] !== FrontendAdapters::Remote->value) {
$frontend_config = (array)$row['frontend_config'];
if (!empty($frontend_config['port'])) {
@ -369,7 +372,7 @@ class Configuration
}
}
if ($row['backend_type'] !== Adapters::BACKEND_NONE) {
if ($row['backend_type'] !== BackendAdapters::None->value) {
$backend_config = (array)$row['backend_config'];
// For DJ port, consider both the assigned port and port+1 to be reserved and in-use.
@ -406,15 +409,15 @@ class Configuration
[, $program_name] = explode(':', $adapter->getProgramName($station));
$config_lines = [
'user' => 'azuracast',
'priority' => $priority ?? 50,
'command' => $adapter->getCommand($station),
'directory' => $station->getRadioConfigDir(),
'environment' => 'TZ="' . $station->getTimezone() . '"',
'stdout_logfile' => $adapter->getLogPath($station),
'user' => 'azuracast',
'priority' => $priority ?? 50,
'command' => $adapter->getCommand($station),
'directory' => $station->getRadioConfigDir(),
'environment' => 'TZ="' . $station->getTimezone() . '"',
'stdout_logfile' => $adapter->getLogPath($station),
'stdout_logfile_maxbytes' => '5MB',
'stdout_logfile_backups' => '10',
'redirect_stderr' => 'true',
'stdout_logfile_backups' => '10',
'redirect_stderr' => 'true',
];
$supervisor_config[] = '[program:' . $program_name . ']';

View File

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace App\Radio\Enums;
interface AdapterTypeInterface
{
public function getValue(): string;
public function getName(): string;
public function getClass(): string;
}

View File

@ -0,0 +1,42 @@
<?php
// phpcs:ignoreFile
declare(strict_types=1);
namespace App\Radio\Enums;
use App\Radio\Backend\Liquidsoap;
use App\Radio\Backend\None;
enum BackendAdapters: string implements AdapterTypeInterface
{
case Liquidsoap = 'liquidsoap';
case None = 'none';
public function getValue(): string
{
return $this->value;
}
public function getName(): string
{
return match ($this) {
self::Liquidsoap => 'Liquidsoap',
self::None => 'Disabled',
};
}
public function getClass(): string
{
return match ($this) {
self::Liquidsoap => Liquidsoap::class,
self::None => None::class,
};
}
public static function default(): self
{
return self::Liquidsoap;
}
}

View File

@ -0,0 +1,46 @@
<?php
// phpcs:ignoreFile
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
{
case Icecast = 'icecast';
case SHOUTcast = 'shoutcast2';
case Remote = 'remote';
public function getValue(): string
{
return $this->value;
}
public function getName(): string
{
return match ($this) {
self::Icecast => 'Icecast 2.4',
self::SHOUTcast => 'SHOUTcast DNAS 2',
self::Remote => 'Remote',
};
}
public function getClass(): string
{
return match ($this) {
self::Icecast => Icecast::class,
self::SHOUTcast => SHOUTcast::class,
self::Remote => Remote::class,
};
}
public static function default(): self
{
return self::Icecast;
}
}

View File

@ -0,0 +1,45 @@
<?php
// phpcs:ignoreFile
declare(strict_types=1);
namespace App\Radio\Enums;
use App\Radio\Remote\AzuraRelay;
use App\Radio\Remote\Icecast;
use App\Radio\Remote\SHOUTcast1;
use App\Radio\Remote\SHOUTcast2;
enum RemoteAdapters: string implements AdapterTypeInterface
{
case SHOUTcast1 = 'shoutcast1';
case SHOUTcast2 = 'shoutcast2';
case Icecast = 'icecast';
case AzuraRelay = 'azurarelay';
public function getValue(): string
{
return $this->value;
}
public function getName(): string
{
return match ($this) {
self::SHOUTcast1 => 'SHOUTcast 1',
self::SHOUTcast2 => 'SHOUTcast 2',
self::Icecast => 'Icecast',
self::AzuraRelay => 'AzuraRelay',
};
}
public function getClass(): string
{
return match ($this) {
self::SHOUTcast1 => SHOUTcast1::class,
self::SHOUTcast2 => SHOUTcast2::class,
self::Icecast => Icecast::class,
self::AzuraRelay => AzuraRelay::class,
};
}
}

View File

@ -0,0 +1,35 @@
<?php
// phpcs:ignoreFile
declare(strict_types=1);
namespace App\Radio\Enums;
enum StreamFormats: string
{
case Mp3 = 'mp3';
case Ogg = 'ogg';
case Aac = 'aac';
case Opus = 'opus';
case Flac = 'flac';
public function getExtension(): string
{
return match ($this) {
self::Aac => 'mp4',
self::Ogg => 'ogg',
self::Opus => 'opus',
self::Flac => 'flac',
default => 'mp3',
};
}
public function sendIcyMetadata(): bool
{
return match ($this) {
self::Opus, self::Flac => true,
default => false,
};
}
}

View File

@ -0,0 +1,14 @@
<?php
// phpcs:ignoreFile
declare(strict_types=1);
namespace App\Radio\Enums;
enum StreamProtocols: string
{
case Icy = 'icy';
case Http = 'http';
case Https = 'https';
}

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Service;
use App\Enums\SupportedLocales;
use App\Service\IpGeolocator;
use Exception;
use MaxMind\Db\Reader;
@ -71,7 +72,7 @@ class IpGeolocation
/**
* @return mixed[]
*/
public function getLocationInfo(string $ip, string $locale): array
public function getLocationInfo(string $ip, SupportedLocales $locale): array
{
if (!$this->isInitialized) {
$this->initialize();
@ -80,7 +81,7 @@ class IpGeolocation
$reader = $this->reader;
if (null === $reader) {
return [
'status' => 'error',
'status' => 'error',
'message' => $this->getAttribution(),
];
}
@ -100,12 +101,12 @@ class IpGeolocation
}
return [
'status' => 'error',
'status' => 'error',
'message' => 'Internal/Reserved IP',
];
} catch (Exception $e) {
return [
'status' => 'error',
'status' => 'error',
'message' => $e->getMessage(),
];
}
@ -117,25 +118,25 @@ class IpGeolocation
}
return [
'status' => 'success',
'lat' => $ipInfo['location']['latitude'] ?? 0.0,
'lon' => $ipInfo['location']['longitude'] ?? 0.0,
'status' => 'success',
'lat' => $ipInfo['location']['latitude'] ?? 0.0,
'lon' => $ipInfo['location']['longitude'] ?? 0.0,
'timezone' => $ipInfo['location']['time_zone'] ?? '',
'region' => $this->getLocalizedString($ipInfo['subdivisions'][0]['names'] ?? null, $locale),
'country' => $this->getLocalizedString($ipInfo['country']['names'] ?? null, $locale),
'city' => $this->getLocalizedString($ipInfo['city']['names'] ?? null, $locale),
'message' => $this->attribution,
'region' => $this->getLocalizedString($ipInfo['subdivisions'][0]['names'] ?? null, $locale),
'country' => $this->getLocalizedString($ipInfo['country']['names'] ?? null, $locale),
'city' => $this->getLocalizedString($ipInfo['city']['names'] ?? null, $locale),
'message' => $this->attribution,
];
}
protected function getLocalizedString(?array $names, string $locale): string
protected function getLocalizedString(?array $names, SupportedLocales $locale): string
{
if (empty($names)) {
return '';
}
// Convert "en_US" to "en-US", the format MaxMind uses.
$locale = str_replace('_', '-', $locale);
$locale = str_replace('_', '-', $locale->value);
// Check for an exact match.
if (isset($names[$locale])) {

View File

@ -35,11 +35,11 @@ abstract class AbstractTask implements ScheduledTaskInterface
}
/**
* @param string $type
* @param Entity\Enums\StorageLocationTypes $type
*
* @return ReadWriteBatchIteratorAggregate|Entity\StorageLocation[]
*/
protected function iterateStorageLocations(string $type): ReadWriteBatchIteratorAggregate
protected function iterateStorageLocations(Entity\Enums\StorageLocationTypes $type): ReadWriteBatchIteratorAggregate
{
return ReadWriteBatchIteratorAggregate::fromQuery(
$this->em->createQuery(
@ -48,7 +48,7 @@ abstract class AbstractTask implements ScheduledTaskInterface
FROM App\Entity\StorageLocation sl
WHERE sl.type = :type
DQL
)->setParameter('type', $type),
)->setParameter('type', $type->value),
1
);
}

View File

@ -63,7 +63,7 @@ class CheckFolderPlaylistsTask extends AbstractTask
)->setParameter('storageLocation', $station->getMediaStorageLocation());
foreach ($station->getPlaylists() as $playlist) {
if (Entity\StationPlaylist::SOURCE_SONGS !== $playlist->getSource()) {
if (Entity\Enums\PlaylistSources::Songs !== $playlist->getSourceEnum()) {
continue;
}

View File

@ -63,7 +63,7 @@ class CheckMediaTask extends AbstractTask
public function run(bool $force = false): void
{
$storageLocations = $this->iterateStorageLocations(Entity\StorageLocation::TYPE_STATION_MEDIA);
$storageLocations = $this->iterateStorageLocations(Entity\Enums\StorageLocationTypes::StationMedia);
foreach ($storageLocations as $storageLocation) {
$this->logger->info(

View File

@ -23,7 +23,7 @@ class CleanupStorageTask extends AbstractTask
$this->cleanStationTempFiles($station);
}
$storageLocations = $this->iterateStorageLocations(Entity\StorageLocation::TYPE_STATION_MEDIA);
$storageLocations = $this->iterateStorageLocations(Entity\Enums\StorageLocationTypes::StationMedia);
foreach ($storageLocations as $storageLocation) {
/** @var Entity\StorageLocation $storageLocation */
$this->cleanMediaStorageLocation($storageLocation);

View File

@ -27,7 +27,11 @@ class MoveBroadcastsTask extends AbstractTask
public function run(bool $force = false): void
{
foreach ($this->iterateStorageLocations(Entity\StorageLocation::TYPE_STATION_RECORDINGS) as $storageLocation) {
foreach (
$this->iterateStorageLocations(
Entity\Enums\StorageLocationTypes::StationRecordings
) as $storageLocation
) {
$this->processForStorageLocation($storageLocation);
}
}

View File

@ -53,7 +53,7 @@ class RotateLogsTask extends AbstractTask
if ($backupStorageId > 0) {
$storageLocation = $this->storageLocationRepo->findByType(
Entity\StorageLocation::TYPE_BACKUP,
Entity\Enums\StorageLocationTypes::Backup,
$backupStorageId
);

Some files were not shown because too many files have changed in this diff Show More