mirror of
https://github.com/koel/koel
synced 2024-11-10 06:34:14 +00:00
feat: convert smart playlist options to enums
This commit is contained in:
parent
4ecf947cc9
commit
f5677a5b2f
6 changed files with 109 additions and 116 deletions
34
app/Enums/SmartPlaylistModel.php
Normal file
34
app/Enums/SmartPlaylistModel.php
Normal file
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum SmartPlaylistModel: string
|
||||
{
|
||||
case TITLE = 'title';
|
||||
case ALBUM_NAME = 'album.name';
|
||||
case ARTIST_NAME = 'artist.name';
|
||||
case PLAY_COUNT = 'interactions.play_count';
|
||||
case LAST_PLAYED = 'interactions.last_played_at';
|
||||
case USER_ID = 'interactions.user_id';
|
||||
case LENGTH = 'length';
|
||||
case DATE_ADDED = 'created_at';
|
||||
case DATE_MODIFIED = 'updated_at';
|
||||
case GENRE = 'genre';
|
||||
case YEAR = 'year';
|
||||
|
||||
public function toColumnName(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::ALBUM_NAME => 'albums.name',
|
||||
self::ARTIST_NAME => 'artists.name',
|
||||
self::DATE_ADDED => 'songs.created_at',
|
||||
self::DATE_MODIFIED => 'songs.updated_at',
|
||||
default => $this->value,
|
||||
};
|
||||
}
|
||||
|
||||
public function isDate(): bool
|
||||
{
|
||||
return in_array($this, [self::LAST_PLAYED, self::DATE_ADDED, self::DATE_MODIFIED], true);
|
||||
}
|
||||
}
|
28
app/Enums/SmartPlaylistOperator.php
Normal file
28
app/Enums/SmartPlaylistOperator.php
Normal file
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum SmartPlaylistOperator: string
|
||||
{
|
||||
case IS = 'is';
|
||||
case IS_NOT = 'isNot';
|
||||
case CONTAINS = 'contains';
|
||||
case NOT_CONTAIN = 'notContain';
|
||||
case IS_BETWEEN = 'isBetween';
|
||||
case IS_GREATER_THAN = 'isGreaterThan';
|
||||
case IS_LESS_THAN = 'isLessThan';
|
||||
case BEGINS_WITH = 'beginsWith';
|
||||
case ENDS_WITH = 'endsWith';
|
||||
case IN_LAST = 'inLast';
|
||||
case NOT_IN_LAST = 'notInLast';
|
||||
case IS_NOT_BETWEEN = 'isNotBetween';
|
||||
|
||||
public function toEloquentClause(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::IS_BETWEEN => 'whereBetween',
|
||||
self::IS_NOT_BETWEEN => 'whereNotBetween',
|
||||
default => 'where',
|
||||
};
|
||||
}
|
||||
}
|
|
@ -2,69 +2,18 @@
|
|||
|
||||
namespace App\Values;
|
||||
|
||||
use App\Enums\SmartPlaylistModel;
|
||||
use App\Enums\SmartPlaylistOperator;
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
use Illuminate\Support\Str;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
final class SmartPlaylistRule implements Arrayable
|
||||
{
|
||||
public const OPERATOR_IS = 'is';
|
||||
public const OPERATOR_IS_NOT = 'isNot';
|
||||
public const OPERATOR_CONTAINS = 'contains';
|
||||
public const OPERATOR_NOT_CONTAIN = 'notContain';
|
||||
public const OPERATOR_IS_BETWEEN = 'isBetween';
|
||||
public const OPERATOR_IS_GREATER_THAN = 'isGreaterThan';
|
||||
public const OPERATOR_IS_LESS_THAN = 'isLessThan';
|
||||
public const OPERATOR_BEGINS_WITH = 'beginsWith';
|
||||
public const OPERATOR_ENDS_WITH = 'endsWith';
|
||||
public const OPERATOR_IN_LAST = 'inLast';
|
||||
public const OPERATOR_NOT_IN_LAST = 'notInLast';
|
||||
public const OPERATOR_IS_NOT_BETWEEN = 'isNotBetween';
|
||||
|
||||
public const VALID_OPERATORS = [
|
||||
self::OPERATOR_BEGINS_WITH,
|
||||
self::OPERATOR_CONTAINS,
|
||||
self::OPERATOR_ENDS_WITH,
|
||||
self::OPERATOR_IN_LAST,
|
||||
self::OPERATOR_IS,
|
||||
self::OPERATOR_IS_BETWEEN,
|
||||
self::OPERATOR_IS_NOT_BETWEEN,
|
||||
self::OPERATOR_IS_GREATER_THAN,
|
||||
self::OPERATOR_IS_LESS_THAN,
|
||||
self::OPERATOR_IS_NOT,
|
||||
self::OPERATOR_NOT_CONTAIN,
|
||||
self::OPERATOR_NOT_IN_LAST,
|
||||
];
|
||||
|
||||
private const MODEL_TITLE = 'title';
|
||||
public const MODEL_ALBUM_NAME = 'album.name';
|
||||
public const MODEL_ARTIST_NAME = 'artist.name';
|
||||
private const MODEL_PLAY_COUNT = 'interactions.play_count';
|
||||
public const MODEL_LAST_PLAYED = 'interactions.last_played_at';
|
||||
private const MODEL_USER_ID = 'interactions.user_id';
|
||||
private const MODEL_LENGTH = 'length';
|
||||
public const MODEL_DATE_ADDED = 'created_at';
|
||||
public const MODEL_DATE_MODIFIED = 'updated_at';
|
||||
private const MODEL_GENRE = 'genre';
|
||||
private const MODEL_YEAR = 'year';
|
||||
|
||||
private const VALID_MODELS = [
|
||||
self::MODEL_TITLE,
|
||||
self::MODEL_ALBUM_NAME,
|
||||
self::MODEL_ARTIST_NAME,
|
||||
self::MODEL_PLAY_COUNT,
|
||||
self::MODEL_LAST_PLAYED,
|
||||
self::MODEL_LENGTH,
|
||||
self::MODEL_DATE_ADDED,
|
||||
self::MODEL_DATE_MODIFIED,
|
||||
self::MODEL_GENRE,
|
||||
self::MODEL_YEAR,
|
||||
];
|
||||
|
||||
public string $id;
|
||||
public string $operator;
|
||||
public SmartPlaylistModel $model;
|
||||
public SmartPlaylistOperator $operator;
|
||||
public array $value;
|
||||
public string $model;
|
||||
|
||||
private function __construct(array $config)
|
||||
{
|
||||
|
@ -72,21 +21,25 @@ final class SmartPlaylistRule implements Arrayable
|
|||
|
||||
$this->id = $config['id'] ?? Str::uuid()->toString();
|
||||
$this->value = $config['value'];
|
||||
$this->model = $config['model'];
|
||||
$this->operator = $config['operator'];
|
||||
$this->model = SmartPlaylistModel::from($config['model']);
|
||||
$this->operator = SmartPlaylistOperator::from($config['operator']);
|
||||
}
|
||||
|
||||
/** @noinspection PhpExpressionResultUnusedInspection */
|
||||
public static function assertConfig(array $config, bool $allowUserIdModel = true): void
|
||||
{
|
||||
if ($config['id'] ?? null) {
|
||||
Assert::uuid($config['id']);
|
||||
}
|
||||
|
||||
Assert::oneOf($config['operator'], self::VALID_OPERATORS);
|
||||
Assert::oneOf(
|
||||
$config['model'],
|
||||
$allowUserIdModel ? array_prepend(self::VALID_MODELS, self::MODEL_USER_ID) : self::VALID_MODELS
|
||||
);
|
||||
SmartPlaylistOperator::from($config['operator']);
|
||||
|
||||
if (!$allowUserIdModel) {
|
||||
Assert::false($config['model'] === SmartPlaylistModel::USER_ID);
|
||||
}
|
||||
|
||||
SmartPlaylistModel::from($config['model']);
|
||||
|
||||
Assert::isArray($config['value']);
|
||||
Assert::countBetween($config['value'], 1, 2);
|
||||
}
|
||||
|
@ -101,8 +54,8 @@ final class SmartPlaylistRule implements Arrayable
|
|||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'model' => $this->model,
|
||||
'operator' => $this->operator,
|
||||
'model' => $this->model->value,
|
||||
'operator' => $this->operator->value,
|
||||
'value' => $this->value,
|
||||
];
|
||||
}
|
||||
|
|
|
@ -2,30 +2,14 @@
|
|||
|
||||
namespace App\Values;
|
||||
|
||||
use App\Enums\SmartPlaylistModel as Model;
|
||||
use App\Enums\SmartPlaylistOperator as Operator;
|
||||
use App\Values\SmartPlaylistRule as Rule;
|
||||
use Carbon\Carbon;
|
||||
use Closure;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
final class SmartPlaylistSqlElements
|
||||
{
|
||||
private const DATE_MODELS = [
|
||||
Rule::MODEL_LAST_PLAYED,
|
||||
Rule::MODEL_DATE_ADDED,
|
||||
Rule::MODEL_DATE_MODIFIED,
|
||||
];
|
||||
|
||||
private const MODEL_COLUMN_REMAP = [
|
||||
Rule::MODEL_ALBUM_NAME => 'albums.name',
|
||||
Rule::MODEL_ARTIST_NAME => 'artists.name',
|
||||
Rule::MODEL_DATE_ADDED => 'songs.created_at',
|
||||
Rule::MODEL_DATE_MODIFIED => 'songs.updated_at',
|
||||
];
|
||||
|
||||
private const CLAUSE_WHERE = 'where';
|
||||
private const CLAUSE_WHERE_BETWEEN = 'whereBetween';
|
||||
private const CLAUSE_WHERE_NOT_BETWEEN = 'whereNotBetween';
|
||||
|
||||
public string $clause;
|
||||
public array $parameters;
|
||||
|
||||
|
@ -43,44 +27,38 @@ final class SmartPlaylistSqlElements
|
|||
// If the rule is a date rule and the operator is "is" or "is not", we need to
|
||||
// convert the date to a range of dates and use the "between" or "not between" operator instead,
|
||||
// as we store dates as timestamps in the database.
|
||||
if (
|
||||
in_array($rule->model, self::DATE_MODELS, true) &&
|
||||
in_array($operator, [Rule::OPERATOR_IS, Rule::OPERATOR_IS_NOT], true)
|
||||
) {
|
||||
$operator = $operator === Rule::OPERATOR_IS ? Rule::OPERATOR_IS_BETWEEN : Rule::OPERATOR_IS_NOT_BETWEEN;
|
||||
if ($rule->model->isDate() && in_array($operator, [Operator::IS, Operator::IS_NOT], true)) {
|
||||
$operator = $operator === Operator::IS ? Operator::IS_BETWEEN : Operator::IS_NOT_BETWEEN;
|
||||
|
||||
$nextDay = Carbon::createFromFormat('Y-m-d', $value[0])->addDay()->format('Y-m-d');
|
||||
$value = [$value[0], $nextDay];
|
||||
}
|
||||
|
||||
$column = array_key_exists($rule->model, self::MODEL_COLUMN_REMAP)
|
||||
? self::MODEL_COLUMN_REMAP[$rule->model]
|
||||
: $rule->model;
|
||||
return new self(
|
||||
$operator->toEloquentClause(),
|
||||
...self::generateParameters($rule->model, $operator, $value)
|
||||
);
|
||||
}
|
||||
|
||||
$resolvers = [
|
||||
Rule::OPERATOR_BEGINS_WITH => [$column, 'LIKE', "$value[0]%"],
|
||||
Rule::OPERATOR_ENDS_WITH => [$column, 'LIKE', "%$value[0]"],
|
||||
Rule::OPERATOR_IS => [$column, '=', $value[0]],
|
||||
Rule::OPERATOR_IS_NOT => [$column, '<>', $value[0]],
|
||||
Rule::OPERATOR_CONTAINS => [$column, 'LIKE', "%$value[0]%"],
|
||||
Rule::OPERATOR_NOT_CONTAIN => [$column, 'NOT LIKE', "%$value[0]%"],
|
||||
Rule::OPERATOR_IS_LESS_THAN => [$column, '<', $value[0]],
|
||||
Rule::OPERATOR_IS_GREATER_THAN => [$column, '>', $value[0]],
|
||||
Rule::OPERATOR_IS_BETWEEN => [$column, $value],
|
||||
Rule::OPERATOR_IS_NOT_BETWEEN => [$column, $value],
|
||||
Rule::OPERATOR_NOT_IN_LAST => static fn () => [$column, '<', now()->subDays($value[0])],
|
||||
Rule::OPERATOR_IN_LAST => static fn () => [$column, '>=', now()->subDays($value[0])],
|
||||
];
|
||||
/** @return array<mixed> */
|
||||
private static function generateParameters(Model $model, Operator $operator, array $value): array
|
||||
{
|
||||
$column = $model->toColumnName();
|
||||
|
||||
Assert::keyExists($resolvers, $operator);
|
||||
|
||||
$clause = match ($operator) {
|
||||
Rule::OPERATOR_IS_BETWEEN => self::CLAUSE_WHERE_BETWEEN,
|
||||
Rule::OPERATOR_IS_NOT_BETWEEN => self::CLAUSE_WHERE_NOT_BETWEEN,
|
||||
default => self::CLAUSE_WHERE,
|
||||
$resolver = match ($operator) {
|
||||
Operator::BEGINS_WITH => [$column, 'LIKE', "$value[0]%"],
|
||||
Operator::ENDS_WITH => [$column, 'LIKE', "%$value[0]"],
|
||||
Operator::IS => [$column, '=', $value[0]],
|
||||
Operator::IS_NOT => [$column, '<>', $value[0]],
|
||||
Operator::CONTAINS => [$column, 'LIKE', "%$value[0]%"],
|
||||
Operator::NOT_CONTAIN => [$column, 'NOT LIKE', "%$value[0]%"],
|
||||
Operator::IS_LESS_THAN => [$column, '<', $value[0]],
|
||||
Operator::IS_GREATER_THAN => [$column, '>', $value[0]],
|
||||
Operator::IS_BETWEEN, Operator::IS_NOT_BETWEEN => [$column, $value],
|
||||
Operator::NOT_IN_LAST => static fn () => [$column, '<', now()->subDays($value[0])],
|
||||
Operator::IN_LAST => static fn () => [$column, '>=', now()->subDays($value[0])],
|
||||
};
|
||||
|
||||
$parameters = $resolvers[$operator] instanceof Closure ? $resolvers[$operator]() : $resolvers[$operator];
|
||||
|
||||
return new self($clause, ...$parameters);
|
||||
return $resolver instanceof Closure ? $resolver() : $resolver;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ class PlaylistTest extends PlusTestCase
|
|||
|
||||
$rule = SmartPlaylistRule::make([
|
||||
'model' => 'artist.name',
|
||||
'operator' => SmartPlaylistRule::OPERATOR_IS,
|
||||
'operator' => 'is',
|
||||
'value' => ['Bob Dylan'],
|
||||
]);
|
||||
|
||||
|
|
|
@ -52,7 +52,7 @@ class PlaylistTest extends TestCase
|
|||
|
||||
$rule = SmartPlaylistRule::make([
|
||||
'model' => 'artist.name',
|
||||
'operator' => SmartPlaylistRule::OPERATOR_IS,
|
||||
'operator' => 'is',
|
||||
'value' => ['Bob Dylan'],
|
||||
]);
|
||||
|
||||
|
@ -87,7 +87,7 @@ class PlaylistTest extends TestCase
|
|||
'rules' => [
|
||||
SmartPlaylistRule::make([
|
||||
'model' => 'artist.name',
|
||||
'operator' => SmartPlaylistRule::OPERATOR_IS,
|
||||
'operator' => 'is',
|
||||
'value' => ['Bob Dylan'],
|
||||
])->toArray(),
|
||||
],
|
||||
|
|
Loading…
Reference in a new issue