Made it possible to sort on multiple columns in db:list. Cleaned up output API.
This commit is contained in:
10
src/Cli.php
10
src/Cli.php
@@ -34,11 +34,19 @@ class Cli extends Application
|
||||
}
|
||||
|
||||
$definition->addOption(
|
||||
new InputOption('with-context', null, InputOption::VALUE_NEGATABLE, 'Add context to output messages.')
|
||||
new InputOption('context', null, InputOption::VALUE_NEGATABLE, 'Add context to output messages.')
|
||||
);
|
||||
|
||||
$definition->addOption(new InputOption('trace', null, InputOption::VALUE_NONE, 'Enable tracing mode.'));
|
||||
|
||||
$definition->addOption(
|
||||
new InputOption(
|
||||
'output', 'o', InputOption::VALUE_REQUIRED,
|
||||
sprintf('Output mode. Can be [%s].', implode(', ', Command::DISPLAY_OUTPUT)),
|
||||
Command::DISPLAY_OUTPUT[0]
|
||||
)
|
||||
);
|
||||
|
||||
return $definition;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ class Command extends BaseCommand
|
||||
{
|
||||
use LockableTrait;
|
||||
|
||||
protected array $outputs = [
|
||||
public const DISPLAY_OUTPUT = [
|
||||
'table',
|
||||
'json',
|
||||
'yaml',
|
||||
@@ -31,11 +31,11 @@ class Command extends BaseCommand
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
if ($input->hasOption('with-context') && true === $input->getOption('with-context')) {
|
||||
if ($input->hasOption('context') && true === $input->getOption('context')) {
|
||||
Config::save('logs.context', true);
|
||||
}
|
||||
|
||||
if ($input->hasOption('no-with-context') && true === $input->getOption('no-with-context')) {
|
||||
if ($input->hasOption('no-context') && true === $input->getOption('no-context')) {
|
||||
Config::save('logs.context', false);
|
||||
}
|
||||
|
||||
@@ -249,5 +249,19 @@ class Command extends BaseCommand
|
||||
|
||||
$suggestions->suggestValues($suggest);
|
||||
}
|
||||
|
||||
if ($input->mustSuggestOptionValuesFor('output')) {
|
||||
$currentValue = $input->getCompletionValue();
|
||||
|
||||
$suggest = [];
|
||||
|
||||
foreach (self::DISPLAY_OUTPUT as $name) {
|
||||
if (empty($currentValue) || str_starts_with($name, $currentValue)) {
|
||||
$suggest[] = $name;
|
||||
}
|
||||
}
|
||||
|
||||
$suggestions->suggestValues($suggest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,6 @@ use App\Command;
|
||||
use App\Libs\Config;
|
||||
use App\Libs\Options;
|
||||
use RuntimeException;
|
||||
use Symfony\Component\Console\Completion\CompletionInput;
|
||||
use Symfony\Component\Console\Completion\CompletionSuggestions;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
@@ -22,13 +20,6 @@ final class ListCommand extends Command
|
||||
{
|
||||
$this->setName('backend:library:list')
|
||||
->setDescription('Get Backend libraries list.')
|
||||
->addOption(
|
||||
'output',
|
||||
'o',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
sprintf('Output mode. Can be [%s].', implode(', ', $this->outputs)),
|
||||
$this->outputs[0],
|
||||
)
|
||||
->addOption('include-raw-response', null, InputOption::VALUE_NONE, 'Include unfiltered raw response.')
|
||||
->addOption('config', 'c', InputOption::VALUE_REQUIRED, 'Use Alternative config file.')
|
||||
->addArgument('backend', InputArgument::REQUIRED, 'Backend name.');
|
||||
@@ -102,29 +93,4 @@ final class ListCommand extends Command
|
||||
return self::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
|
||||
{
|
||||
parent::complete($input, $suggestions);
|
||||
|
||||
$methods = [
|
||||
'output' => 'outputs',
|
||||
];
|
||||
|
||||
foreach ($methods as $key => $of) {
|
||||
if ($input->mustSuggestOptionValuesFor($key)) {
|
||||
$currentValue = $input->getCompletionValue();
|
||||
|
||||
$suggest = [];
|
||||
|
||||
foreach ($this->{$of} as $name) {
|
||||
if (empty($currentValue) || str_starts_with($name, $currentValue)) {
|
||||
$suggest[] = $name;
|
||||
}
|
||||
}
|
||||
|
||||
$suggestions->suggestValues($suggest);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,16 +30,6 @@ final class MismatchCommand extends Command
|
||||
->setDescription(
|
||||
'Find possible mis-identified item in a library. This only works for Media that follow Plex naming format.'
|
||||
)
|
||||
->addOption(
|
||||
'output',
|
||||
'o',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
sprintf(
|
||||
'Output mode. Can be [%s]. Modes other than table mode gives more info.',
|
||||
implode(', ', $this->outputs)
|
||||
),
|
||||
$this->outputs[0],
|
||||
)
|
||||
->addOption('percentage', 'p', InputOption::VALUE_OPTIONAL, 'Acceptable percentage.', 50.0)
|
||||
->addOption(
|
||||
'method',
|
||||
@@ -269,7 +259,6 @@ final class MismatchCommand extends Command
|
||||
parent::complete($input, $suggestions);
|
||||
|
||||
$methods = [
|
||||
'output' => 'outputs',
|
||||
'method' => 'methods',
|
||||
];
|
||||
|
||||
|
||||
@@ -8,8 +8,6 @@ use App\Command;
|
||||
use App\Libs\Config;
|
||||
use App\Libs\Options;
|
||||
use RuntimeException;
|
||||
use Symfony\Component\Console\Completion\CompletionInput;
|
||||
use Symfony\Component\Console\Completion\CompletionSuggestions;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
@@ -24,16 +22,6 @@ final class UnmatchedCommand extends Command
|
||||
$this->setName('backend:library:unmatched')
|
||||
->setDescription('Find top level Items in library that has no external ids.')
|
||||
->addOption('show-all', null, InputOption::VALUE_NONE, 'Show all items regardless of the match status.')
|
||||
->addOption(
|
||||
'output',
|
||||
'o',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
sprintf(
|
||||
'Output mode. Can be [%s]. Modes other than table mode gives more info.',
|
||||
implode(', ', $this->outputs)
|
||||
),
|
||||
$this->outputs[0],
|
||||
)
|
||||
->addOption(
|
||||
'timeout',
|
||||
null,
|
||||
@@ -151,29 +139,4 @@ final class UnmatchedCommand extends Command
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
|
||||
{
|
||||
parent::complete($input, $suggestions);
|
||||
|
||||
$methods = [
|
||||
'output' => 'outputs',
|
||||
];
|
||||
|
||||
foreach ($methods as $key => $of) {
|
||||
if ($input->mustSuggestOptionValuesFor($key)) {
|
||||
$currentValue = $input->getCompletionValue();
|
||||
|
||||
$suggest = [];
|
||||
|
||||
foreach ($this->{$of} as $name) {
|
||||
if (empty($currentValue) || str_starts_with($name, $currentValue)) {
|
||||
$suggest[] = $name;
|
||||
}
|
||||
}
|
||||
|
||||
$suggestions->suggestValues($suggest);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,6 @@ use App\Command;
|
||||
use App\Libs\Config;
|
||||
use App\Libs\Options;
|
||||
use RuntimeException;
|
||||
use Symfony\Component\Console\Completion\CompletionInput;
|
||||
use Symfony\Component\Console\Completion\CompletionSuggestions;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
@@ -22,13 +20,6 @@ final class IdCommand extends Command
|
||||
{
|
||||
$this->setName('backend:search:id')
|
||||
->setDescription('Get backend metadata related to specific id.')
|
||||
->addOption(
|
||||
'output',
|
||||
'o',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
sprintf('Output mode. Can be [%s].', implode(', ', $this->outputs)),
|
||||
$this->outputs[0],
|
||||
)
|
||||
->addOption('include-raw-response', null, InputOption::VALUE_NONE, 'Include unfiltered raw response.')
|
||||
->addOption('config', 'c', InputOption::VALUE_REQUIRED, 'Use Alternative config file.')
|
||||
->addArgument('backend', InputArgument::REQUIRED, 'Backend name.')
|
||||
@@ -89,29 +80,4 @@ final class IdCommand extends Command
|
||||
return self::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
|
||||
{
|
||||
parent::complete($input, $suggestions);
|
||||
|
||||
$methods = [
|
||||
'output' => 'outputs',
|
||||
];
|
||||
|
||||
foreach ($methods as $key => $of) {
|
||||
if ($input->mustSuggestOptionValuesFor($key)) {
|
||||
$currentValue = $input->getCompletionValue();
|
||||
|
||||
$suggest = [];
|
||||
|
||||
foreach ($this->{$of} as $name) {
|
||||
if (empty($currentValue) || str_starts_with($name, $currentValue)) {
|
||||
$suggest[] = $name;
|
||||
}
|
||||
}
|
||||
|
||||
$suggestions->suggestValues($suggest);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,6 @@ use App\Command;
|
||||
use App\Libs\Config;
|
||||
use App\Libs\Options;
|
||||
use RuntimeException;
|
||||
use Symfony\Component\Console\Completion\CompletionInput;
|
||||
use Symfony\Component\Console\Completion\CompletionSuggestions;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
@@ -22,13 +20,6 @@ final class QueryCommand extends Command
|
||||
{
|
||||
$this->setName('backend:search:query')
|
||||
->setDescription('Search backend libraries for specific title keyword.')
|
||||
->addOption(
|
||||
'output',
|
||||
'o',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
sprintf('Output mode. Can be [%s].', implode(', ', $this->outputs)),
|
||||
$this->outputs[0],
|
||||
)
|
||||
->addOption('include-raw-response', null, InputOption::VALUE_NONE, 'Include unfiltered raw response.')
|
||||
->addOption('limit', 'l', InputOption::VALUE_REQUIRED, 'Limit returned results.', 25)
|
||||
->addOption('config', 'c', InputOption::VALUE_REQUIRED, 'Use Alternative config file.')
|
||||
@@ -94,29 +85,4 @@ final class QueryCommand extends Command
|
||||
return self::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
|
||||
{
|
||||
parent::complete($input, $suggestions);
|
||||
|
||||
$methods = [
|
||||
'output' => 'outputs',
|
||||
];
|
||||
|
||||
foreach ($methods as $key => $of) {
|
||||
if ($input->mustSuggestOptionValuesFor($key)) {
|
||||
$currentValue = $input->getCompletionValue();
|
||||
|
||||
$suggest = [];
|
||||
|
||||
foreach ($this->{$of} as $name) {
|
||||
if (empty($currentValue) || str_starts_with($name, $currentValue)) {
|
||||
$suggest[] = $name;
|
||||
}
|
||||
}
|
||||
|
||||
$suggestions->suggestValues($suggest);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,6 @@ use App\Command;
|
||||
use App\Libs\Config;
|
||||
use App\Libs\Options;
|
||||
use RuntimeException;
|
||||
use Symfony\Component\Console\Completion\CompletionInput;
|
||||
use Symfony\Component\Console\Completion\CompletionSuggestions;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
@@ -23,13 +21,6 @@ final class ListCommand extends Command
|
||||
{
|
||||
$this->setName('backend:users:list')
|
||||
->setDescription('Get backend users list.')
|
||||
->addOption(
|
||||
'output',
|
||||
'o',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
sprintf('Output mode. Can be [%s].', implode(', ', $this->outputs)),
|
||||
$this->outputs[0],
|
||||
)
|
||||
->addOption('with-tokens', 't', InputOption::VALUE_NONE, 'Include access tokens in response.')
|
||||
->addOption('include-raw-response', null, InputOption::VALUE_NONE, 'Include unfiltered raw response.')
|
||||
->addOption('config', 'c', InputOption::VALUE_REQUIRED, 'Use Alternative config file.')
|
||||
@@ -108,29 +99,4 @@ final class ListCommand extends Command
|
||||
return self::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
|
||||
{
|
||||
parent::complete($input, $suggestions);
|
||||
|
||||
$methods = [
|
||||
'output' => 'outputs',
|
||||
];
|
||||
|
||||
foreach ($methods as $key => $of) {
|
||||
if ($input->mustSuggestOptionValuesFor($key)) {
|
||||
$currentValue = $input->getCompletionValue();
|
||||
|
||||
$suggest = [];
|
||||
|
||||
foreach ($this->{$of} as $name) {
|
||||
if (empty($currentValue) || str_starts_with($name, $currentValue)) {
|
||||
$suggest[] = $name;
|
||||
}
|
||||
}
|
||||
|
||||
$suggestions->suggestValues($suggest);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
final class ListCommand extends Command
|
||||
{
|
||||
public const CHANGEABLE_COLUMNS = [
|
||||
private const COLUMNS_CHANGEABLE = [
|
||||
iFace::COLUMN_WATCHED,
|
||||
iFace::COLUMN_VIA,
|
||||
iFace::COLUMN_TITLE,
|
||||
@@ -34,6 +34,18 @@ final class ListCommand extends Command
|
||||
iFace::COLUMN_UPDATED,
|
||||
];
|
||||
|
||||
private const COLUMNS_SORTABLE = [
|
||||
iFace::COLUMN_ID,
|
||||
iFace::COLUMN_TYPE,
|
||||
iFace::COLUMN_UPDATED,
|
||||
iFace::COLUMN_WATCHED,
|
||||
iFace::COLUMN_VIA,
|
||||
iFace::COLUMN_TITLE,
|
||||
iFace::COLUMN_YEAR,
|
||||
iFace::COLUMN_SEASON,
|
||||
iFace::COLUMN_EPISODE,
|
||||
];
|
||||
|
||||
private PDO $pdo;
|
||||
|
||||
public function __construct(private StorageInterface $storage)
|
||||
@@ -51,9 +63,8 @@ final class ListCommand extends Command
|
||||
'via',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Limit results to this specified server. This filter is not reliable. and changes based on last server query.'
|
||||
'Limit results to this specified backend. This filter is not reliable. and changes based on last backend query.'
|
||||
)
|
||||
->addOption('output', null, InputOption::VALUE_REQUIRED, 'Display output as [json, yaml, table]', 'table')
|
||||
->addOption(
|
||||
'type',
|
||||
null,
|
||||
@@ -65,14 +76,17 @@ final class ListCommand extends Command
|
||||
->addOption('episode', null, InputOption::VALUE_REQUIRED, 'Select episode number')
|
||||
->addOption('year', null, InputOption::VALUE_REQUIRED, 'Select year.')
|
||||
->addOption('id', null, InputOption::VALUE_REQUIRED, 'Select db record number')
|
||||
->addOption('sort', null, InputOption::VALUE_REQUIRED, 'sort order by [id, updated]', 'updated')
|
||||
->addOption('asc', null, InputOption::VALUE_NONE, 'Sort records in ascending order.')
|
||||
->addOption('desc', null, InputOption::VALUE_NONE, 'Sort records in descending order. (Default)')
|
||||
->addOption(
|
||||
'sort',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
|
||||
'Set sort by columns. for example, <comment>--sort season:asc --sort episode:desc</comment>',
|
||||
)
|
||||
->addOption(
|
||||
'metadata-as',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Display metadata from this server instead of latest.'
|
||||
'Display metadata from this backend instead of latest.'
|
||||
)
|
||||
->setDescription('List Database entries.');
|
||||
|
||||
@@ -93,13 +107,19 @@ final class ListCommand extends Command
|
||||
'metadata',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'Search in (metadata) provided by servers JSON Field using (--key, --value) options.'
|
||||
'Search in (metadata) provided by backends JSON Field using (--key, --value) options.'
|
||||
)
|
||||
->addOption(
|
||||
'extra',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'Search in (extra information) JSON Field using (--key, --value) options.'
|
||||
)
|
||||
->addOption(
|
||||
'dump-query',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'Dump the generated query and exit.'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -108,35 +128,35 @@ final class ListCommand extends Command
|
||||
*/
|
||||
protected function runCommand(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$list = [];
|
||||
|
||||
$limit = (int)$input->getOption('limit');
|
||||
|
||||
$es = fn(string $val) => $this->storage->identifier($val);
|
||||
|
||||
$params = [
|
||||
'limit' => $limit <= 0 ? 20 : $limit,
|
||||
];
|
||||
|
||||
$where = [];
|
||||
$sql = $where = [];
|
||||
|
||||
$sql = "SELECT * FROM state ";
|
||||
$sql[] = sprintf('SELECT * FROM %s', $es('state'));
|
||||
|
||||
if ($input->getOption('id')) {
|
||||
$where[] = iFace::COLUMN_ID . ' = :id';
|
||||
$where[] = $es(iFace::COLUMN_ID) . ' = :id';
|
||||
$params['id'] = $input->getOption('id');
|
||||
}
|
||||
|
||||
if ($input->getOption('via')) {
|
||||
$where[] = iFace::COLUMN_VIA . ' = :via';
|
||||
$where[] = $es(iFace::COLUMN_VIA) . ' = :via';
|
||||
$params['via'] = $input->getOption('via');
|
||||
}
|
||||
|
||||
if ($input->getOption('year')) {
|
||||
$where[] = iFace::COLUMN_YEAR . ' = :year';
|
||||
$where[] = $es(iFace::COLUMN_YEAR) . ' = :year';
|
||||
$params['year'] = $input->getOption('year');
|
||||
}
|
||||
|
||||
if ($input->getOption('type')) {
|
||||
$where[] = iFace::COLUMN_TYPE . ' = :type';
|
||||
$where[] = $es(iFace::COLUMN_TYPE) . ' = :type';
|
||||
$params['type'] = match ($input->getOption('type')) {
|
||||
iFace::TYPE_MOVIE => iFace::TYPE_MOVIE,
|
||||
default => iFace::TYPE_EPISODE,
|
||||
@@ -144,17 +164,17 @@ final class ListCommand extends Command
|
||||
}
|
||||
|
||||
if ($input->getOption('title')) {
|
||||
$where[] = iFace::COLUMN_TITLE . " LIKE '%' || :title || '%'";
|
||||
$where[] = $es(iFace::COLUMN_TITLE) . ' LIKE "%" || :title || "%"';
|
||||
$params['title'] = $input->getOption('title');
|
||||
}
|
||||
|
||||
if (null !== $input->getOption('season')) {
|
||||
$where[] = iFace::COLUMN_SEASON . ' = :season';
|
||||
$where[] = $es(iFace::COLUMN_SEASON) . ' = :season';
|
||||
$params['season'] = $input->getOption('season');
|
||||
}
|
||||
|
||||
if (null !== $input->getOption('episode')) {
|
||||
$where[] = iFace::COLUMN_EPISODE . ' = :episode';
|
||||
$where[] = $es(iFace::COLUMN_EPISODE) . ' = :episode';
|
||||
$params['episode'] = $input->getOption('episode');
|
||||
}
|
||||
|
||||
@@ -203,20 +223,53 @@ final class ListCommand extends Command
|
||||
}
|
||||
|
||||
if (count($where) >= 1) {
|
||||
$sql .= 'WHERE ' . implode(' AND ', $where);
|
||||
$sql[] = 'WHERE ' . implode(' AND ', $where);
|
||||
}
|
||||
|
||||
$sort = match ($input->getOption('sort')) {
|
||||
'id' => iFace::COLUMN_ID,
|
||||
'season' => iFace::COLUMN_SEASON,
|
||||
'episode' => iFace::COLUMN_EPISODE,
|
||||
'type' => iFace::COLUMN_TYPE,
|
||||
default => iFace::COLUMN_UPDATED,
|
||||
};
|
||||
$sorts = [];
|
||||
|
||||
$sortOrder = ($input->getOption('asc')) ? 'ASC' : 'DESC';
|
||||
foreach ($input->getOption('sort') as $sort) {
|
||||
if (1 !== preg_match('/(?P<field>\w+)(:(?P<dir>\w+))?/', $sort, $matches)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$sql .= " ORDER BY {$sort} {$sortOrder} LIMIT :limit";
|
||||
if (null === ($matches['field'] ?? null) || false === in_array($matches['field'], self::COLUMNS_SORTABLE)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$sorts[] = sprintf(
|
||||
'%s %s',
|
||||
$es($matches['field']),
|
||||
match (strtolower($matches['dir'] ?? 'desc')) {
|
||||
default => 'DESC',
|
||||
'asc' => 'ASC',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (count($sorts) < 1) {
|
||||
$sorts[] = sprintf('%s DESC', $es('updated'));
|
||||
}
|
||||
|
||||
$sql[] = 'ORDER BY ' . implode(', ', $sorts) . ' LIMIT :limit';
|
||||
$sql = implode(' ', $sql);
|
||||
|
||||
if ($input->getOption('dump-query')) {
|
||||
$arr = [
|
||||
'query' => $sql,
|
||||
'parameters' => $params,
|
||||
'raw' => $this->storage->getRawSQLString($sql, $params),
|
||||
];
|
||||
|
||||
if ('table' === $input->getOption('output')) {
|
||||
$arr['parameters'] = arrayToString($params);
|
||||
unset($arr['raw']);
|
||||
$arr = [$arr];
|
||||
}
|
||||
|
||||
$this->displayContent($arr, $output, $input->getOption('output'));
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
@@ -262,15 +315,16 @@ final class ListCommand extends Command
|
||||
if (null !== ($via = $input->getOption('metadata-as'))) {
|
||||
$path = $row[iFace::COLUMN_META_DATA][$via] ?? [];
|
||||
|
||||
foreach (self::CHANGEABLE_COLUMNS as $column) {
|
||||
foreach (self::COLUMNS_CHANGEABLE as $column) {
|
||||
if (null === ($path[$column] ?? null)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$row[$column] = 'int' === get_debug_type($row[$column]) ? (int)$path[$column] : $path[$column];
|
||||
}
|
||||
if (null !== ($row[iFace::COLUMN_EXTRA][$via][iFace::COLUMN_EXTRA_DATE] ?? null)) {
|
||||
$row[iFace::COLUMN_UPDATED] = $row[iFace::COLUMN_EXTRA][$via][iFace::COLUMN_EXTRA_DATE];
|
||||
|
||||
if (null !== ($dateFromBackend = $path[iFace::COLUMN_META_DATA_PLAYED_AT] ?? $path[iFace::COLUMN_META_DATA_ADDED_AT] ?? null)) {
|
||||
$row[iFace::COLUMN_UPDATED] = $dateFromBackend;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,44 +334,37 @@ final class ListCommand extends Command
|
||||
|
||||
unset($row);
|
||||
|
||||
if ('json' === $input->getOption('output')) {
|
||||
$output->writeln(
|
||||
json_encode(
|
||||
1 === count($rows) ? $rows[0] : $rows,
|
||||
JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
|
||||
)
|
||||
);
|
||||
} elseif ('yaml' === $input->getOption('output')) {
|
||||
$output->writeln(Yaml::dump(1 === count($rows) ? $rows[0] : $rows, 8, 2));
|
||||
} else {
|
||||
$x = 0;
|
||||
if ('table' === $input->getOption('output')) {
|
||||
$list = [];
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$row[iFace::COLUMN_UPDATED] = $row[iFace::COLUMN_UPDATED]->getTimestamp();
|
||||
$row[iFace::COLUMN_WATCHED] = (int)$row[iFace::COLUMN_WATCHED];
|
||||
$entity = Container::get(iFace::class)->fromArray($row);
|
||||
|
||||
$x++;
|
||||
|
||||
$list[] = [
|
||||
$entity->id,
|
||||
ucfirst($entity->type),
|
||||
$entity->getName(),
|
||||
$entity->via ?? '??',
|
||||
makeDate($entity->updated)->format('Y-m-d H:i:s T'),
|
||||
$entity->isWatched() ? 'Yes' : 'No',
|
||||
ag($entity->extra[$entity->via] ?? [], iFace::COLUMN_EXTRA_EVENT, '-'),
|
||||
$item = [
|
||||
'id' => $entity->id,
|
||||
'Type' => ucfirst($entity->type),
|
||||
'Title' => $entity->getName(),
|
||||
'Via (Last)' => $entity->via ?? '??',
|
||||
'Date' => makeDate($entity->updated)->format('Y-m-d H:i:s T'),
|
||||
'Played' => $entity->isWatched() ? 'Yes' : 'No',
|
||||
'Via (Event)' => ag($entity->extra[$entity->via] ?? [], iFace::COLUMN_EXTRA_EVENT, '-'),
|
||||
];
|
||||
|
||||
if ($x < $rowCount) {
|
||||
$list[] = new TableSeparator();
|
||||
}
|
||||
$list[] = $item;
|
||||
$list[] = new TableSeparator();
|
||||
}
|
||||
|
||||
$rows = null;
|
||||
|
||||
(new Table($output))->setHeaders(['Id', 'Type', 'Title', 'Via (Last)', 'Date', 'Played', 'Via Event'])
|
||||
->setStyle('box')->setRows($list)->render();
|
||||
if (count($list) >= 2) {
|
||||
array_pop($list);
|
||||
}
|
||||
|
||||
(new Table($output))->setHeaders(array_keys($list[0] ?? []))->setStyle('box')->setRows($list)->render();
|
||||
} else {
|
||||
$this->displayContent($rows, $output, $input->getOption('output'));
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
@@ -355,14 +402,16 @@ final class ListCommand extends Command
|
||||
$suggestions->suggestValues($suggest);
|
||||
}
|
||||
|
||||
if ($input->mustSuggestOptionValuesFor('output')) {
|
||||
if ($input->mustSuggestOptionValuesFor('sort')) {
|
||||
$currentValue = $input->getCompletionValue();
|
||||
|
||||
$suggest = [];
|
||||
|
||||
foreach (['json', 'yaml', 'table'] as $name) {
|
||||
if (empty($currentValue) || str_starts_with($name, $currentValue)) {
|
||||
$suggest[] = $name;
|
||||
foreach (self::COLUMNS_SORTABLE as $name) {
|
||||
foreach ([$name . ':desc', $name . ':asc'] as $subName) {
|
||||
if (empty($currentValue) || true === str_starts_with($subName, $currentValue)) {
|
||||
$suggest[] = $subName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Commands\System;
|
||||
|
||||
use App\Command;
|
||||
use Symfony\Component\Console\Completion\CompletionInput;
|
||||
use Symfony\Component\Console\Completion\CompletionSuggestions;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
final class EnvCommand extends Command
|
||||
@@ -16,13 +13,6 @@ final class EnvCommand extends Command
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setName('system:env')
|
||||
->addOption(
|
||||
'output',
|
||||
'o',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
sprintf('Output mode. Can be [%s].', implode(', ', $this->outputs)),
|
||||
$this->outputs[0],
|
||||
)
|
||||
->setDescription('Dump loaded environment variables.');
|
||||
}
|
||||
|
||||
@@ -53,30 +43,4 @@ final class EnvCommand extends Command
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
|
||||
{
|
||||
parent::complete($input, $suggestions);
|
||||
|
||||
$methods = [
|
||||
'output' => 'outputs',
|
||||
];
|
||||
|
||||
foreach ($methods as $key => $of) {
|
||||
if ($input->mustSuggestOptionValuesFor($key)) {
|
||||
$currentValue = $input->getCompletionValue();
|
||||
|
||||
$suggest = [];
|
||||
|
||||
foreach ($this->{$of} as $name) {
|
||||
if (empty($currentValue) || str_starts_with($name, $currentValue)) {
|
||||
$suggest[] = $name;
|
||||
}
|
||||
}
|
||||
|
||||
$suggestions->suggestValues($suggest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -33,8 +33,15 @@ final class PDOAdapter implements StorageInterface
|
||||
'update' => null,
|
||||
];
|
||||
|
||||
private string $driver = 'sqlite';
|
||||
|
||||
public function __construct(private LoggerInterface $logger, private PDO $pdo)
|
||||
{
|
||||
$driver = $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
|
||||
|
||||
if (is_string($driver)) {
|
||||
$this->driver = $driver;
|
||||
}
|
||||
}
|
||||
|
||||
public function setOptions(array $options): self
|
||||
@@ -515,13 +522,45 @@ final class PDOAdapter implements StorageInterface
|
||||
*/
|
||||
public function getRawSQLString(string $sql, array $parameters): string
|
||||
{
|
||||
$keys = $replace = [];
|
||||
$replacer = [];
|
||||
|
||||
foreach ($parameters as $key => $val) {
|
||||
$keys[] = '/(\:' . preg_quote($key, '/') . ')(?:\b|\,)/';
|
||||
$replace[] = ctype_digit((string)$val) ? $val : "'{$val}'";
|
||||
$replacer['/(\:' . preg_quote($key, '/') . ')(?:\b|\,)/'] = ctype_digit(
|
||||
(string)$val
|
||||
) ? (int)$val : '"' . $val . '"';
|
||||
}
|
||||
|
||||
return preg_replace($keys, $replace, $sql);
|
||||
return preg_replace(array_keys($replacer), array_values($replacer), $sql);
|
||||
}
|
||||
|
||||
public function identifier(string $text, bool $quote = true): string
|
||||
{
|
||||
// table or column has to be valid ASCII name.
|
||||
// this is opinionated, but we only allow [a-zA-Z0-9_] in column/table name.
|
||||
if (!\preg_match('#\w#', $text)) {
|
||||
throw new \RuntimeException(
|
||||
sprintf(
|
||||
'Invalid identifier "%s": Column/table must be valid ASCII code.',
|
||||
$text
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// The first character cannot be [0-9]:
|
||||
if (\preg_match('/^\d/', $text)) {
|
||||
throw new \RuntimeException(
|
||||
sprintf(
|
||||
'Invalid identifier "%s": Must begin with a letter or underscore.',
|
||||
$text
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return !$quote ? $text : match ($this->driver) {
|
||||
'mssql' => '[' . $text . ']',
|
||||
'mysql' => '`' . $text . '`',
|
||||
default => '"' . $text . '"',
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user