Merge commit '22451f4a7078faf579e8b4f67d0b51e03981f135'

This commit is contained in:
Buster "Silver Eagle" Neece 2021-12-22 19:32:40 -06:00
parent 31f0c3619a
commit 501ab48dcb
No known key found for this signature in database
GPG Key ID: 9FC8B9E008872109
38 changed files with 923 additions and 694 deletions

View File

@ -53,7 +53,6 @@
"matomo/device-detector": "^5.0",
"mezzio/mezzio-session": "^1.3",
"mezzio/mezzio-session-cache": "^1.4",
"mnapoli/silly-php-di": "^1.2",
"monolog/monolog": "^2",
"myclabs/deep-copy": "^1.10",
"nesbot/carbon": "^2.36",
@ -100,7 +99,8 @@
"symfony/polyfill-php72": "1.99",
"symfony/polyfill-php73": "1.99",
"symfony/polyfill-php74": "1.99",
"symfony/polyfill-php80": "1.99"
"symfony/polyfill-php80": "1.99",
"symfony/polyfill-php81": "1.99"
},
"conflict": {
"gettext/gettext": ">=5"
@ -132,7 +132,15 @@
"config": {
"discard-changes": true,
"preferred-install": "dist",
"sort-packages": true
"sort-packages": true,
"allow-plugins": {
"composer/package-versions-deprecated": true,
"dealerdirect/phpcodesniffer-composer-installer": true,
"ergebnis/composer-normalize": true,
"pyrech/composer-changelogs": true,
"ramsey/composer-repl": true,
"wikimedia/composer-merge-plugin": true
}
},
"extra": {
"merge-plugin": {

237
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "fd1e38effd0adbef62a54c83b3a4f856",
"content-hash": "46cce91d9427c4a7e2297a993606ed14",
"packages": [
{
"name": "aws/aws-crt-php",
@ -486,19 +486,19 @@
"source": {
"type": "git",
"url": "https://github.com/AzuraCast/slim-callable-eventdispatcher.git",
"reference": "0c1a17c81573fbdbcb65b04aea5ddf1743889e91"
"reference": "0260f552c84f312819dca7556f03f0386f40b14c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/AzuraCast/slim-callable-eventdispatcher/zipball/0c1a17c81573fbdbcb65b04aea5ddf1743889e91",
"reference": "0c1a17c81573fbdbcb65b04aea5ddf1743889e91",
"url": "https://api.github.com/repos/AzuraCast/slim-callable-eventdispatcher/zipball/0260f552c84f312819dca7556f03f0386f40b14c",
"reference": "0260f552c84f312819dca7556f03f0386f40b14c",
"shasum": ""
},
"require": {
"ext-json": "*",
"php": ">=8.0",
"slim/slim": "^4",
"symfony/event-dispatcher": "^5"
"symfony/event-dispatcher": "^5|^6"
},
"require-dev": {
"php-parallel-lint/php-console-highlighter": "^0.5.0",
@ -543,7 +543,7 @@
"type": "patreon"
}
],
"time": "2021-08-05T00:22:23+00:00"
"time": "2021-12-22T17:29:07+00:00"
},
{
"name": "bacon/bacon-qr-code",
@ -4158,104 +4158,6 @@
],
"time": "2021-09-15T08:19:41+00:00"
},
{
"name": "mnapoli/silly",
"version": "1.7.3",
"source": {
"type": "git",
"url": "https://github.com/mnapoli/silly.git",
"reference": "e437baa502c3e1691d342374e71446d4bac1cad5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/mnapoli/silly/zipball/e437baa502c3e1691d342374e71446d4bac1cad5",
"reference": "e437baa502c3e1691d342374e71446d4bac1cad5",
"shasum": ""
},
"require": {
"php": ">=7.0",
"php-di/invoker": "~2.0",
"psr/container": "^1.0|^2.0",
"symfony/console": "~3.0|~4.0|~5.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.12",
"mnapoli/phpunit-easymock": "~1.0",
"phpunit/phpunit": "~6.4"
},
"type": "library",
"autoload": {
"psr-4": {
"Silly\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Silly CLI micro-framework based on Symfony Console",
"keywords": [
"PSR-11",
"cli",
"console",
"framework",
"micro-framework",
"silly"
],
"support": {
"issues": "https://github.com/mnapoli/silly/issues",
"source": "https://github.com/mnapoli/silly/tree/1.7.3"
},
"funding": [
{
"url": "https://github.com/mnapoli",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/mnapoli/silly",
"type": "tidelift"
}
],
"time": "2021-12-13T09:21:21+00:00"
},
{
"name": "mnapoli/silly-php-di",
"version": "1.2.0",
"source": {
"type": "git",
"url": "https://github.com/mnapoli/silly-php-di.git",
"reference": "bd4af1c2cdf6141dc941a9a4728d1db4d4b01ef0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/mnapoli/silly-php-di/zipball/bd4af1c2cdf6141dc941a9a4728d1db4d4b01ef0",
"reference": "bd4af1c2cdf6141dc941a9a4728d1db4d4b01ef0",
"shasum": ""
},
"require": {
"mnapoli/silly": "~1.1",
"php-di/php-di": "~4.4 || ^5.0 || ^6.0.0"
},
"require-dev": {
"phpunit/phpunit": "~4.5"
},
"type": "library",
"autoload": {
"psr-4": {
"Silly\\Edition\\PhpDi\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Silly framework edition configured with PHP-DI",
"support": {
"issues": "https://github.com/mnapoli/silly-php-di/issues",
"source": "https://github.com/mnapoli/silly-php-di/tree/master"
},
"time": "2018-04-05T08:53:27+00:00"
},
{
"name": "monolog/monolog",
"version": "2.3.5",
@ -4483,9 +4385,6 @@
"require": {
"php": "^7.1 || ^8.0"
},
"replace": {
"myclabs/deep-copy": "self.version"
},
"require-dev": {
"doctrine/collections": "^1.0",
"doctrine/common": "^2.6",
@ -7028,25 +6927,25 @@
},
{
"name": "symfony/deprecation-contracts",
"version": "v3.0.0",
"version": "v2.5.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
"reference": "c726b64c1ccfe2896cb7df2e1331c357ad1c8ced"
"reference": "6f981ee24cf69ee7ce9736146d1c57c2780598a8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/c726b64c1ccfe2896cb7df2e1331c357ad1c8ced",
"reference": "c726b64c1ccfe2896cb7df2e1331c357ad1c8ced",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/6f981ee24cf69ee7ce9736146d1c57c2780598a8",
"reference": "6f981ee24cf69ee7ce9736146d1c57c2780598a8",
"shasum": ""
},
"require": {
"php": ">=8.0.2"
"php": ">=7.1"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.0-dev"
"dev-main": "2.5-dev"
},
"thanks": {
"name": "symfony/contracts",
@ -7075,7 +6974,7 @@
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.0.0"
"source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.0"
},
"funding": [
{
@ -7091,7 +6990,7 @@
"type": "tidelift"
}
],
"time": "2021-11-01T23:48:49+00:00"
"time": "2021-07-12T14:48:14+00:00"
},
{
"name": "symfony/doctrine-messenger",
@ -8102,85 +8001,6 @@
],
"time": "2021-05-27T09:27:20+00:00"
},
{
"name": "symfony/polyfill-php81",
"version": "v1.23.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php81.git",
"reference": "e66119f3de95efc359483f810c4c3e6436279436"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/e66119f3de95efc359483f810c4c3e6436279436",
"reference": "e66119f3de95efc359483f810c4c3e6436279436",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Php81\\": ""
},
"files": [
"bootstrap.php"
],
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php81/tree/v1.23.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-05-21T13:25:03+00:00"
},
{
"name": "symfony/process",
"version": "v5.4.0",
@ -8654,21 +8474,22 @@
},
{
"name": "symfony/service-contracts",
"version": "v2.4.1",
"version": "v2.5.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/service-contracts.git",
"reference": "d664541b99d6fb0247ec5ff32e87238582236204"
"reference": "1ab11b933cd6bc5464b08e81e2c5b07dec58b0fc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/service-contracts/zipball/d664541b99d6fb0247ec5ff32e87238582236204",
"reference": "d664541b99d6fb0247ec5ff32e87238582236204",
"url": "https://api.github.com/repos/symfony/service-contracts/zipball/1ab11b933cd6bc5464b08e81e2c5b07dec58b0fc",
"reference": "1ab11b933cd6bc5464b08e81e2c5b07dec58b0fc",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"psr/container": "^1.1"
"psr/container": "^1.1",
"symfony/deprecation-contracts": "^2.1"
},
"conflict": {
"ext-psr": "<1.1|>=2"
@ -8679,7 +8500,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "2.4-dev"
"dev-main": "2.5-dev"
},
"thanks": {
"name": "symfony/contracts",
@ -8716,7 +8537,7 @@
"standards"
],
"support": {
"source": "https://github.com/symfony/service-contracts/tree/v2.4.1"
"source": "https://github.com/symfony/service-contracts/tree/v2.5.0"
},
"funding": [
{
@ -8732,7 +8553,7 @@
"type": "tidelift"
}
],
"time": "2021-11-04T16:37:19+00:00"
"time": "2021-11-04T16:48:04+00:00"
},
{
"name": "symfony/stopwatch",
@ -12511,12 +12332,12 @@
"source": {
"type": "git",
"url": "https://github.com/Roave/SecurityAdvisories.git",
"reference": "fff53639bf1fa25f311c3e54932ac8c827f9a343"
"reference": "31d9d9e2977ae7d796d82271be09e46f4bdf41b3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/fff53639bf1fa25f311c3e54932ac8c827f9a343",
"reference": "fff53639bf1fa25f311c3e54932ac8c827f9a343",
"url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/31d9d9e2977ae7d796d82271be09e46f4bdf41b3",
"reference": "31d9d9e2977ae7d796d82271be09e46f4bdf41b3",
"shasum": ""
},
"conflict": {
@ -12759,7 +12580,7 @@
"shopware/production": "<=6.3.5.2",
"shopware/shopware": "<5.7.6",
"showdoc/showdoc": "<=2.9.13",
"silverstripe/admin": "<4.8.1",
"silverstripe/admin": ">=1,<1.8.1",
"silverstripe/assets": ">=1,<1.4.7|>=1.5,<1.5.2",
"silverstripe/cms": "<4.3.6|>=4.4,<4.4.4",
"silverstripe/comments": ">=1.3,<1.9.99|>=2,<2.9.99|>=3,<3.1.1",
@ -12936,7 +12757,7 @@
"type": "tidelift"
}
],
"time": "2021-12-17T20:13:17+00:00"
"time": "2021-12-22T21:13:38+00:00"
},
{
"name": "sebastian/cli-parser",
@ -14525,5 +14346,5 @@
"ext-xmlwriter": "*"
},
"platform-dev": [],
"plugin-api-version": "2.1.0"
"plugin-api-version": "2.2.0"
}

View File

@ -1,160 +1,37 @@
<?php
use App\Console\Application;
use App\Console\Command;
return function (Application $console) {
// Liquidsoap Internal CLI commands
$console->command(
'azuracast:internal:auth station-id [--dj-user=] [--dj-password=]',
Command\Internal\DjAuthCommand::class
)->setDescription('Authorize a streamer to connect as a source for the radio service.');
$console->command(
'azuracast:internal:djoff station-id [--dj-user=]',
Command\Internal\DjOffCommand::class
)->setDescription('Indicate that a DJ has finished streaming to a station.');
$console->command(
'azuracast:internal:djon station-id [--dj-user=]',
Command\Internal\DjOnCommand::class
)->setDescription('Indicate that a DJ has begun streaming to a station.');
$console->command(
'azuracast:internal:feedback station-id [-s|--song=] [-m|--media=] [-p|--playlist=]',
Command\Internal\FeedbackCommand::class
)->setDescription('Send upcoming song feedback from the AutoDJ back to AzuraCast.');
$console->command(
'azuracast:internal:sftp-event action username path [target-path] [ssh-cmd]',
Command\Internal\SftpEventCommand::class
)->setDescription('Process an event triggered via SFTP');
$console->command(
'azuracast:internal:sftp-auth',
Command\Internal\SftpAuthCommand::class
)->setDescription('Attempt SFTP authentication');
$console->command(
'azuracast:internal:nextsong station-id [as-autodj]',
Command\Internal\NextSongCommand::class
)->defaults(
[
'as-autodj' => true,
]
)->setDescription('Return the next song to the AutoDJ.');
$console->command(
'azuracast:internal:ip',
Command\Internal\GetIpCommand::class
)->setDescription('Get the external IP address for this instance.');
// Locales
$console->command(
'locale:generate',
Command\Locale\GenerateCommand::class
)->setDescription(__('Generate the translation locale file.'));
$console->command(
'locale:import',
Command\Locale\ImportCommand::class
)->setDescription(__('Convert translated locale files into PHP arrays.'));
// Setup
$console->command(
'azuracast:setup:initialize',
Command\InitializeCommand::class
)->setDescription(__('Ensure key settings are initialized within AzuraCast.'));
$console->command(
'azuracast:config:migrate',
Command\MigrateConfigCommand::class
)->setDescription(__('Migrate existing configuration to new INI format if any exists.'));
$console->command(
'azuracast:setup:fixtures',
Command\SetupFixturesCommand::class
)->setDescription(__('Install fixtures for demo / local development.'));
$console->command(
'azuracast:setup [--update] [--load-fixtures] [--release]',
Command\SetupCommand::class
)->setDescription(__('Run all general AzuraCast setup steps.'));
// Maintenance
$console->command(
'azuracast:radio:restart [station-name]',
Command\RestartRadioCommand::class
)->setDescription('Restart all radio stations, or a single one if specified.');
$console->command(
'sync:run [--force] [task]',
Command\SyncCommand::class
)->setDescription(__('Run one or more scheduled synchronization tasks.'));
$console->command(
'queue:process [runtime] [--worker-name=]',
Command\MessageQueue\ProcessCommand::class
)->setDescription(__('Process the message queue.'));
$console->command(
'queue:clear [queue]',
Command\MessageQueue\ClearCommand::class
)->setDescription(__('Clear the contents of the message queue.'));
$console->command(
'azuracast:media:reprocess [station-name]',
Command\ReprocessMediaCommand::class
)->setDescription('Manually reload all media metadata from file.');
$console->command(
'azuracast:api:docs',
Command\GenerateApiDocsCommand::class
)->setDescription('Trigger regeneration of AzuraCast API documentation.');
$console->command(
'azuracast:debug:optimize-tables',
Command\Debug\OptimizeTablesCommand::class
)->setDescription('Optimize all tables in the database.');
// User-side tools
$console->command(
'azuracast:account:list',
Command\Users\ListCommand::class
)->setDescription('List all accounts in the system.');
$console->command(
'azuracast:account:login-token email',
Command\Users\LoginTokenCommand::class
)->setDescription('Create a unique login recovery URL for the specified account.');
$console->command(
'azuracast:account:reset-password email',
Command\Users\ResetPasswordCommand::class
)->setDescription('Reset the password of the specified account.');
$console->command(
'azuracast:account:set-administrator email',
Command\Users\SetAdministratorCommand::class
)->setDescription('Set the account specified as a global administrator.');
$console->command(
'azuracast:settings:list',
Command\Settings\ListCommand::class
)->setDescription(__('List all settings in the AzuraCast settings database.'));
$console->command(
'azuracast:settings:set setting-key setting-value',
Command\Settings\SetCommand::class
)->setDescription('Set the value of a setting in the AzuraCast settings database.');
$console->command(
'azuracast:backup [path] [--storage-location-id=] [--exclude-media]',
Command\Backup\BackupCommand::class
)->setDescription(__('Back up the AzuraCast database and statistics (and optionally media).'));
$console->command(
'azuracast:restore path [--restore] [--release]',
Command\Backup\RestoreCommand::class
)->setDescription('Restore a backup previously generated by AzuraCast.');
return function (App\Event\BuildConsoleCommands $event) {
$event->addAliases([
'azuracast:backup' => Command\Backup\BackupCommand::class,
'azuracast:restore' => Command\Backup\RestoreCommand::class,
'azuracast:debug:optimize-tables' => Command\Debug\OptimizeTablesCommand::class,
'azuracast:internal:auth' => Command\Internal\DjAuthCommand::class,
'azuracast:internal:djoff' => Command\Internal\DjOffCommand::class,
'azuracast:internal:djon' => Command\Internal\DjOnCommand::class,
'azuracast:internal:feedback' => Command\Internal\FeedbackCommand::class,
'azuracast:internal:sftp-event' => Command\Internal\SftpEventCommand::class,
'azuracast:internal:sftp-auth' => Command\Internal\SftpAuthCommand::class,
'azuracast:internal:nextsong' => Command\Internal\NextSongCommand::class,
'azuracast:internal:ip' => Command\Internal\GetIpCommand::class,
'locale:generate' => Command\Locale\GenerateCommand::class,
'locale:import' => Command\Locale\ImportCommand::class,
'queue:process' => Command\MessageQueue\ProcessCommand::class,
'queue:clear' => Command\MessageQueue\ClearCommand::class,
'azuracast:settings:list' => Command\Settings\ListCommand::class,
'azuracast:settings:set' => Command\Settings\SetCommand::class,
'azuracast:account:list' => Command\Users\ListCommand::class,
'azuracast:account:login-token' => Command\Users\LoginTokenCommand::class,
'azuracast:account:reset-password' => Command\Users\ResetPasswordCommand::class,
'azuracast:account:set-administrator' => Command\Users\SetAdministratorCommand::class,
'azuracast:setup:initialize' => Command\InitializeCommand::class,
'azuracast:config:migrate' => Command\MigrateConfigCommand::class,
'azuracast:setup:fixtures' => Command\SetupFixturesCommand::class,
'azuracast:setup' => Command\SetupCommand::class,
'azuracast:radio:restart' => Command\RestartRadioCommand::class,
'sync:run' => Command\SyncCommand::class,
'azuracast:media:reprocess' => Command\ReprocessMediaCommand::class,
'azuracast:api:docs' => Command\GenerateApiDocsCommand::class,
]);
};

View File

@ -11,13 +11,11 @@ return function (CallableEventDispatcherInterface $dispatcher) {
Event\BuildConsoleCommands::class,
function (Event\BuildConsoleCommands $event) use ($dispatcher) {
$console = $event->getConsole();
$di = $console->getContainer();
$di = $event->getContainer();
/** @var Environment $environment */
$environment = $di->get(Environment::class);
$console->command('cache:clear', Command\ClearCacheCommand::class)
->setDescription('Clear all application caches.');
$event->addAliases([
'cache:clear' => Command\ClearCacheCommand::class,
]);
// Doctrine ORM/DBAL
Doctrine\ORM\Tools\Console\ConsoleRunner::addCommands($console);
@ -26,6 +24,9 @@ return function (CallableEventDispatcherInterface $dispatcher) {
/** @var Doctrine\ORM\EntityManagerInterface $em */
$em = $di->get(Doctrine\ORM\EntityManagerInterface::class);
/** @var Environment $environment */
$environment = $di->get(Environment::class);
$helper_set = $console->getHelperSet();
$doctrine_helpers = Doctrine\ORM\Tools\Console\ConsoleRunner::createHelperSet($em);
$helper_set->set($doctrine_helpers->get('db'), 'db');
@ -59,7 +60,7 @@ return function (CallableEventDispatcherInterface $dispatcher) {
);
Doctrine\Migrations\Tools\Console\ConsoleRunner::addCommands($console, $migrateFactory);
call_user_func(include(__DIR__ . '/cli.php'), $console);
call_user_func(include(__DIR__ . '/cli.php'), $event);
}
);

View File

@ -232,15 +232,20 @@ return [
) {
$console = new App\Console\Application(
$environment->getAppName() . ' Command Line Tools (' . $environment->getAppEnvironment() . ')',
$version->getVersion(),
$di
$version->getVersion()
);
$console->setDispatcher($dispatcher);
// Trigger an event for the core app and all plugins to build their CLI commands.
$event = new App\Event\BuildConsoleCommands($console);
$event = new Event\BuildConsoleCommands($console, $di);
$dispatcher->dispatch($event);
$commandLoader = new Symfony\Component\Console\CommandLoader\ContainerCommandLoader(
$di,
$event->getAliases()
);
$console->setCommandLoader($commandLoader);
return $console;
},

View File

@ -8,7 +8,7 @@ use RuntimeException;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\StreamOutput;
class Application extends \Silly\Edition\PhpDi\Application
class Application extends \Symfony\Component\Console\Application
{
/**
* Run a one-off command from elsewhere in the application, and pass through the results.

View File

@ -9,22 +9,45 @@ use App\Console\Command\Traits;
use App\Entity;
use App\Utilities;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use const PATHINFO_EXTENSION;
#[AsCommand(
name: 'azuracast:backup',
description: 'Back up the AzuraCast database and statistics (and optionally media).',
)]
class BackupCommand extends CommandAbstract
{
use Traits\PassThruProcess;
public function __invoke(
SymfonyStyle $io,
EntityManagerInterface $em,
Entity\Repository\StorageLocationRepository $storageLocationRepo,
?string $path = '',
bool $excludeMedia = false,
?int $storageLocationId = null
): int {
public function __construct(
protected EntityManagerInterface $em,
protected Entity\Repository\StorageLocationRepository $storageLocationRepo,
) {
parent::__construct();
}
protected function configure(): void
{
$this->addArgument('path', InputArgument::REQUIRED)
->addOption('storage-location-id', null, InputOption::VALUE_OPTIONAL, '', '')
->addOption('exclude-media', null, InputOption::VALUE_NONE);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$path = $input->getArgument('path');
$excludeMedia = (bool)$input->getOption('exclude-media');
$storageLocationId = $input->getOption('storage-location-id');
$start_time = microtime(true);
if (empty($path)) {
@ -44,7 +67,7 @@ class BackupCommand extends CommandAbstract
return 1;
}
$storageLocation = $storageLocationRepo->findByType(
$storageLocation = $this->storageLocationRepo->findByType(
Entity\StorageLocation::TYPE_BACKUP,
$storageLocationId
);
@ -81,7 +104,7 @@ class BackupCommand extends CommandAbstract
$path_db_dump = $tmp_dir_mariadb . '/db.sql';
$conn = $em->getConnection();
$conn = $this->em->getConnection();
$connParams = $conn->getParams();
// phpcs:disable Generic.Files.LineLength
@ -104,7 +127,7 @@ class BackupCommand extends CommandAbstract
// Include station media if specified.
if ($includeMedia) {
$stations = $em->createQuery(
$stations = $this->em->createQuery(
<<<'DQL'
SELECT s FROM App\Entity\Station s
DQL

View File

@ -8,21 +8,42 @@ use App\Console\Command\CommandAbstract;
use App\Console\Command\Traits;
use App\Utilities;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use const PATHINFO_EXTENSION;
#[AsCommand(
name: 'azuracast:restore',
description: 'Restore a backup previously generated by AzuraCast.',
)]
class RestoreCommand extends CommandAbstract
{
use Traits\PassThruProcess;
public function __invoke(
SymfonyStyle $io,
OutputInterface $output,
EntityManagerInterface $em,
string $path
): int {
public function __construct(
protected EntityManagerInterface $em
) {
parent::__construct();
}
protected function configure(): void
{
$this->addArgument('path', InputArgument::REQUIRED)
->addOption('restore', null, InputOption::VALUE_NONE)
->addOption('release', null, InputOption::VALUE_NONE);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$path = $input->getArgument('path');
$start_time = microtime(true);
$io->title('AzuraCast Restore');
@ -96,7 +117,7 @@ class RestoreCommand extends CommandAbstract
return 1;
}
$conn = $em->getConnection();
$conn = $this->em->getConnection();
$connParams = $conn->getParams();
// Drop all preloaded tables prior to running a DB dump backup.

View File

@ -7,28 +7,41 @@ namespace App\Console\Command;
use App\Entity\Repository\SettingsRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Cache\Adapter\AdapterInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'cache:clear',
description: 'Clear all application caches.',
)]
class ClearCacheCommand extends CommandAbstract
{
public function __invoke(
SymfonyStyle $io,
AdapterInterface $cache,
EntityManagerInterface $em,
SettingsRepository $settingsRepo,
): int {
public function __construct(
protected AdapterInterface $cache,
protected EntityManagerInterface $em,
protected SettingsRepository $settingsRepo,
) {
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$io = new SymfonyStyle($input, $output);
// Flush all Redis entries.
$cache->clear();
$this->cache->clear();
// Clear "Now Playing" cache on all station rows.
$em->createQuery(
$this->em->createQuery(
<<<'DQL'
UPDATE App\Entity\Station s SET s.nowplaying=null
DQL
)->execute();
// Clear cached system settings.
$settings = $settingsRepo->readSettings();
$settings = $this->settingsRepo->readSettings();
$settings->setNowplaying(null);
$settings->updateUpdateLastRun();
$settings->setUpdateResults(null);
@ -37,7 +50,7 @@ class ClearCacheCommand extends CommandAbstract
$settings->setExternalIp(null);
}
$settingsRepo->writeSettings($settings);
$this->settingsRepo->writeSettings($settings);
$io->success('Local cache flushed.');
return 0;

View File

@ -4,27 +4,18 @@ declare(strict_types=1);
namespace App\Console\Command;
use App\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\OutputInterface;
abstract class CommandAbstract
abstract class CommandAbstract extends Command
{
protected Application $application;
public function __construct(Application $application)
{
$this->application = $application;
}
public function getApplication(): Application
{
return $this->application;
}
protected function runCommand(OutputInterface $output, string $command_name, array $command_args = []): void
{
$command = $this->getApplication()->find($command_name);
$command = $this->getApplication()?->find($command_name);
if (null === $command) {
return;
}
$input = new ArrayInput(['command' => $command_name] + $command_args);
$input->setInteractive(false);

View File

@ -6,19 +6,34 @@ namespace App\Console\Command\Debug;
use App\Console\Command\CommandAbstract;
use Doctrine\DBAL\Connection;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'azuracast:debug:optimize-tables',
description: 'Optimize all tables in the database.',
)]
class OptimizeTablesCommand extends CommandAbstract
{
public function __invoke(SymfonyStyle $io, Connection $db): int
public function __construct(
protected Connection $db
) {
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$io->title('Optimizing Database Tables...');
foreach ($db->fetchAllAssociative('SHOW TABLES') as $tableRow) {
foreach ($this->db->fetchAllAssociative('SHOW TABLES') as $tableRow) {
$table = reset($tableRow);
$io->listing([$table]);
$db->executeQuery('OPTIMIZE TABLE ' . $db->quoteIdentifier($table));
$this->db->executeQuery('OPTIMIZE TABLE ' . $this->db->quoteIdentifier($table));
}
$io->success('All tables optimized.');

View File

@ -4,28 +4,35 @@ declare(strict_types=1);
namespace App\Console\Command;
use App\Console\Application;
use App\Environment;
use App\Version;
use OpenApi\Annotations\OpenApi;
use OpenApi\Generator;
use OpenApi\Util;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'azuracast:api:docs',
description: 'Trigger regeneration of AzuraCast API documentation.',
)]
class GenerateApiDocsCommand extends CommandAbstract
{
public function __construct(
Application $application,
protected Environment $environment,
protected Version $version,
protected LoggerInterface $logger
) {
parent::__construct($application);
parent::__construct();
}
public function __invoke(SymfonyStyle $io): int
protected function execute(InputInterface $input, OutputInterface $output)
{
$io = new SymfonyStyle($input, $output);
$yaml = $this->generate()?->toYaml();
$yaml_path = $this->environment->getBaseDirectory() . '/web/static/api/openapi.yml';

View File

@ -6,24 +6,35 @@ namespace App\Console\Command;
use App\Entity;
use App\Environment;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'azuracast:setup:initialize',
description: 'Ensure key settings are initialized within AzuraCast.',
)]
class InitializeCommand extends CommandAbstract
{
public function __invoke(
SymfonyStyle $io,
OutputInterface $output,
Environment $environment,
Entity\Repository\StorageLocationRepository $storageLocationRepo
): int {
public function __construct(
protected Environment $environment,
protected Entity\Repository\StorageLocationRepository $storageLocationRepo,
) {
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$io->title(__('Initialize AzuraCast'));
$io->writeln(__('Initializing essential settings...'));
$io->listing(
[
__('Environment: %s', ucfirst($environment->getAppEnvironment())),
__('Installation Method: %s', $environment->isDocker() ? 'Docker' : 'Ansible'),
__('Environment: %s', ucfirst($this->environment->getAppEnvironment())),
__('Installation Method: %s', $this->environment->isDocker() ? 'Docker' : 'Ansible'),
]
);
@ -49,7 +60,7 @@ class InitializeCommand extends CommandAbstract
$this->runCommand($output, 'cache:clear');
// Ensure default storage locations exist.
$storageLocationRepo->createDefaultStorageLocations();
$this->storageLocationRepo->createDefaultStorageLocations();
$io->newLine();
$io->success(

View File

@ -6,29 +6,54 @@ namespace App\Console\Command\Internal;
use App\Console\Command\CommandAbstract;
use App\Entity;
use App\Entity\Repository\SettingsRepository;
use App\Radio\Adapters;
use App\Radio\Backend\Liquidsoap;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'azuracast:internal:auth',
description: 'Authorize a streamer to connect as a source for the radio service.',
)]
class DjAuthCommand extends CommandAbstract
{
public function __invoke(
SymfonyStyle $io,
EntityManagerInterface $em,
Adapters $adapters,
int $stationId,
string $djUser = '',
string $djPassword = ''
): int {
$station = $em->getRepository(Entity\Station::class)->find($stationId);
public function __construct(
protected Adapters $adapters,
protected EntityManagerInterface $em,
protected SettingsRepository $settingsRepo,
) {
parent::__construct();
}
protected function configure(): void
{
$this->addArgument('station-id', InputArgument::REQUIRED)
->addOption('dj-user', null, InputOption::VALUE_REQUIRED, '', '')
->addOption('dj-password', null, InputOption::VALUE_REQUIRED, '', '');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$stationId = (int)$input->getArgument('station-id');
$djUser = $input->getOption('dj-user');
$djPassword = $input->getOption('dj-password');
$station = $this->em->getRepository(Entity\Station::class)->find($stationId);
if (!($station instanceof Entity\Station) || !$station->getEnableStreamers()) {
$io->write('false');
return 0;
}
$adapter = $adapters->getBackendAdapter($station);
$adapter = $this->adapters->getBackendAdapter($station);
if ($adapter instanceof Liquidsoap) {
$response = $adapter->authenticateStreamer($station, $djUser, $djPassword);

View File

@ -9,24 +9,46 @@ use App\Entity;
use App\Radio\Adapters;
use App\Radio\Backend\Liquidsoap;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'azuracast:internal:djoff',
description: 'Indicate that a DJ has finished streaming to a station.',
)]
class DjOffCommand extends CommandAbstract
{
public function __invoke(
SymfonyStyle $io,
EntityManagerInterface $em,
Adapters $adapters,
int $stationId,
string $djUser = ''
): int {
$station = $em->find(Entity\Station::class, $stationId);
public function __construct(
protected Adapters $adapters,
protected EntityManagerInterface $em
) {
parent::__construct();
}
protected function configure(): void
{
$this->addArgument('station-id', InputArgument::REQUIRED)
->addOption('dj-user', null, InputOption::VALUE_REQUIRED, '', '');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$stationId = (int)$input->getArgument('station-id');
$djUser = $input->getOption('dj-user');
$station = $this->em->find(Entity\Station::class, $stationId);
if (!($station instanceof Entity\Station) || !$station->getEnableStreamers()) {
return 1;
}
$adapter = $adapters->getBackendAdapter($station);
$adapter = $this->adapters->getBackendAdapter($station);
if ($adapter instanceof Liquidsoap) {
$io->write($adapter->onDisconnect($station, $djUser));

View File

@ -9,24 +9,46 @@ use App\Entity;
use App\Radio\Adapters;
use App\Radio\Backend\Liquidsoap;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'azuracast:internal:djon',
description: 'Indicate that a DJ has begun streaming to a station.',
)]
class DjOnCommand extends CommandAbstract
{
public function __invoke(
SymfonyStyle $io,
EntityManagerInterface $em,
Adapters $adapters,
int $stationId,
string $djUser = ''
): int {
$station = $em->find(Entity\Station::class, $stationId);
public function __construct(
protected Adapters $adapters,
protected EntityManagerInterface $em
) {
parent::__construct();
}
protected function configure(): void
{
$this->addArgument('station-id', InputArgument::REQUIRED)
->addOption('dj-user', null, InputOption::VALUE_REQUIRED, '', '');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$stationId = (int)$input->getArgument('station-id');
$djUser = $input->getOption('dj-user');
$station = $this->em->find(Entity\Station::class, $stationId);
if (!($station instanceof Entity\Station) || !$station->getEnableStreamers()) {
return 1;
}
$adapter = $adapters->getBackendAdapter($station);
$adapter = $this->adapters->getBackendAdapter($station);
if ($adapter instanceof Liquidsoap) {
$io->write($adapter->onConnect($station, $djUser));

View File

@ -9,20 +9,44 @@ use App\Entity;
use App\Sync\Task\NowPlayingTask;
use Doctrine\ORM\EntityManagerInterface;
use Exception;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'azuracast:internal:feedback',
description: 'Send upcoming song feedback from the AutoDJ back to AzuraCast.',
)]
class FeedbackCommand extends CommandAbstract
{
public function __invoke(
SymfonyStyle $io,
EntityManagerInterface $em,
NowPlayingTask $nowPlaying,
int $stationId,
string $song = null,
string $media = null,
string $playlist = null
): int {
$station = $em->find(Entity\Station::class, $stationId);
public function __construct(
protected EntityManagerInterface $em,
protected NowPlayingTask $nowPlaying,
) {
parent::__construct();
}
protected function configure(): void
{
$this->addArgument('station-id', InputArgument::REQUIRED)
->addOption('song', 's', InputOption::VALUE_OPTIONAL, '', '')
->addOption('media', 'm', InputOption::VALUE_OPTIONAL, '', '')
->addOption('playlist', 'p', InputOption::VALUE_OPTIONAL, '', '');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$stationId = (int)$input->getArgument('station-id');
$song = $input->getOption('song');
$media = $input->getOption('media');
$playlist = $input->getOption('playlist');
$station = $this->em->find(Entity\Station::class, $stationId);
if (!($station instanceof Entity\Station)) {
$io->write('false');
@ -30,9 +54,9 @@ class FeedbackCommand extends CommandAbstract
}
try {
$nowPlaying->queueStation($station, [
'song_id' => $song,
'media_id' => $media,
$this->nowPlaying->queueStation($station, [
'song_id' => $song,
'media_id' => $media,
'playlist_id' => $playlist,
]);

View File

@ -6,15 +6,28 @@ namespace App\Console\Command\Internal;
use App\Console\Command\CommandAbstract;
use App\Service\AzuraCastCentral;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'azuracast:internal:ip',
description: 'Get the external IP address for this instance.',
)]
class GetIpCommand extends CommandAbstract
{
public function __invoke(
SymfonyStyle $io,
AzuraCastCentral $acCentral
): int {
$io->write($acCentral->getIp() ?? 'Unknown');
public function __construct(
protected AzuraCastCentral $acCentral,
) {
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$io->write($this->acCentral->getIp() ?? 'Unknown');
return 0;
}
}

View File

@ -8,25 +8,47 @@ use App\Console\Command\CommandAbstract;
use App\Entity;
use App\Radio\AutoDJ;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'azuracast:internal:nextsong',
description: 'Return the next song to the AutoDJ.',
)]
class NextSongCommand extends CommandAbstract
{
public function __invoke(
SymfonyStyle $io,
EntityManagerInterface $em,
AutoDJ $autoDJ,
int $stationId,
bool $asAutodj = false
): int {
$station = $em->find(Entity\Station::class, $stationId);
public function __construct(
protected EntityManagerInterface $em,
protected AutoDJ $autoDJ,
) {
parent::__construct();
}
protected function configure(): void
{
$this->addArgument('station-id', InputArgument::REQUIRED)
->addOption('as-autodj', null, InputOption::VALUE_NONE);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$stationId = (int)$input->getArgument('station-id');
$asAutodj = (bool)$input->getOption('as-autodj');
$station = $this->em->find(Entity\Station::class, $stationId);
if (!($station instanceof Entity\Station)) {
$io->write('false');
return 0;
}
$io->write($autoDJ->annotateNextSong($station, $asAutodj));
$io->write($this->autoDJ->annotateNextSong($station, $asAutodj));
return 0;
}
}

View File

@ -8,23 +8,36 @@ use App\Console\Command\CommandAbstract;
use App\Entity\SftpUser;
use Brick\Math\BigInteger;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use const JSON_NUMERIC_CHECK;
use const JSON_THROW_ON_ERROR;
use const JSON_UNESCAPED_SLASHES;
#[AsCommand(
name: 'azuracast:internal:sftp-auth',
description: 'Attempt SFTP authentication.',
)]
class SftpAuthCommand extends CommandAbstract
{
public function __invoke(
SymfonyStyle $io,
EntityManagerInterface $em
): int {
public function __construct(
protected EntityManagerInterface $em,
) {
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$username = getenv('SFTPGO_AUTHD_USERNAME') ?: null;
$password = getenv('SFTPGO_AUTHD_PASSWORD') ?: null;
$pubKey = getenv('SFTPGO_AUTHD_PUBLIC_KEY') ?: null;
$sftpUser = $em->getRepository(SftpUser::class)->findOneBy(['username' => $username]);
$sftpUser = $this->em->getRepository(SftpUser::class)->findOneBy(['username' => $username]);
if ($sftpUser instanceof SftpUser && $sftpUser->authenticate($password, $pubKey)) {
$storageLocation = $sftpUser->getStation()->getMediaStorageLocation();
@ -35,14 +48,14 @@ class SftpAuthCommand extends CommandAbstract
: 0;
$row = [
'status' => 1,
'username' => $sftpUser->getUsername(),
'status' => 1,
'username' => $sftpUser->getUsername(),
'expiration_date' => 0,
'home_dir' => $storageLocation->getPath(),
'uid' => 0,
'gid' => 0,
'quota_size' => $quota,
'permissions' => [
'home_dir' => $storageLocation->getPath(),
'uid' => 0,
'gid' => 0,
'quota_size' => $quota,
'permissions' => [
'/' => ['*'],
],
];

View File

@ -4,7 +4,6 @@ declare(strict_types=1);
namespace App\Console\Command\Internal;
use App\Console\Application;
use App\Console\Command\CommandAbstract;
use App\Entity;
use App\Media\BatchUtilities;
@ -12,42 +11,57 @@ use App\Message;
use Doctrine\ORM\EntityManagerInterface;
use League\Flysystem\PathPrefixer;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Messenger\MessageBus;
#[AsCommand(
name: 'azuracast:internal:sftp-event',
description: 'Send upcoming song feedback from the AutoDJ back to AzuraCast.',
)]
class SftpEventCommand extends CommandAbstract
{
public function __construct(
Application $application,
protected EntityManagerInterface $em,
protected MessageBus $messageBus,
protected LoggerInterface $logger,
protected BatchUtilities $batchUtilities
protected BatchUtilities $batchUtilities,
) {
parent::__construct($application);
parent::__construct();
}
public function __invoke(
SymfonyStyle $io,
EntityManagerInterface $em,
string $action = null,
string $username = null,
string $path = null,
string $targetPath = null,
string $sshCmd = null
): int {
protected function configure(): void
{
$this->addArgument('action', InputArgument::REQUIRED)
->addArgument('username', InputArgument::REQUIRED)
->addArgument('path', InputArgument::REQUIRED)
->addArgument('target-path', InputArgument::OPTIONAL)
->addArgument('ssh-cmd', InputArgument::OPTIONAL);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$action = $input->getArgument('action');
$username = $input->getArgument('username');
$path = $input->getArgument('path');
$targetPath = $input->getArgument('target-path');
$sshCmd = $input->getArgument('ssh-cmd');
$this->logger->notice(
'SFTP file event triggered',
[
'action' => $action,
'username' => $username,
'path' => $path,
'action' => $action,
'username' => $username,
'path' => $path,
'targetPath' => $targetPath,
'sshCmd' => $sshCmd,
'sshCmd' => $sshCmd,
]
);
// Determine which station the username belongs to.
$sftpUser = $em->getRepository(Entity\SftpUser::class)->findOneBy(
$sftpUser = $this->em->getRepository(Entity\SftpUser::class)->findOneBy(
[
'username' => $username,
]
@ -101,7 +115,7 @@ class SftpEventCommand extends CommandAbstract
'Processing new SFTP upload.',
[
'storageLocation' => (string)$storageLocation,
'path' => $relativePath,
'path' => $relativePath,
]
);
@ -125,7 +139,7 @@ class SftpEventCommand extends CommandAbstract
'Processing SFTP file/folder deletion.',
[
'storageLocation' => (string)$storageLocation,
'path' => $relativePath,
'path' => $relativePath,
]
);
@ -182,8 +196,8 @@ class SftpEventCommand extends CommandAbstract
'Processing SFTP file/folder rename.',
[
'storageLocation' => (string)$storageLocation,
'from' => $from,
'to' => $to,
'from' => $from,
'to' => $to,
]
);

View File

@ -9,26 +9,37 @@ use App\Environment;
use Gettext\Translations;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use RecursiveRegexIterator;
use RegexIterator;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'locale:generate',
description: 'Generate the translation locale file.',
)]
class GenerateCommand extends CommandAbstract
{
public function __invoke(
SymfonyStyle $io,
Environment $environment
): int {
public function __construct(
protected Environment $environment
) {
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$io->title('Generate Locales');
$exportDir = $environment->getBaseDirectory() . '/resources/locale';
$exportDir = $this->environment->getBaseDirectory() . '/resources/locale';
$dest_file = $exportDir . '/default.pot';
$translations = new Translations();
// Find all JS/Vue file translations.
$directory = new RecursiveDirectoryIterator($environment->getBaseDirectory() . '/frontend/vue');
$directory = new RecursiveDirectoryIterator($this->environment->getBaseDirectory() . '/frontend/vue');
$iterator = new RecursiveIteratorIterator($directory);
$vueRegex = new RegexIterator($iterator, '/^.+\.(vue)$/i', RegexIterator::GET_MATCH);
@ -43,9 +54,9 @@ class GenerateCommand extends CommandAbstract
// Find all PHP/PHTML files in the application's code.
$translatable_folders = [
$environment->getBaseDirectory() . '/src',
$environment->getBaseDirectory() . '/config',
$environment->getViewsDirectory(),
$this->environment->getBaseDirectory() . '/src',
$this->environment->getBaseDirectory() . '/config',
$this->environment->getViewsDirectory(),
];
foreach ($translatable_folders as $folder) {

View File

@ -9,18 +9,30 @@ use App\Environment;
use App\Locale;
use Gettext\Translation;
use Gettext\Translations;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'locale:import',
description: 'Convert translated locale files into PHP arrays.',
)]
class ImportCommand extends CommandAbstract
{
public function __invoke(
SymfonyStyle $io,
Environment $environment
): int {
public function __construct(
protected Environment $environment
) {
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$io->title('Import Locales');
$locales = Locale::SUPPORTED_LOCALES;
$locale_base = $environment->getBaseDirectory() . '/resources/locale';
$locale_base = $this->environment->getBaseDirectory() . '/resources/locale';
$jsTranslations = [];

View File

@ -7,20 +7,39 @@ namespace App\Console\Command\MessageQueue;
use App\Console\Command\CommandAbstract;
use App\MessageQueue\AbstractQueueManager;
use App\MessageQueue\QueueManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'queue:clear',
description: 'Clear the contents of the message queue.',
)]
class ClearCommand extends CommandAbstract
{
public function __invoke(
SymfonyStyle $io,
QueueManagerInterface $queueManager,
?string $queue = null
): int {
public function __construct(
protected QueueManagerInterface $queueManager,
) {
parent::__construct();
}
protected function configure(): void
{
$this->addArgument('queue', InputArgument::OPTIONAL);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$queue = $input->getArgument('queue');
$allQueues = AbstractQueueManager::getAllQueues();
if (!empty($queue)) {
if (in_array($queue, $allQueues, true)) {
$queueManager->clearQueue($queue);
$this->queueManager->clearQueue($queue);
$io->success(sprintf('Message queue "%s" cleared.', $queue));
} else {
@ -29,7 +48,7 @@ class ClearCommand extends CommandAbstract
}
} else {
foreach ($allQueues as $queueName) {
$queueManager->clearQueue($queueName);
$this->queueManager->clearQueue($queueName);
}
$io->success('All message queues cleared.');

View File

@ -12,23 +12,44 @@ use App\MessageQueue\QueueManagerInterface;
use App\MessageQueue\ResetArrayCacheMiddleware;
use Azura\SlimCallableEventDispatcher\CallableEventDispatcherInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Messenger\EventListener\StopWorkerOnTimeLimitListener;
use Symfony\Component\Messenger\MessageBus;
use Symfony\Component\Messenger\Worker;
use Throwable;
#[AsCommand(
name: 'queue:process',
description: 'Process the message queue.',
)]
class ProcessCommand extends CommandAbstract
{
public function __invoke(
MessageBus $messageBus,
CallableEventDispatcherInterface $eventDispatcher,
QueueManagerInterface $queueManager,
LoggerInterface $logger,
Environment $environment,
?int $runtime = 0,
?string $workerName = null
): int {
$logger->notice(
public function __construct(
protected MessageBus $messageBus,
protected CallableEventDispatcherInterface $eventDispatcher,
protected QueueManagerInterface $queueManager,
protected LoggerInterface $logger,
protected Environment $environment,
) {
parent::__construct();
}
protected function configure(): void
{
$this->addArgument('runtime', InputArgument::OPTIONAL)
->addOption('worker-name', null, InputOption::VALUE_OPTIONAL);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$runtime = (int)$input->getArgument('runtime');
$workerName = $input->getOption('worker-name');
$this->logger->notice(
'Starting new Message Queue worker process.',
[
'runtime' => $runtime,
@ -37,38 +58,36 @@ class ProcessCommand extends CommandAbstract
);
if (null !== $workerName) {
$queueManager->setWorkerName($workerName);
$this->queueManager->setWorkerName($workerName);
}
$receivers = $queueManager->getTransports();
$receivers = $this->queueManager->getTransports();
$eventDispatcher->addServiceSubscriber(ClearEntityManagerSubscriber::class);
$eventDispatcher->addServiceSubscriber(LogWorkerExceptionSubscriber::class);
$eventDispatcher->addServiceSubscriber(ResetArrayCacheMiddleware::class);
$this->eventDispatcher->addServiceSubscriber(ClearEntityManagerSubscriber::class);
$this->eventDispatcher->addServiceSubscriber(LogWorkerExceptionSubscriber::class);
$this->eventDispatcher->addServiceSubscriber(ResetArrayCacheMiddleware::class);
if ($runtime <= 0) {
$runtime = $environment->isProduction()
$runtime = $this->environment->isProduction()
? 300
: 30;
}
$eventDispatcher->addSubscriber(new StopWorkerOnTimeLimitListener($runtime, $logger));
$this->eventDispatcher->addSubscriber(new StopWorkerOnTimeLimitListener($runtime, $this->logger));
try {
$worker = new Worker($receivers, $messageBus, $eventDispatcher, $logger);
$worker = new Worker($receivers, $this->messageBus, $this->eventDispatcher, $this->logger);
$worker->run();
} catch (Throwable $e) {
$logger->error(
$this->logger->error(
sprintf('Message queue error: %s', $e->getMessage()),
[
'workerName' => $workerName,
'exception' => $e,
'exception' => $e,
]
);
return 1;
}
return 0;
}
}

View File

@ -5,34 +5,47 @@ declare(strict_types=1);
namespace App\Console\Command;
use App\Environment;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'azuracast:config:migrate',
description: 'Migrate existing configuration to new INI format if any exists.',
)]
class MigrateConfigCommand extends CommandAbstract
{
public function __invoke(
SymfonyStyle $io,
Environment $environment
): int {
public function __construct(
protected Environment $environment,
) {
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$envSettings = [];
$iniPath = $environment->getBaseDirectory() . '/env.ini';
$iniPath = $this->environment->getBaseDirectory() . '/env.ini';
if (is_file($iniPath)) {
$envSettings = (array)parse_ini_file($iniPath);
}
// Migrate from existing legacy config files.
$legacyIniPath = $environment->getBaseDirectory() . '/app/env.ini';
$legacyIniPath = $this->environment->getBaseDirectory() . '/app/env.ini';
if (is_file($legacyIniPath)) {
$iniSettings = parse_ini_file($legacyIniPath);
$envSettings = array_merge($envSettings, (array)$iniSettings);
}
$legacyAppEnvFile = $environment->getBaseDirectory() . '/app/.env';
$legacyAppEnvFile = $this->environment->getBaseDirectory() . '/app/.env';
if (is_file($legacyAppEnvFile)) {
$envSettings[Environment::APP_ENV] ??= file_get_contents($legacyAppEnvFile);
}
$legacyDbConfFile = $environment->getBaseDirectory() . '/app/config/db.conf.php';
$legacyDbConfFile = $this->environment->getBaseDirectory() . '/app/config/db.conf.php';
if (is_file($legacyDbConfFile)) {
$dbConf = include($legacyDbConfFile);
@ -45,11 +58,11 @@ class MigrateConfigCommand extends CommandAbstract
// Migrate from older environment variable names to new ones.
$settingsToMigrate = [
'application_env' => Environment::APP_ENV,
'db_host' => Environment::DB_HOST,
'db_port' => Environment::DB_PORT,
'db_name' => Environment::DB_NAME,
'db_username' => Environment::DB_USER,
'db_password' => Environment::DB_PASSWORD,
'db_host' => Environment::DB_HOST,
'db_port' => Environment::DB_PORT,
'db_name' => Environment::DB_NAME,
'db_username' => Environment::DB_USER,
'db_password' => Environment::DB_PASSWORD,
];
foreach ($settingsToMigrate as $oldSetting => $newSetting) {

View File

@ -5,19 +5,38 @@ declare(strict_types=1);
namespace App\Console\Command;
use App\Entity;
use App\Entity\Repository\StationRepository;
use App\Entity\Station;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'azuracast:media:reprocess',
description: 'Manually reload all media metadata from file.',
)]
class ReprocessMediaCommand extends CommandAbstract
{
public function __invoke(
SymfonyStyle $io,
EntityManagerInterface $em,
StationRepository $stationRepo,
?string $stationName = null
): int {
public function __construct(
protected EntityManagerInterface $em,
protected Entity\Repository\StationRepository $stationRepo,
) {
parent::__construct();
}
protected function configure(): void
{
$this->addArgument('station-name', InputArgument::OPTIONAL);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$stationName = $input->getArgument('station-name');
$io->title('Manually Reprocess Media');
if (empty($stationName)) {
@ -25,7 +44,7 @@ class ReprocessMediaCommand extends CommandAbstract
$storageLocation = null;
} else {
$station = $stationRepo->findByIdentifier($stationName);
$station = $this->stationRepo->findByIdentifier($stationName);
if (!$station instanceof Station) {
$io->error('Station not found.');
return 1;
@ -36,7 +55,7 @@ class ReprocessMediaCommand extends CommandAbstract
$io->writeln(sprintf('Reprocessing media for station: %s', $station->getName()));
}
$reprocessMediaQueue = $em->createQueryBuilder()
$reprocessMediaQueue = $this->em->createQueryBuilder()
->update(Entity\StationMedia::class, 'sm')
->set('sm.mtime', 'NULL');

View File

@ -4,25 +4,44 @@ declare(strict_types=1);
namespace App\Console\Command;
use App\Entity\Repository\StationRepository;
use App\Entity\Station;
use App\Entity;
use App\Radio\Configuration;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'azuracast:radio:restart',
description: 'Restart all radio stations, or a single one if specified.',
)]
class RestartRadioCommand extends CommandAbstract
{
public function __invoke(
SymfonyStyle $io,
EntityManagerInterface $em,
StationRepository $stationRepo,
Configuration $configuration,
?string $stationName = null
): int {
if (!empty($stationName)) {
$station = $stationRepo->findByIdentifier($stationName);
public function __construct(
protected EntityManagerInterface $em,
protected Entity\Repository\StationRepository $stationRepo,
protected Configuration $configuration,
) {
parent::__construct();
}
if (!$station instanceof Station) {
protected function configure(): void
{
$this->addArgument('station-name', InputArgument::OPTIONAL);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$stationName = $input->getArgument('station-name');
if (!empty($stationName)) {
$station = $this->stationRepo->findByIdentifier($stationName);
if (!$station instanceof Entity\Station) {
$io->error('Station not found.');
return 1;
}
@ -31,14 +50,14 @@ class RestartRadioCommand extends CommandAbstract
} else {
$io->section('Restarting all radio stations...');
/** @var Station[] $stations */
$stations = $stationRepo->fetchAll();
/** @var Entity\Station[] $stations */
$stations = $this->stationRepo->fetchAll();
}
$io->progressStart(count($stations));
foreach ($stations as $station) {
$configuration->writeConfiguration($station, true);
$this->configuration->writeConfiguration($station, true);
$io->progressAdvance();
}

View File

@ -7,14 +7,26 @@ namespace App\Console\Command\Settings;
use App\Console\Command\CommandAbstract;
use App\Entity;
use App\Utilities;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'azuracast:settings:list',
description: 'List all settings in the AzuraCast settings database.',
)]
class ListCommand extends CommandAbstract
{
public function __invoke(
SymfonyStyle $io,
Entity\Repository\SettingsRepository $settingsTableRepo
): int {
public function __construct(
protected Entity\Repository\SettingsRepository $settingsTableRepo,
) {
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$io->title(__('AzuraCast Settings'));
$headers = [
@ -23,8 +35,8 @@ class ListCommand extends CommandAbstract
];
$rows = [];
$settings = $settingsTableRepo->readSettings();
foreach ($settingsTableRepo->toArray($settings) as $setting_key => $setting_value) {
$settings = $this->settingsTableRepo->readSettings();
foreach ($this->settingsTableRepo->toArray($settings) as $setting_key => $setting_value) {
$value = print_r($setting_value, true);
$value = Utilities\Strings::truncateText($value, 600);

View File

@ -6,20 +6,41 @@ namespace App\Console\Command\Settings;
use App\Console\Command\CommandAbstract;
use App\Entity;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'azuracast:settings:set',
description: 'Set the value of a setting in the AzuraCast settings database.',
)]
class SetCommand extends CommandAbstract
{
public function __invoke(
SymfonyStyle $io,
Entity\Repository\SettingsRepository $settingsTableRepo,
string $settingKey,
string $settingValue
): int {
public function __construct(
protected Entity\Repository\SettingsRepository $settingsTableRepo,
) {
parent::__construct();
}
protected function configure(): void
{
$this->addArgument('setting-key', InputArgument::REQUIRED)
->addArgument('setting-value', InputArgument::REQUIRED);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$settingKey = $input->getArgument('setting-key');
$settingValue = $input->getArgument('setting-value');
$io->title('AzuraCast Settings');
if (strtolower($settingValue) === 'null') {
$settingsTableRepo->writeSettings([$settingKey => null]);
$this->settingsTableRepo->writeSettings([$settingKey => null]);
$io->success(sprintf('Setting "%s" removed.', $settingKey));
return 0;
@ -29,7 +50,7 @@ class SetCommand extends CommandAbstract
$settingValue = json_decode($settingValue, true, 512, JSON_THROW_ON_ERROR);
}
$settingsTableRepo->writeSettings([$settingKey => $settingValue]);
$this->settingsTableRepo->writeSettings([$settingKey => $settingValue]);
$io->success(sprintf('Setting "%s" updated.', $settingKey));

View File

@ -7,30 +7,46 @@ namespace App\Console\Command;
use App\Entity;
use App\Environment;
use App\Service\AzuraCastCentral;
use Psr\Container\ContainerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'azuracast:setup',
description: 'Run all general AzuraCast setup steps.',
)]
class SetupCommand extends CommandAbstract
{
public function __invoke(
SymfonyStyle $io,
OutputInterface $output,
Environment $environment,
ContainerInterface $di,
Entity\Repository\SettingsRepository $settingsRepo,
Entity\Repository\StationRepository $stationRepo,
Entity\Repository\StorageLocationRepository $storageLocationRepo,
AzuraCastCentral $acCentral,
bool $update = false,
bool $loadFixtures = false
): int {
public function __construct(
protected Environment $environment,
protected Entity\Repository\SettingsRepository $settingsRepo,
protected AzuraCastCentral $acCentral,
) {
parent::__construct();
}
protected function configure(): void
{
$this->addOption('update', null, InputOption::VALUE_NONE)
->addOption('load-fixtures', null, InputOption::VALUE_NONE)
->addOption('release', null, InputOption::VALUE_NONE);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$update = (bool)$input->getOption('update');
$loadFixtures = (bool)$input->getOption('load-fixtures');
$io->title(__('AzuraCast Setup'));
$io->writeln(__('Welcome to AzuraCast. Please wait while some key dependencies of AzuraCast are set up...'));
$this->runCommand($output, 'azuracast:setup:initialize');
if ($loadFixtures || (!$environment->isProduction() && !$update)) {
if ($loadFixtures || (!$this->environment->isProduction() && !$update)) {
$io->newLine();
$io->section(__('Installing Data Fixtures'));
@ -43,9 +59,9 @@ class SetupCommand extends CommandAbstract
$this->runCommand($output, 'azuracast:radio:restart');
// Update system setting logging when updates were last run.
$settings = $settingsRepo->readSettings();
$settings = $this->settingsRepo->readSettings();
$settings->updateUpdateLastRun();
$settingsRepo->writeSettings($settings);
$this->settingsRepo->writeSettings($settings);
if ($update) {
$io->success(
@ -54,7 +70,7 @@ class SetupCommand extends CommandAbstract
]
);
} else {
$public_ip = $acCentral->getIp(false);
$public_ip = $this->acCentral->getIp(false);
/** @noinspection HttpUrlsUsage */
$io->success(

View File

@ -12,20 +12,32 @@ use Doctrine\ORM\EntityManagerInterface;
use Psr\Container\ContainerInterface;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'azuracast:setup:fixtures',
description: 'Install fixtures for demo / local development.',
)]
class SetupFixturesCommand extends CommandAbstract
{
public function __invoke(
SymfonyStyle $io,
EntityManagerInterface $em,
ContainerInterface $di,
Environment $environment
): int {
public function __construct(
protected EntityManagerInterface $em,
protected ContainerInterface $di,
protected Environment $environment,
) {
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$loader = new Loader();
// Dependency-inject the fixtures and load them.
$fixturesDir = $environment->getBaseDirectory() . '/src/Entity/Fixture';
$fixturesDir = $this->environment->getBaseDirectory() . '/src/Entity/Fixture';
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($fixturesDir),
@ -39,13 +51,13 @@ class SetupFixturesCommand extends CommandAbstract
}
$className = 'App\\Entity\\Fixture\\' . $fileName;
$fixture = $di->get($className);
$fixture = $this->di->get($className);
$loader->addFixture($fixture);
}
$purger = new ORMPurger($em);
$executor = new ORMExecutor($em, $purger);
$purger = new ORMPurger($this->em);
$executor = new ORMExecutor($this->em, $purger);
$executor->execute($loader->getFixtures());
$io->success(__('Fixtures loaded.'));

View File

@ -6,17 +6,36 @@ namespace App\Console\Command;
use App;
use App\Sync\Runner;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
#[AsCommand(
name: 'sync:run',
description: 'Run one or more scheduled synchronization tasks.',
)]
class SyncCommand extends CommandAbstract
{
public function __invoke(
Runner $sync,
?string $task = null,
bool $force = false
): int {
$task ??= App\Event\GetSyncTasks::SYNC_NOWPLAYING;
public function __construct(
protected Runner $sync,
) {
parent::__construct();
}
$sync->runSyncTask($task, $force);
protected function configure(): void
{
$this->addArgument('task', InputArgument::OPTIONAL)
->addOption('force', null, InputOption::VALUE_NONE);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$task = $input->getArgument('task') ?? App\Event\GetSyncTasks::SYNC_NOWPLAYING;
$force = (bool)$input->getOption('force');
$this->sync->runSyncTask($task, $force);
return 0;
}
}

View File

@ -7,17 +7,29 @@ namespace App\Console\Command\Users;
use App\Console\Command\CommandAbstract;
use App\Entity;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'azuracast:account:list',
description: 'List all accounts in the system.',
)]
class ListCommand extends CommandAbstract
{
public function __invoke(
SymfonyStyle $io,
EntityManagerInterface $em
): int {
public function __construct(
protected EntityManagerInterface $em
) {
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$io->title('AzuraCast User Accounts');
$usersRaw = $em->getRepository(Entity\User::class)
$usersRaw = $this->em->getRepository(Entity\User::class)
->findAll();
$headers = [
@ -44,7 +56,6 @@ class ListCommand extends CommandAbstract
];
}
$io->table($headers, $users);
return 0;
}

View File

@ -8,30 +8,49 @@ use App\Console\Command\CommandAbstract;
use App\Entity;
use App\Http\RouterInterface;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'azuracast:account:login-token',
description: 'Create a unique login recovery URL for the specified account.',
)]
class LoginTokenCommand extends CommandAbstract
{
public function __invoke(
SymfonyStyle $io,
EntityManagerInterface $em,
Entity\Repository\UserLoginTokenRepository $loginTokenRepo,
RouterInterface $router,
string $email
): int {
public function __construct(
protected EntityManagerInterface $em,
protected Entity\Repository\UserLoginTokenRepository $loginTokenRepo,
protected RouterInterface $router,
) {
parent::__construct();
}
protected function configure(): void
{
$this->addArgument('email', InputArgument::REQUIRED);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$email = $input->getArgument('email');
$io->title('Generate Account Login Recovery URL');
$user = $em->getRepository(Entity\User::class)
$user = $this->em->getRepository(Entity\User::class)
->findOneBy(['email' => $email]);
if ($user instanceof Entity\User) {
$loginToken = $loginTokenRepo->createToken($user);
$loginToken = $this->loginTokenRepo->createToken($user);
$url = $router->named(
'account:recover',
['token' => $loginToken],
[],
true
$url = $this->router->named(
route_name: 'account:recover',
route_params: ['token' => $loginToken],
absolute: true
);
$io->text([

View File

@ -8,18 +8,38 @@ use App\Console\Command\CommandAbstract;
use App\Entity;
use App\Utilities;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'azuracast:account:reset-password',
description: 'Reset the password of the specified account.',
)]
class ResetPasswordCommand extends CommandAbstract
{
public function __invoke(
SymfonyStyle $io,
EntityManagerInterface $em,
string $email
): int {
public function __construct(
protected EntityManagerInterface $em
) {
parent::__construct();
}
protected function configure(): void
{
$this->addArgument('email', InputArgument::REQUIRED);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$email = $input->getArgument('email');
$io->title('Reset Account Password');
$user = $em->getRepository(Entity\User::class)
$user = $this->em->getRepository(Entity\User::class)
->findOneBy(['email' => $email]);
if ($user instanceof Entity\User) {
@ -28,8 +48,8 @@ class ResetPasswordCommand extends CommandAbstract
$user->setNewPassword($temp_pw);
$user->setTwoFactorSecret();
$em->persist($user);
$em->flush();
$this->em->persist($user);
$this->em->flush();
$io->text([
'The account password has been reset. The new temporary password is:',

View File

@ -7,31 +7,51 @@ namespace App\Console\Command\Users;
use App\Console\Command\CommandAbstract;
use App\Entity;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'azuracast:account:set-administrator',
description: 'Set the account specified as a global administrator.',
)]
class SetAdministratorCommand extends CommandAbstract
{
public function __invoke(
SymfonyStyle $io,
EntityManagerInterface $em,
Entity\Repository\RolePermissionRepository $perms_repo,
string $email
): int {
public function __construct(
protected EntityManagerInterface $em,
protected Entity\Repository\RolePermissionRepository $permsRepo,
) {
parent::__construct();
}
protected function configure(): void
{
$this->addArgument('email', InputArgument::REQUIRED);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$email = $input->getArgument('email');
$io->title('Set Administrator');
$user = $em->getRepository(Entity\User::class)
$user = $this->em->getRepository(Entity\User::class)
->findOneBy(['email' => $email]);
if ($user instanceof Entity\User) {
$adminRole = $perms_repo->ensureSuperAdministratorRole();
$adminRole = $this->permsRepo->ensureSuperAdministratorRole();
$user_roles = $user->getRoles();
if (!$user_roles->contains($adminRole)) {
$user_roles->add($adminRole);
}
$em->persist($user);
$em->flush();
$this->em->persist($user);
$this->em->flush();
$io->text(
__(

View File

@ -5,12 +5,16 @@ declare(strict_types=1);
namespace App\Event;
use App\Console\Application;
use Psr\Container\ContainerInterface;
use Symfony\Contracts\EventDispatcher\Event;
class BuildConsoleCommands extends Event
{
protected array $aliases = [];
public function __construct(
protected Application $cli
protected Application $cli,
protected ContainerInterface $di
) {
}
@ -18,4 +22,19 @@ class BuildConsoleCommands extends Event
{
return $this->cli;
}
public function getContainer(): ContainerInterface
{
return $this->di;
}
public function addAliases(array $aliases): void
{
$this->aliases = array_merge($this->aliases, $aliases);
}
public function getAliases(): array
{
return $this->aliases;
}
}