diff --git a/src/Commands/Database/ListCommand.php b/src/Commands/Database/ListCommand.php
index c20afba3..b25b4ee4 100644
--- a/src/Commands/Database/ListCommand.php
+++ b/src/Commands/Database/ListCommand.php
@@ -8,19 +8,19 @@ use App\Command;
use App\Libs\Config;
use App\Libs\Container;
use App\Libs\Database\DatabaseInterface as iDB;
-use App\Libs\Entity\StateInterface as iFace;
+use App\Libs\Entity\StateInterface as iState;
use App\Libs\Guid;
+use App\Libs\Mappers\Import\DirectMapper;
use App\Libs\Routable;
use Exception;
use PDO;
use RuntimeException;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
-use Symfony\Component\Console\Helper\Table;
-use Symfony\Component\Console\Helper\TableSeparator;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Yaml\Yaml;
#[Routable(command: self::ROUTE)]
@@ -29,30 +29,30 @@ final class ListCommand extends Command
public const ROUTE = 'db:list';
private const COLUMNS_CHANGEABLE = [
- iFace::COLUMN_WATCHED,
- iFace::COLUMN_VIA,
- iFace::COLUMN_TITLE,
- iFace::COLUMN_YEAR,
- iFace::COLUMN_SEASON,
- iFace::COLUMN_EPISODE,
- iFace::COLUMN_UPDATED,
+ iState::COLUMN_WATCHED,
+ iState::COLUMN_VIA,
+ iState::COLUMN_TITLE,
+ iState::COLUMN_YEAR,
+ iState::COLUMN_SEASON,
+ iState::COLUMN_EPISODE,
+ iState::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,
+ iState::COLUMN_ID,
+ iState::COLUMN_TYPE,
+ iState::COLUMN_UPDATED,
+ iState::COLUMN_WATCHED,
+ iState::COLUMN_VIA,
+ iState::COLUMN_TITLE,
+ iState::COLUMN_YEAR,
+ iState::COLUMN_SEASON,
+ iState::COLUMN_EPISODE,
];
private PDO $pdo;
- public function __construct(private iDB $db)
+ public function __construct(private iDB $db, private DirectMapper $mapper)
{
$this->pdo = $this->db->getPdo();
@@ -139,6 +139,12 @@ final class ListCommand extends Command
InputOption::VALUE_NONE,
'Use equal check instead of LIKE for JSON field query.'
)
+ ->addOption(
+ 'mark-as',
+ 'm',
+ InputOption::VALUE_REQUIRED,
+ 'Change items play state. Expects [played, unplayed] as value. Requires interaction.'
+ )
->setDescription('List Database entries.')
->setHelp(
r(
@@ -169,6 +175,15 @@ final class ListCommand extends Command
{cmd} {route} --key 'backend_name.id' --value 'backend_item_id' --metadata
+ # How to mark items as played/unplayed?
+
+ Use the filters to narrow down the list to what you want to the state of, once you have the list
+ append the [-m, --mark-as] to the command with value of [played, unplayed]. This flag requires interaction.
+
+ Example, to mark a show that has id of [tvdb://269586], you would do something like.
+
+ {cmd} {route} --parent tvdb://269586 --mark-as played
+
HELP,
[
'cmd' => trim(commandContext()),
@@ -186,6 +201,10 @@ final class ListCommand extends Command
{
$limit = (int)$input->getOption('limit');
+ if (null !== ($changeState = $input->getOption('mark-as'))) {
+ $limit = PHP_INT_MAX;
+ }
+
$es = fn(string $val) => $this->db->identifier($val);
$params = [
@@ -197,40 +216,40 @@ final class ListCommand extends Command
$sql[] = sprintf('SELECT * FROM %s', $es('state'));
if ($input->getOption('id')) {
- $where[] = $es(iFace::COLUMN_ID) . ' = :id';
+ $where[] = $es(iState::COLUMN_ID) . ' = :id';
$params['id'] = $input->getOption('id');
}
if ($input->getOption('via')) {
- $where[] = $es(iFace::COLUMN_VIA) . ' = :via';
+ $where[] = $es(iState::COLUMN_VIA) . ' = :via';
$params['via'] = $input->getOption('via');
}
if ($input->getOption('year')) {
- $where[] = $es(iFace::COLUMN_YEAR) . ' = :year';
+ $where[] = $es(iState::COLUMN_YEAR) . ' = :year';
$params['year'] = $input->getOption('year');
}
if ($input->getOption('type')) {
- $where[] = $es(iFace::COLUMN_TYPE) . ' = :type';
+ $where[] = $es(iState::COLUMN_TYPE) . ' = :type';
$params['type'] = match ($input->getOption('type')) {
- iFace::TYPE_MOVIE => iFace::TYPE_MOVIE,
- default => iFace::TYPE_EPISODE,
+ iState::TYPE_MOVIE => iState::TYPE_MOVIE,
+ default => iState::TYPE_EPISODE,
};
}
if ($input->getOption('title')) {
- $where[] = $es(iFace::COLUMN_TITLE) . ' LIKE "%" || :title || "%"';
+ $where[] = $es(iState::COLUMN_TITLE) . ' LIKE "%" || :title || "%"';
$params['title'] = $input->getOption('title');
}
if (null !== $input->getOption('season')) {
- $where[] = $es(iFace::COLUMN_SEASON) . ' = :season';
+ $where[] = $es(iState::COLUMN_SEASON) . ' = :season';
$params['season'] = $input->getOption('season');
}
if (null !== $input->getOption('episode')) {
- $where[] = $es(iFace::COLUMN_EPISODE) . ' = :episode';
+ $where[] = $es(iState::COLUMN_EPISODE) . ' = :episode';
$params['episode'] = $input->getOption('episode');
}
@@ -245,7 +264,7 @@ final class ListCommand extends Command
return self::INVALID;
}
- $where[] = "json_extract(" . iFace::COLUMN_PARENT . ",'$.{$parent}') = :parent";
+ $where[] = "json_extract(" . iState::COLUMN_PARENT . ",'$.{$parent}') = :parent";
$params['parent'] = array_values($d->getAll())[0];
}
@@ -260,7 +279,7 @@ final class ListCommand extends Command
return self::INVALID;
}
- $where[] = "json_extract(" . iFace::COLUMN_GUIDS . ",'$.{$guid}') = :guid";
+ $where[] = "json_extract(" . iState::COLUMN_GUIDS . ",'$.{$guid}') = :guid";
$params['guid'] = array_values($d->getAll())[0];
}
@@ -274,9 +293,9 @@ final class ListCommand extends Command
}
if ($input->getOption('exact')) {
- $where[] = "json_extract(" . iFace::COLUMN_META_DATA . ",'$.{$sField}') = :jf_metadata_value ";
+ $where[] = "json_extract(" . iState::COLUMN_META_DATA . ",'$.{$sField}') = :jf_metadata_value ";
} else {
- $where[] = "json_extract(" . iFace::COLUMN_META_DATA . ",'$.{$sField}') LIKE \"%\" || :jf_metadata_value || \"%\"";
+ $where[] = "json_extract(" . iState::COLUMN_META_DATA . ",'$.{$sField}') LIKE \"%\" || :jf_metadata_value || \"%\"";
}
$params['jf_metadata_value'] = $sValue;
@@ -292,9 +311,9 @@ final class ListCommand extends Command
}
if ($input->getOption('exact')) {
- $where[] = "json_extract(" . iFace::COLUMN_EXTRA . ",'$.{$sField}') = :jf_extra_value";
+ $where[] = "json_extract(" . iState::COLUMN_EXTRA . ",'$.{$sField}') = :jf_extra_value";
} else {
- $where[] = "json_extract(" . iFace::COLUMN_EXTRA . ",'$.{$sField}') LIKE \"%\" || :jf_extra_value || \"%\"";
+ $where[] = "json_extract(" . iState::COLUMN_EXTRA . ",'$.{$sField}') LIKE \"%\" || :jf_extra_value || \"%\"";
}
$params['jf_extra_value'] = $sValue;
@@ -389,7 +408,7 @@ final class ListCommand extends Command
}
foreach ($rows as &$row) {
- foreach (iFace::ENTITY_ARRAY_KEYS as $key) {
+ foreach (iState::ENTITY_ARRAY_KEYS as $key) {
if (null === ($row[$key] ?? null)) {
continue;
}
@@ -397,7 +416,7 @@ final class ListCommand extends Command
}
if (null !== ($via = $input->getOption('show-as'))) {
- $path = $row[iFace::COLUMN_META_DATA][$via] ?? [];
+ $path = $row[iState::COLUMN_META_DATA][$via] ?? [];
foreach (self::COLUMNS_CHANGEABLE as $column) {
if (null === ($path[$column] ?? null)) {
@@ -407,48 +426,80 @@ final class ListCommand extends Command
$row[$column] = 'int' === get_debug_type($row[$column]) ? (int)$path[$column] : $path[$column];
}
- if (null !== ($dateFromBackend = $path[iFace::COLUMN_META_DATA_PLAYED_AT] ?? $path[iFace::COLUMN_META_DATA_ADDED_AT] ?? null)) {
- $row[iFace::COLUMN_UPDATED] = $dateFromBackend;
+ if (null !== ($dateFromBackend = $path[iState::COLUMN_META_DATA_PLAYED_AT] ?? $path[iState::COLUMN_META_DATA_ADDED_AT] ?? null)) {
+ $row[iState::COLUMN_UPDATED] = $dateFromBackend;
}
}
- $row[iFace::COLUMN_WATCHED] = (bool)$row[iFace::COLUMN_WATCHED];
- $row[iFace::COLUMN_UPDATED] = makeDate($row[iFace::COLUMN_UPDATED]);
+ $row[iState::COLUMN_WATCHED] = (bool)$row[iState::COLUMN_WATCHED];
+ $row[iState::COLUMN_UPDATED] = makeDate($row[iState::COLUMN_UPDATED]);
}
unset($row);
if ('table' === $input->getOption('output')) {
- $list = [];
+ foreach ($rows as &$row) {
+ $row[iState::COLUMN_UPDATED] = $row[iState::COLUMN_UPDATED]->getTimestamp();
+ $row[iState::COLUMN_WATCHED] = (int)$row[iState::COLUMN_WATCHED];
+ $entity = Container::get(iState::class)->fromArray($row);
- 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);
-
- $item = [
+ $row = [
'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, '-'),
+ 'type' => ucfirst($entity->type),
+ 'title' => $entity->getName(),
+ 'via' => $entity->via ?? '??',
+ 'date' => makeDate($entity->updated)->format('Y-m-d H:i:s T'),
+ 'played' => $entity->isWatched() ? 'Yes' : 'No',
+ 'event' => ag($entity->extra[$entity->via] ?? [], iState::COLUMN_EXTRA_EVENT, '-'),
];
+ }
+ unset($row);
+ }
- $list[] = $item;
- $list[] = new TableSeparator();
+ $this->displayContent($rows, $output, $input->getOption('output'));
+
+ if (null !== $changeState && count($rows) >= 1) {
+ $changeState = strtolower($changeState);
+ $text = r(
+ <<Are you sure you want to mark [{total}] items as [{state}] ? [Y|N] [Default: No]
+ TEXT,
+ [
+ 'total' => count($rows),
+ 'state' => 'played' === $changeState ? 'Played' : 'Unplayed',
+ ]
+ );
+
+ $question = new ConfirmationQuestion($text . PHP_EOL . '> ', false);
+
+ if (false === $this->getHelper('question')->ask($input, $output, $question)) {
+ return self::FAILURE;
}
- $rows = null;
+ foreach ($rows ?? [] as $row) {
+ $entity = $this->mapper->get(
+ Container::get(iState::class)->fromArray([iState::COLUMN_ID => $row['id']])
+ );
- if (count($list) >= 2) {
- array_pop($list);
+ $entity->watched = 'played' === $changeState ? 1 : 0;
+ $entity->updated = time();
+ $entity->extra = ag_set($entity->getExtra(), $entity->via, [
+ iState::COLUMN_EXTRA_EVENT => 'cli.mark' . ($entity->isWatched() ? 'played' : 'unplayed'),
+ iState::COLUMN_EXTRA_DATE => (string)makeDate('now'),
+ ]);
+
+ $this->mapper->add($entity);
+
+ queuePush($entity);
}
- (new Table($output))->setHeaders(array_keys($list[0] ?? []))->setStyle('box')->setRows($list)->render();
- } else {
- $this->displayContent($rows, $output, $input->getOption('output'));
+ $output->writeln(
+ r('Successfully marked [{total}] items as [{state}].', [
+ 'total' => count($rows),
+ 'state' => 'played' === $changeState ? 'Played' : 'Unplayed',
+ ])
+ );
}
return self::SUCCESS;
@@ -477,7 +528,21 @@ final class ListCommand extends Command
$suggest = [];
- foreach ([iFace::TYPE_MOVIE, iFace::TYPE_EPISODE] as $name) {
+ foreach ([iState::TYPE_MOVIE, iState::TYPE_EPISODE] as $name) {
+ if (empty($currentValue) || str_starts_with($name, $currentValue)) {
+ $suggest[] = $name;
+ }
+ }
+
+ $suggestions->suggestValues($suggest);
+ }
+
+ if ($input->mustSuggestOptionValuesFor('mark-as')) {
+ $currentValue = $input->getCompletionValue();
+
+ $suggest = [];
+
+ foreach (['played', 'unplayed'] as $name) {
if (empty($currentValue) || str_starts_with($name, $currentValue)) {
$suggest[] = $name;
}